fix(https): restore TLS handshake — name hostname + correct PKI path
Some checks failed
Some checks failed
Closes #10. Two linked bugs in 26.4-alpha's Phase 1 HTTPS made the force-HTTPS toggle fatal: every SNI handshake on :443 died with SSL_ERROR_INTERNAL_ERROR_ALERT, so the toggle redirected users from working HTTP to broken HTTPS. Root cause 1: bare `:443 { tls internal }` gives Caddy no hostname to issue a leaf cert for, so /var/lib/caddy/certificates/ stayed empty and Caddy sent TLS `internal_error` on every handshake. Fix: the :443 block is now `__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ { tls internal }`, with the marker substituted by webinstaller/app.py at install time and by furtka.updater._refresh_caddyfile on self-update (reads /etc/hostname, falls back to "furtka"). `auto_https disable_redirects` keeps Caddy's built-in redirect out of the way of the /settings toggle. Root cause 2: furtka/https.py and the /rootCA.crt handler both referenced /var/lib/caddy/.local/share/caddy/pki/authorities/local/ — a path that doesn't exist. caddy.service sets XDG_DATA_HOME=/var/lib, so Caddy's storage is /var/lib/caddy/ directly. Fix: both paths corrected. Verified on the 192.168.178.110 smoke VM by swapping the Caddyfile in, reloading, handshaking, restoring: TLS 1.3 handshake succeeds, leaf cert issued under /var/lib/caddy/certificates/local/, /rootCA.crt returns 200. Tests: new cases assert the Caddyfile ships the hostname placeholder, the webinstaller substitutes it, _refresh_caddyfile re-substitutes from /etc/hostname on update, and the asset sets auto_https disable_redirects. Unit tests still stub the Caddy reload — the real handshake regression needs a smoke-VM integration test (follow-up, separate from this fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ae14f4108
commit
8fbe67ffb9
6 changed files with 126 additions and 25 deletions
|
|
@ -1,18 +1,33 @@
|
||||||
# Serves the Furtka landing page + live JSON on :80 (plain HTTP) and :443
|
# Serves the Furtka landing page + live JSON on :80 (plain HTTP) and on
|
||||||
# (HTTPS via Caddy's built-in `tls internal` — locally-issued certs signed
|
# HTTPS via Caddy's built-in `tls internal` — locally-issued certs signed
|
||||||
# by a root CA that Caddy generates on first start and stores under
|
# by a root CA that Caddy generates on first start and stores under
|
||||||
# /var/lib/caddy/.local/share/caddy/pki/authorities/local/). Static pages
|
# /var/lib/caddy/pki/authorities/local/. Static pages are read from
|
||||||
# are read from /opt/furtka/current/ — updates flip the symlink and
|
# /opt/furtka/current/ — updates flip the symlink and everything picks up
|
||||||
# everything picks up the new content without a Caddy restart (a
|
# the new content without a Caddy restart (a `systemctl reload caddy` is
|
||||||
# `systemctl reload caddy` is still triggered post-swap to flush the
|
# still triggered post-swap to flush the file-server's handle cache).
|
||||||
# file-server's handle cache). /apps and /api are reverse-proxied to the
|
# /apps and /api are reverse-proxied to the resource-manager API
|
||||||
# resource-manager API (furtka serve, bound to 127.0.0.1:7000).
|
# (furtka serve, bound to 127.0.0.1:7000).
|
||||||
|
#
|
||||||
|
# Hostname templating: __FURTKA_HOSTNAME__ gets substituted with the
|
||||||
|
# install-time hostname by webinstaller/app.py on first install and by
|
||||||
|
# furtka.updater._refresh_caddyfile on every self-update. A bare `:443
|
||||||
|
# { tls internal }` (no hostname) never triggers leaf-cert issuance, so
|
||||||
|
# SNI-based handshakes die with `SSL_ERROR_INTERNAL_ERROR_ALERT` — the
|
||||||
|
# 26.4-alpha regression this file exists to cure.
|
||||||
#
|
#
|
||||||
# Force-HTTPS: /etc/caddy/furtka.d/*.caddyfile gets imported into the :80
|
# Force-HTTPS: /etc/caddy/furtka.d/*.caddyfile gets imported into the :80
|
||||||
# block. The /api/furtka/https/force endpoint creates or removes
|
# block. The /api/furtka/https/force endpoint creates or removes
|
||||||
# redirect.caddyfile there to toggle the HTTP→HTTPS redirect, then reloads
|
# redirect.caddyfile there to toggle the HTTP→HTTPS redirect, then reloads
|
||||||
# Caddy. Glob imports silently no-op on an empty/missing directory, so the
|
# Caddy. Glob imports silently no-op on an empty/missing directory, so the
|
||||||
# toggle-off state is "no file present" rather than "empty file".
|
# toggle-off state is "no file present" rather than "empty file".
|
||||||
|
{
|
||||||
|
# Named-hostname :443 blocks would otherwise make Caddy add its own
|
||||||
|
# HTTP→HTTPS redirect — but we already serve our own `:80` block and
|
||||||
|
# the opt-in /settings toggle owns the redirect. Disable the built-in
|
||||||
|
# to keep a single source of truth.
|
||||||
|
auto_https disable_redirects
|
||||||
|
}
|
||||||
|
|
||||||
(furtka_routes) {
|
(furtka_routes) {
|
||||||
handle /api/* {
|
handle /api/* {
|
||||||
reverse_proxy localhost:7000
|
reverse_proxy localhost:7000
|
||||||
|
|
@ -38,7 +53,7 @@
|
||||||
# Available on both :80 and :443 so users can grab it before they've
|
# Available on both :80 and :443 so users can grab it before they've
|
||||||
# trusted it. The private key next to it stays 0600 / caddy-owned.
|
# trusted it. The private key next to it stays 0600 / caddy-owned.
|
||||||
handle /rootCA.crt {
|
handle /rootCA.crt {
|
||||||
root * /var/lib/caddy/.local/share/caddy/pki/authorities/local
|
root * /var/lib/caddy/pki/authorities/local
|
||||||
rewrite * /root.crt
|
rewrite * /root.crt
|
||||||
file_server
|
file_server
|
||||||
header Content-Type "application/x-x509-ca-cert"
|
header Content-Type "application/x-x509-ca-cert"
|
||||||
|
|
@ -59,7 +74,7 @@
|
||||||
import furtka_routes
|
import furtka_routes
|
||||||
}
|
}
|
||||||
|
|
||||||
:443 {
|
__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ {
|
||||||
tls internal
|
tls internal
|
||||||
import furtka_routes
|
import furtka_routes
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"""Local-CA HTTPS helpers for the `tls internal` setup.
|
"""Local-CA HTTPS helpers for the `tls internal` setup.
|
||||||
|
|
||||||
Caddy generates the local root CA lazily on first start and keeps it under
|
Caddy generates the local root CA lazily on first start and keeps it under
|
||||||
$XDG_DATA_HOME/caddy/pki/authorities/local/ — on the target that's
|
$XDG_DATA_HOME/caddy/pki/authorities/local/ — our packaged caddy.service
|
||||||
/var/lib/caddy/.local/share/caddy/pki/authorities/local/ (the caddy system
|
sets `XDG_DATA_HOME=/var/lib`, so on the target that resolves to
|
||||||
user's XDG_DATA_HOME resolves there). The private key stays 0600 /
|
/var/lib/caddy/pki/authorities/local/. The private key stays 0600 /
|
||||||
caddy-owned; we only ever read the public root.crt next to it.
|
caddy-owned; we only ever read the public root.crt next to it.
|
||||||
|
|
||||||
This module exposes two operations:
|
This module exposes two operations:
|
||||||
|
|
@ -18,7 +18,7 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
CA_CERT_PATH = Path("/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt")
|
CA_CERT_PATH = Path("/var/lib/caddy/pki/authorities/local/root.crt")
|
||||||
SNIPPET_DIR = Path("/etc/caddy/furtka.d")
|
SNIPPET_DIR = Path("/etc/caddy/furtka.d")
|
||||||
REDIRECT_SNIPPET = SNIPPET_DIR / "redirect.caddyfile"
|
REDIRECT_SNIPPET = SNIPPET_DIR / "redirect.caddyfile"
|
||||||
REDIRECT_CONTENT = "redir https://{host}{uri} permanent\n"
|
REDIRECT_CONTENT = "redir https://{host}{uri} permanent\n"
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ _CADDY_SNIPPET_DIR = Path(
|
||||||
os.environ.get("FURTKA_CADDY_SNIPPET_DIR", str(_CADDYFILE_LIVE.parent / "furtka.d"))
|
os.environ.get("FURTKA_CADDY_SNIPPET_DIR", str(_CADDYFILE_LIVE.parent / "furtka.d"))
|
||||||
)
|
)
|
||||||
_SYSTEMD_DIR = Path(os.environ.get("FURTKA_SYSTEMD_DIR", "/etc/systemd/system"))
|
_SYSTEMD_DIR = Path(os.environ.get("FURTKA_SYSTEMD_DIR", "/etc/systemd/system"))
|
||||||
|
_HOSTNAME_FILE = Path(os.environ.get("FURTKA_HOSTNAME_FILE", "/etc/hostname"))
|
||||||
|
_CADDYFILE_HOSTNAME_MARKER = "__FURTKA_HOSTNAME__"
|
||||||
|
|
||||||
|
|
||||||
class UpdateError(RuntimeError):
|
class UpdateError(RuntimeError):
|
||||||
|
|
@ -216,19 +218,38 @@ def _extract_tarball(tarball: Path, dest: Path) -> str:
|
||||||
return version_file.read_text().strip()
|
return version_file.read_text().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _current_hostname() -> str:
|
||||||
|
"""Read the box's hostname from /etc/hostname, falling back to 'furtka'.
|
||||||
|
|
||||||
|
Used to substitute the __FURTKA_HOSTNAME__ marker in the shipped Caddyfile
|
||||||
|
so Caddy's `tls internal` sees a real name to issue a leaf cert for.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
name = _HOSTNAME_FILE.read_text().strip()
|
||||||
|
except (FileNotFoundError, PermissionError, OSError):
|
||||||
|
return "furtka"
|
||||||
|
return name or "furtka"
|
||||||
|
|
||||||
|
|
||||||
def _refresh_caddyfile(source: Path) -> bool:
|
def _refresh_caddyfile(source: Path) -> bool:
|
||||||
"""Copy the shipped Caddyfile to /etc/caddy/ iff it differs. Returns True
|
"""Copy the shipped Caddyfile to /etc/caddy/ iff it differs. Returns True
|
||||||
if the file changed (so caddy needs more than a bare reload)."""
|
if the file changed (so caddy needs more than a bare reload).
|
||||||
|
|
||||||
|
Substitutes __FURTKA_HOSTNAME__ with the current hostname before comparing
|
||||||
|
and writing — same rendering the webinstaller applies at install time, so
|
||||||
|
a self-update lands byte-identical content when nothing else changed.
|
||||||
|
"""
|
||||||
if not source.is_file():
|
if not source.is_file():
|
||||||
return False
|
return False
|
||||||
# Snippet dir for the /api/furtka/https/force toggle. Pre-HTTPS installs
|
# Snippet dir for the /api/furtka/https/force toggle. Pre-HTTPS installs
|
||||||
# don't have this dir; ensure it so the Caddyfile's glob import can't
|
# don't have this dir; ensure it so the Caddyfile's glob import can't
|
||||||
# trip an older Caddy on a missing path during the first reload.
|
# trip an older Caddy on a missing path during the first reload.
|
||||||
_CADDY_SNIPPET_DIR.mkdir(mode=0o755, parents=True, exist_ok=True)
|
_CADDY_SNIPPET_DIR.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
if _CADDYFILE_LIVE.is_file() and source.read_bytes() == _CADDYFILE_LIVE.read_bytes():
|
rendered = source.read_text().replace(_CADDYFILE_HOSTNAME_MARKER, _current_hostname())
|
||||||
|
if _CADDYFILE_LIVE.is_file() and rendered == _CADDYFILE_LIVE.read_text():
|
||||||
return False
|
return False
|
||||||
_CADDYFILE_LIVE.parent.mkdir(parents=True, exist_ok=True)
|
_CADDYFILE_LIVE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy(source, _CADDYFILE_LIVE)
|
_CADDYFILE_LIVE.write_text(rendered)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ def updater(tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("FURTKA_LOCK_PATH", str(tmp_path / "update.lock"))
|
monkeypatch.setenv("FURTKA_LOCK_PATH", str(tmp_path / "update.lock"))
|
||||||
monkeypatch.setenv("FURTKA_CADDYFILE_PATH", str(tmp_path / "etc_caddy" / "Caddyfile"))
|
monkeypatch.setenv("FURTKA_CADDYFILE_PATH", str(tmp_path / "etc_caddy" / "Caddyfile"))
|
||||||
monkeypatch.setenv("FURTKA_SYSTEMD_DIR", str(tmp_path / "etc_systemd_system"))
|
monkeypatch.setenv("FURTKA_SYSTEMD_DIR", str(tmp_path / "etc_systemd_system"))
|
||||||
|
hostname_file = tmp_path / "etc_hostname"
|
||||||
|
hostname_file.write_text("testbox\n")
|
||||||
|
monkeypatch.setenv("FURTKA_HOSTNAME_FILE", str(hostname_file))
|
||||||
(tmp_path / "etc_systemd_system").mkdir()
|
(tmp_path / "etc_systemd_system").mkdir()
|
||||||
# Reload the module so the path constants pick up the env vars.
|
# Reload the module so the path constants pick up the env vars.
|
||||||
import importlib
|
import importlib
|
||||||
|
|
@ -206,6 +209,31 @@ def test_refresh_caddyfile_noops_if_source_missing(updater, tmp_path):
|
||||||
assert updater._refresh_caddyfile(tmp_path / "does-not-exist") is False
|
assert updater._refresh_caddyfile(tmp_path / "does-not-exist") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_caddyfile_substitutes_hostname_placeholder(updater, tmp_path):
|
||||||
|
# Self-update rewrites the shipped Caddyfile against the box's real
|
||||||
|
# hostname, same substitution the installer does on first boot. Without
|
||||||
|
# this the named-hostname :443 block ships with a literal
|
||||||
|
# `__FURTKA_HOSTNAME__` and Caddy refuses to load the config.
|
||||||
|
src = tmp_path / "src"
|
||||||
|
src.write_text(
|
||||||
|
"__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ {\n\ttls internal\n}\n"
|
||||||
|
)
|
||||||
|
assert updater._refresh_caddyfile(src) is True
|
||||||
|
live = updater._CADDYFILE_LIVE.read_text()
|
||||||
|
assert "testbox.local, testbox {" in live
|
||||||
|
assert "__FURTKA_HOSTNAME__" not in live
|
||||||
|
# Second call with the same source is a no-op — rendered content matches.
|
||||||
|
assert updater._refresh_caddyfile(src) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_hostname_falls_back_when_file_missing(updater, monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setenv("FURTKA_HOSTNAME_FILE", str(tmp_path / "missing"))
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
importlib.reload(updater)
|
||||||
|
assert updater._current_hostname() == "furtka"
|
||||||
|
|
||||||
|
|
||||||
def test_link_new_units_only_links_missing(updater, tmp_path, monkeypatch):
|
def test_link_new_units_only_links_missing(updater, tmp_path, monkeypatch):
|
||||||
unit_dir = tmp_path / "assets_systemd"
|
unit_dir = tmp_path / "assets_systemd"
|
||||||
unit_dir.mkdir()
|
unit_dir.mkdir()
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,10 @@ ASSETS = REPO_ROOT / "assets"
|
||||||
|
|
||||||
# (install target path, asset path under furtka/assets/) — only the files we
|
# (install target path, asset path under furtka/assets/) — only the files we
|
||||||
# still copy bit-for-bit at install time. Scripts + unit files are no longer
|
# still copy bit-for-bit at install time. Scripts + unit files are no longer
|
||||||
# copied; they're reached via /opt/furtka/current and `systemctl link`.
|
# copied; they're reached via /opt/furtka/current and `systemctl link`. The
|
||||||
|
# Caddyfile is not in this list because it's written with the hostname
|
||||||
|
# placeholder substituted — see test_post_install_substitutes_hostname_in_caddyfile.
|
||||||
ASSET_TARGETS = [
|
ASSET_TARGETS = [
|
||||||
("/etc/caddy/Caddyfile", "Caddyfile"),
|
|
||||||
("/var/lib/furtka/status.json", "www/status.json"),
|
("/var/lib/furtka/status.json", "www/status.json"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -123,11 +124,12 @@ def test_caddyfile_asset_serves_from_current():
|
||||||
|
|
||||||
def test_caddyfile_serves_both_http_and_https():
|
def test_caddyfile_serves_both_http_and_https():
|
||||||
# :80 stays so users who haven't installed the CA still reach the box;
|
# :80 stays so users who haven't installed the CA still reach the box;
|
||||||
# :443 uses Caddy's built-in local CA (tls internal) so users who have
|
# HTTPS is served via a named-hostname block so Caddy's `tls internal`
|
||||||
# installed it get the green padlock.
|
# has something to issue a leaf cert for. A bare `:443 { tls internal }`
|
||||||
|
# never triggers issuance — that was the 26.4-alpha regression.
|
||||||
caddy = (ASSETS / "Caddyfile").read_text()
|
caddy = (ASSETS / "Caddyfile").read_text()
|
||||||
assert ":80 {" in caddy
|
assert ":80 {" in caddy
|
||||||
assert ":443 {" in caddy
|
assert "__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ {" in caddy
|
||||||
assert "tls internal" in caddy
|
assert "tls internal" in caddy
|
||||||
# Shared routes live in a named snippet to avoid drift between the two
|
# Shared routes live in a named snippet to avoid drift between the two
|
||||||
# listeners — both site blocks must import it.
|
# listeners — both site blocks must import it.
|
||||||
|
|
@ -135,6 +137,14 @@ def test_caddyfile_serves_both_http_and_https():
|
||||||
assert caddy.count("import furtka_routes") == 2
|
assert caddy.count("import furtka_routes") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_caddyfile_disables_caddy_auto_redirects():
|
||||||
|
# Named-hostname :443 block makes Caddy want to add its own HTTP→HTTPS
|
||||||
|
# redirect. The /settings toggle is the single source of truth, so the
|
||||||
|
# built-in has to be off — otherwise the toggle and auto_https race.
|
||||||
|
caddy = (ASSETS / "Caddyfile").read_text()
|
||||||
|
assert "auto_https disable_redirects" in caddy
|
||||||
|
|
||||||
|
|
||||||
def test_caddyfile_imports_force_redirect_snippet_dir():
|
def test_caddyfile_imports_force_redirect_snippet_dir():
|
||||||
# The /api/furtka/https/force endpoint toggles HTTP→HTTPS by writing or
|
# The /api/furtka/https/force endpoint toggles HTTP→HTTPS by writing or
|
||||||
# removing a snippet file in this dir; the Caddyfile must glob-import it
|
# removing a snippet file in this dir; the Caddyfile must glob-import it
|
||||||
|
|
@ -146,13 +156,31 @@ def test_caddyfile_imports_force_redirect_snippet_dir():
|
||||||
def test_caddyfile_exposes_root_ca_download():
|
def test_caddyfile_exposes_root_ca_download():
|
||||||
# /rootCA.crt is the download handle the UI uses. It must map to the
|
# /rootCA.crt is the download handle the UI uses. It must map to the
|
||||||
# Caddy local-CA pki path and set a Content-Disposition so the browser
|
# Caddy local-CA pki path and set a Content-Disposition so the browser
|
||||||
# treats it as a download rather than trying to render it.
|
# treats it as a download rather than trying to render it. Path is the
|
||||||
|
# real one Caddy uses under XDG_DATA_HOME=/var/lib (see caddy.service
|
||||||
|
# Environment= directive) — not the /var/lib/caddy/.local/share/caddy/
|
||||||
|
# path Caddy docs show for non-systemd installs.
|
||||||
caddy = (ASSETS / "Caddyfile").read_text()
|
caddy = (ASSETS / "Caddyfile").read_text()
|
||||||
assert "handle /rootCA.crt" in caddy
|
assert "handle /rootCA.crt" in caddy
|
||||||
assert "/var/lib/caddy/.local/share/caddy/pki/authorities/local" in caddy
|
assert "/var/lib/caddy/pki/authorities/local" in caddy
|
||||||
|
assert ".local/share/caddy" not in caddy
|
||||||
assert "attachment; filename=furtka-local-rootCA.crt" in caddy
|
assert "attachment; filename=furtka-local-rootCA.crt" in caddy
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_install_substitutes_hostname_in_caddyfile(install_cmds):
|
||||||
|
# Fresh installs: the placeholder the asset ships with must be replaced
|
||||||
|
# with the hostname the user picked in the form. The `testhost` value
|
||||||
|
# comes from the install_cmds fixture. Without substitution Caddy's
|
||||||
|
# `tls internal` never issues a leaf cert for the real hostname.
|
||||||
|
caddyfile_cmd = next(
|
||||||
|
(c for c in install_cmds if " > /etc/caddy/Caddyfile" in c), None
|
||||||
|
)
|
||||||
|
assert caddyfile_cmd is not None
|
||||||
|
written = _extract_written_content(caddyfile_cmd, "/etc/caddy/Caddyfile")
|
||||||
|
assert "__FURTKA_HOSTNAME__" not in written
|
||||||
|
assert "testhost.local, testhost {" in written
|
||||||
|
|
||||||
|
|
||||||
def test_post_install_creates_furtka_d_snippet_dir(install_cmds):
|
def test_post_install_creates_furtka_d_snippet_dir(install_cmds):
|
||||||
# Pre-existing installs pick up the import path via updater._refresh_caddyfile,
|
# Pre-existing installs pick up the import path via updater._refresh_caddyfile,
|
||||||
# but fresh installs never run that — this command is the only guarantee
|
# but fresh installs never run that — this command is the only guarantee
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,16 @@ def _post_install_commands(hostname):
|
||||||
# (systemd unit points there). Content comes from the shipped asset,
|
# (systemd unit points there). Content comes from the shipped asset,
|
||||||
# which we copy in at install time so updates that change routing
|
# which we copy in at install time so updates that change routing
|
||||||
# need a new release to refresh it.
|
# need a new release to refresh it.
|
||||||
_write_file_cmd("/etc/caddy/Caddyfile", _read_asset("Caddyfile")),
|
#
|
||||||
|
# __FURTKA_HOSTNAME__ is the placeholder the asset carries in place
|
||||||
|
# of the real hostname — Caddy's `tls internal` needs a named site
|
||||||
|
# block to issue a leaf cert, and the hostname isn't known until
|
||||||
|
# the user fills in the form. Self-updates re-apply the same
|
||||||
|
# substitution against /etc/hostname (see updater._refresh_caddyfile).
|
||||||
|
_write_file_cmd(
|
||||||
|
"/etc/caddy/Caddyfile",
|
||||||
|
_read_asset("Caddyfile").replace("__FURTKA_HOSTNAME__", hostname),
|
||||||
|
),
|
||||||
# Initial status.json so Caddy doesn't 404 before furtka-status fires.
|
# Initial status.json so Caddy doesn't 404 before furtka-status fires.
|
||||||
_write_file_cmd("/var/lib/furtka/status.json", _read_asset("www/status.json")),
|
_write_file_cmd("/var/lib/furtka/status.json", _read_asset("www/status.json")),
|
||||||
nss_sed,
|
nss_sed,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue