fix(deploy): correct the runner label, the vhost listen line and the cert paths
Some checks failed
deploy / deploy (push) Failing after 5m31s
Some checks failed
deploy / deploy (push) Failing after 5m31s
Three faults, any one of which would have failed the first deploy — found by
reading the house conventions in ~/git/architecture rather than by running it.
`runs-on: fedora-43-rust` is not a registered runner label. The catalogue in
gitea-runners.md §2 has `rust`; the job would never have been scheduled. Also
collapse build and deploy into one job: the `rust` image descends from
runner-fedora-44 and carries node, ssh and rsync, so the artifact upload and
download — on actions/*-artifact@v3, EOL upstream — bought nothing but a
round-trip and a directory-structure assumption. rpm.lair.cafe's working deploy
is single-job for the same reason.
The nginx vhost bound `listen 443 ssl`. TCP 443 on hanzalova belongs to the
stream SNI router (reverse-proxies.md §4-5, and the live bench.internal.conf
confirms it), so the vhost would never have been reached — the router answers
first with whichever certificate its default branch holds. `nginx -t` passes
either way, which is what makes this worth catching by reading rather than by
deploying. Now `listen 127.0.0.1:14443 ssl proxy_protocol;`.
Cert paths pointed at the host identity cert under /etc/pki/tls. A per-service
name needs its own cert (internal-tls.md §2): the host cert's SAN is bob's FQDN,
not tireless.internal, so verification would have failed. Now
/etc/nginx/tls/{cert,key}/tireless.internal.pem, minted with --san and renewed
by step@tireless.timer.
infra-setup.sh now provisions the ingress rather than describing it: mints the
cert through the JWK provisioner (removing the credential even on failure),
installs the vhost via sites-available + symlink, and registers the
split-horizon record on BOTH routers — a record on one router NXDOMAINs at the
other site. opn-cli has no reconfigure verb, so the apply is a direct API POST;
without it the name resolves only whenever Unbound next happens to reload.
Also fold the stale-bindings check into the workflow (closes the CI half of #8)
and let the runner unit fail without failing the deploy, since it correctly
refuses to start until the interactive login exists.
Refs #9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
This commit is contained in:
@@ -2,6 +2,12 @@ name: deploy
|
||||
|
||||
# The workflow is the source of infra truth: hosts, ports and paths live here,
|
||||
# not in a separate manifest (architecture/deployment-gitea-actions.md).
|
||||
#
|
||||
# One job, not build + deploy. The `rust` runner image is based on
|
||||
# runner-fedora-44, so it carries node, npm, ssh and rsync alongside the Rust
|
||||
# toolchain — everything this needs. Splitting the deploy onto a lighter runner
|
||||
# is a cost convenience (architecture/gitea-runners.md §3), and it would cost an
|
||||
# artifact round-trip to buy it. Keeping one job removes that entirely.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -23,8 +29,10 @@ env:
|
||||
VITE_API_BASE_URL: ""
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: fedora-43-rust
|
||||
deploy:
|
||||
# `rust` — not `fedora-43`, which has no cargo, and not `fedora-43-rust`,
|
||||
# which is not a registered label at all (architecture/gitea-runners.md §2).
|
||||
runs-on: rust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -36,8 +44,19 @@ jobs:
|
||||
- name: test
|
||||
run: cargo test --workspace
|
||||
|
||||
# The dashboard consumes TypeScript generated from the Rust domain types
|
||||
# by ts-rs during the test run. If the committed bindings no longer match,
|
||||
# fail here rather than let the dashboard build against a stale type.
|
||||
- name: generated bindings are current
|
||||
run: |
|
||||
if ! git diff --exit-code dashboard/src/api/generated; then
|
||||
echo "::error::generated TypeScript is stale."
|
||||
echo "Run 'cargo test -p tireless-entities' and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Static build so a runner newer than the target cannot produce a binary
|
||||
# the target's glibc rejects (§6 glibc skew).
|
||||
# the target's glibc rejects (architecture/deployment-gitea-actions.md §6).
|
||||
- name: build binaries
|
||||
run: cargo build --release --target x86_64-unknown-linux-musl
|
||||
|
||||
@@ -48,30 +67,13 @@ jobs:
|
||||
npm run lint
|
||||
npm run build
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: tireless
|
||||
path: |
|
||||
target/x86_64-unknown-linux-musl/release/tireless-api
|
||||
target/x86_64-unknown-linux-musl/release/tireless-worker
|
||||
target/x86_64-unknown-linux-musl/release/tireless
|
||||
dashboard/dist/
|
||||
asset/
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: fedora-43
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: tireless
|
||||
|
||||
- name: authorise
|
||||
env:
|
||||
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||
run: |
|
||||
install -d -m 0700 ~/.ssh
|
||||
printf '%s\n' "${{ secrets.RSYNC_SSH_KEY }}" > ~/.ssh/id_gitea_ci
|
||||
chmod 0600 ~/.ssh/id_gitea_ci
|
||||
cat >> ~/.ssh/config <<EOF
|
||||
printf '%s\n' "$RSYNC_SSH_KEY" | install -m 0600 /dev/stdin ~/.ssh/id_gitea_ci
|
||||
cat >> ~/.ssh/config <<'EOF'
|
||||
Host *
|
||||
IdentityFile ~/.ssh/id_gitea_ci
|
||||
StrictHostKeyChecking accept-new
|
||||
@@ -95,11 +97,12 @@ jobs:
|
||||
- name: ship artifacts
|
||||
run: |
|
||||
R="--rsync-path=sudo rsync --mkpath"
|
||||
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless-api \
|
||||
B=target/x86_64-unknown-linux-musl/release
|
||||
rsync $R --chmod 0755 "$B/tireless-api" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless-api
|
||||
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless-worker \
|
||||
rsync $R --chmod 0755 "$B/tireless-worker" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless-worker
|
||||
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless \
|
||||
rsync $R --chmod 0755 "$B/tireless" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless
|
||||
rsync $R --chmod 0640 config.toml \
|
||||
gitea_ci@"$API_HOST":/etc/tireless/config.toml
|
||||
@@ -115,8 +118,10 @@ jobs:
|
||||
- name: ship dashboard
|
||||
run: |
|
||||
# To the proxy, not to bob — that is where nginx serves it from.
|
||||
R="--rsync-path=sudo rsync --mkpath"
|
||||
rsync $R -a --delete dashboard/dist/ \
|
||||
# restorecon because a webroot not labelled httpd_sys_content_t
|
||||
# gives nginx a 403 (architecture/reverse-proxies.md §4).
|
||||
rsync --rsync-path="sudo rsync --mkpath" -a --delete \
|
||||
--chmod=D755,F644 dashboard/dist/ \
|
||||
gitea_ci@"$WEB_HOST":"$WEB_ROOT/"
|
||||
ssh gitea_ci@"$WEB_HOST" "sudo restorecon -R $WEB_ROOT"
|
||||
|
||||
@@ -127,7 +132,8 @@ jobs:
|
||||
sudo restorecon -R /usr/local/bin/tireless-api /usr/local/bin/tireless-worker \
|
||||
/usr/local/bin/tireless /etc/tireless /var/lib/tireless
|
||||
|
||||
# firewalld only learns a freshly-shipped service after a reload (§6).
|
||||
# firewalld only learns a freshly-shipped service after a reload
|
||||
# (architecture/deployment-gitea-actions.md §6).
|
||||
sudo firewall-cmd --reload
|
||||
zone=\$(sudo firewall-cmd --get-default-zone)
|
||||
sudo firewall-cmd --zone=\$zone --query-service=tireless-api \
|
||||
@@ -144,7 +150,12 @@ jobs:
|
||||
# job returns to the pool (design.md §4.2) — but it does cost the tokens
|
||||
# already spent. That is the accepted trade for a simple deploy; see
|
||||
# design.md §10 for why it bites hardest when tireless deploys itself.
|
||||
sudo systemctl restart tireless-runner.service
|
||||
#
|
||||
# It is expected to fail until the interactive Claude Code login has
|
||||
# been completed as the service account (script/infra-setup.sh step 1);
|
||||
# a runner with no credentials refuses to start rather than pretending.
|
||||
sudo systemctl restart tireless-runner.service || \
|
||||
echo "runner did not start — check 'tireless preflight' and the login"
|
||||
EOF
|
||||
|
||||
- name: health probe
|
||||
@@ -153,7 +164,7 @@ jobs:
|
||||
# A loopback probe on bob would pass even if firewalld were closed.
|
||||
ssh gitea_ci@"$WEB_HOST" \
|
||||
"curl -fsS http://$API_HOST:$API_PORT/v1/ready"
|
||||
for unit in tireless-api tireless-poller tireless-runner; do
|
||||
for unit in tireless-api tireless-poller; do
|
||||
ssh gitea_ci@"$API_HOST" "systemctl is-active \$unit.service"
|
||||
done
|
||||
|
||||
@@ -162,4 +173,4 @@ jobs:
|
||||
run: |
|
||||
ssh gitea_ci@"$API_HOST" \
|
||||
"journalctl -u tireless-api -u tireless-poller -u tireless-runner \
|
||||
--since '5 minutes ago' --no-pager"
|
||||
--since '5 minutes ago' --no-pager" || true
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
# tireless — mesh-only vhost on the office proxy (hanzalova.internal).
|
||||
#
|
||||
# Per-service internal cert, minted and renewed per architecture/internal-tls.md.
|
||||
# Static dashboard served directly; /v1 reverse-proxied to the API on bob.
|
||||
|
||||
# Serves the built dashboard from this host and reverse-proxies /v1 to the API
|
||||
# on bob across the mesh. Mesh-only: there is no public counterpart, because
|
||||
# tireless has no multi-user model and no audience outside the operator
|
||||
# (doc/plan/design.md §3.4).
|
||||
#
|
||||
# Installed by script/infra-setup.sh into sites-available/ with a symlink into
|
||||
# sites-enabled/; the deploy workflow only ships the SPA into the web root.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
server_name tireless.internal;
|
||||
|
||||
ssl_certificate /etc/pki/tls/misc/tireless.internal.pem;
|
||||
ssl_certificate_key /etc/pki/tls/private/tireless.internal.pem;
|
||||
# TCP 443 on this host is owned by the stream SNI router
|
||||
# (streams-enabled/sni-router.conf), which preads the server name and
|
||||
# forwards to the local https tier on 127.0.0.1:14443 over PROXY protocol.
|
||||
# A vhost that listens on :443 directly is never reached — the router
|
||||
# answers first, with whichever certificate its default branch holds.
|
||||
# `nginx -t` passes either way, so the mistake surfaces only as the wrong
|
||||
# certificate on an otherwise working handshake.
|
||||
# See architecture/reverse-proxies.md §4-5.
|
||||
listen 127.0.0.1:14443 ssl proxy_protocol;
|
||||
http2 on;
|
||||
|
||||
# Quantum-safe where the peer supports it, classical fallback otherwise
|
||||
# (architecture/generic.md §11).
|
||||
ssl_protocols TLSv1.3 TLSv1.2;
|
||||
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
|
||||
# Per-service internal cert, renewed by step@tireless.timer
|
||||
# (architecture/internal-tls.md §2-3). Not the host cert: its SAN is bob's
|
||||
# FQDN, not this service name.
|
||||
ssl_certificate /etc/nginx/tls/cert/tireless.internal.pem;
|
||||
ssl_certificate_key /etc/nginx/tls/key/tireless.internal.pem;
|
||||
ssl_trusted_certificate /etc/pki/ca-trust/source/anchors/root-internal.pem;
|
||||
ssl_protocols TLSv1.3;
|
||||
|
||||
root /var/www/tireless;
|
||||
index index.html;
|
||||
|
||||
# SPA: unknown paths resolve to the shell, which routes client-side.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /v1/ {
|
||||
proxy_pass http://bob.hanzalova.internal:23296;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -37,4 +44,9 @@ server {
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# SPA: unknown paths resolve to the shell, which routes client-side.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ API_PORT="${API_PORT:-23296}"
|
||||
WEB_HOST="${WEB_HOST:-hanzalova.internal}"
|
||||
WEB_ROOT="${WEB_ROOT:-/var/www/tireless}"
|
||||
RUNNER_PUBKEY="${RUNNER_PUBKEY:-$HOME/.ssh/id_gitea_ci.pub}"
|
||||
# JWK provisioner password for the internal CA, used only to mint the first
|
||||
# cert (architecture/internal-tls.md §4). Never left on a host.
|
||||
PROVISIONER_PW="${PROVISIONER_PW:-$HOME/.step/secrets/provisioner}"
|
||||
|
||||
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m warn\033[0m %s\n' "$*" >&2; }
|
||||
@@ -127,10 +130,155 @@ gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R ${WEB_ROOT}
|
||||
SUDOERS
|
||||
ssh "$host" "sudo visudo -cf /etc/sudoers.d/${APP}_web_gitea_ci"
|
||||
|
||||
mint_service_cert "$host" "$APP"
|
||||
install_vhost "$host"
|
||||
|
||||
info "$host ingress provisioned"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-service internal cert (architecture/internal-tls.md §4)
|
||||
#
|
||||
# The step@ timer only *renews*; the first cert is minted through the JWK
|
||||
# provisioner, whose password is shipped to the host only for as long as the
|
||||
# mint takes and removed even when it fails.
|
||||
# ---------------------------------------------------------------------------
|
||||
mint_service_cert() {
|
||||
local host="$1" name="$2"
|
||||
local cert="/etc/nginx/tls/cert/${name}.internal.pem"
|
||||
local key="/etc/nginx/tls/key/${name}.internal.pem"
|
||||
local root=/etc/pki/ca-trust/source/anchors/root-internal.pem
|
||||
|
||||
local state
|
||||
state=$(ssh "$host" "[ -f $cert ] && sudo step certificate verify $cert \
|
||||
--roots $root >/dev/null 2>&1 && echo valid || echo missing")
|
||||
|
||||
if [[ $state == valid ]]; then
|
||||
info "$name.internal cert already valid on $host"
|
||||
else
|
||||
[[ -f $PROVISIONER_PW ]] || fatal "provisioner password not found at $PROVISIONER_PW"
|
||||
info "minting $name.internal cert on $host"
|
||||
rsync -az --rsync-path='sudo rsync' --chmod=0600 \
|
||||
"$PROVISIONER_PW" "$host:/tmp/${name}-provisioner"
|
||||
# --san is mandatory: modern clients ignore CN, and a CN-only cert fails
|
||||
# with "no alternative certificate subject name matches target hostname".
|
||||
ssh "$host" "
|
||||
sudo install -d -m 0755 /etc/nginx/tls/cert /etc/nginx/tls/key
|
||||
rc=0
|
||||
sudo step ca certificate --force \
|
||||
--provisioner lair \
|
||||
--provisioner-password-file /tmp/${name}-provisioner \
|
||||
--ca-url https://ca.internal \
|
||||
--root $root \
|
||||
--san ${name}.internal \
|
||||
${name}.internal $cert $key || rc=\$?
|
||||
sudo rm -f /tmp/${name}-provisioner
|
||||
[ \$rc -eq 0 ] || { echo 'mint failed' >&2; exit \$rc; }
|
||||
sudo chown root:root $cert $key
|
||||
sudo chmod 644 $cert
|
||||
sudo chmod 640 $key
|
||||
sudo setfacl -m u:nginx:r $key"
|
||||
fi
|
||||
|
||||
# Harmless before the first mint — ExecCondition makes it a no-op.
|
||||
ssh "$host" "sudo systemctl enable --now step@${name}.timer"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# nginx vhost (architecture/reverse-proxies.md §4)
|
||||
# ---------------------------------------------------------------------------
|
||||
install_vhost() {
|
||||
local host="$1"
|
||||
local conf="tireless.internal.conf"
|
||||
|
||||
ssh "$host" "sudo install -d -o root -g root -m 0755 $WEB_ROOT"
|
||||
# A webroot not labelled httpd_sys_content_t gives nginx a 403. Deploys
|
||||
# inherit the directory's type, so labelling it once here is enough.
|
||||
ssh "$host" "sudo restorecon -R $WEB_ROOT"
|
||||
|
||||
rsync --rsync-path='sudo rsync' --chmod 0644 \
|
||||
"$(dirname "$0")/../asset/nginx/tireless.hanzalova.conf" \
|
||||
"$host:/etc/nginx/sites-available/$conf"
|
||||
ssh "$host" "sudo ln -sfn ../sites-available/$conf /etc/nginx/sites-enabled/$conf"
|
||||
|
||||
# `nginx -t` parses without binding, so it cannot see a port owned across
|
||||
# http{} and stream{}. It is necessary, not sufficient — hence the served-cert
|
||||
# check below rather than trusting the reload (internal-tls.md §3).
|
||||
ssh "$host" "sudo nginx -t" || fatal "nginx config test failed on $host"
|
||||
ssh "$host" "sudo systemctl reload nginx"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Split-horizon DNS, on BOTH routers
|
||||
#
|
||||
# A mesh host queries its own site's router, so a record added to only one
|
||||
# router resolves at that site and NXDOMAINs everywhere else — including the
|
||||
# operator's workstation if it is on the other site.
|
||||
# ---------------------------------------------------------------------------
|
||||
register_dns() {
|
||||
local proxy_ip
|
||||
proxy_ip=$(getent hosts "$WEB_HOST" | awk '{print $1; exit}')
|
||||
[[ -n $proxy_ip ]] || fatal "cannot resolve $WEB_HOST to give the routers an address"
|
||||
|
||||
for site in kosherinata hanzalova; do
|
||||
local cfg="$HOME/.opn-cli/${site}.yml"
|
||||
if [[ ! -f $cfg ]]; then
|
||||
warn "no opn-cli config for $site; register tireless.internal there by hand"
|
||||
continue
|
||||
fi
|
||||
info "registering tireless.internal -> $proxy_ip on $site"
|
||||
|
||||
# Idempotent: skip the create when the override is already there, because a
|
||||
# second create silently makes a *duplicate* override rather than failing,
|
||||
# and two overrides for one name is a confusing thing to debug later.
|
||||
#
|
||||
# The listing is a padded ASCII table: `| uuid | enabled | hostname |
|
||||
# domain | rr | …`, so hostname is field 4 and domain field 5 once split on
|
||||
# the pipe. Trim the padding before comparing.
|
||||
if opn-cli --config "$cfg" unbound host list 2>/dev/null | awk -F'|' '
|
||||
NF > 5 {
|
||||
h = $4; d = $5; gsub(/[[:space:]]/, "", h); gsub(/[[:space:]]/, "", d)
|
||||
if (h == "tireless" && d == "internal") found = 1
|
||||
}
|
||||
END { exit !found }'; then
|
||||
info " override already present on $site"
|
||||
else
|
||||
opn-cli --config "$cfg" unbound host create \
|
||||
--hostname tireless --domain internal --rr A --server "$proxy_ip"
|
||||
fi
|
||||
|
||||
# `create` only *saves* the override; Unbound keeps serving the old zone
|
||||
# until the service is reconfigured. opn-cli exposes no reconfigure verb
|
||||
# (it has only host/alias/domain), so call the API endpoint directly —
|
||||
# otherwise the name resolves whenever Unbound next happens to reload,
|
||||
# which looks like success on a slow enough check.
|
||||
apply_unbound "$cfg" "$site"
|
||||
done
|
||||
}
|
||||
|
||||
# POST /api/unbound/service/reconfigure, reading credentials from the opn-cli
|
||||
# config so there is one place they live.
|
||||
apply_unbound() {
|
||||
local cfg="$1" site="$2"
|
||||
local url key secret verify
|
||||
url=$(python3 -c "import yaml;print(yaml.safe_load(open('$cfg'))['url'])")
|
||||
key=$(python3 -c "import yaml;print(yaml.safe_load(open('$cfg'))['api_key'])")
|
||||
secret=$(python3 -c "import yaml;print(yaml.safe_load(open('$cfg'))['api_secret'])")
|
||||
verify=$(python3 -c "import yaml;print(yaml.safe_load(open('$cfg')).get('ssl_verify',True))")
|
||||
|
||||
local curl_opts=(-fsS -u "$key:$secret" -X POST -H 'Content-Type: application/json' -d '{}')
|
||||
[[ $verify == True ]] || curl_opts+=(-k)
|
||||
|
||||
if curl "${curl_opts[@]}" "$url/unbound/service/reconfigure" >/dev/null; then
|
||||
info " unbound reconfigured on $site"
|
||||
else
|
||||
warn "unbound reconfigure failed on $site — apply the pending change in the
|
||||
OPNsense UI, or tireless.internal will resolve only at the other site"
|
||||
fi
|
||||
}
|
||||
|
||||
provision_web_host "$WEB_HOST"
|
||||
register_dns
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Manual steps that cannot be automated
|
||||
@@ -166,19 +314,21 @@ Remaining one-time steps (operator, on the target host):
|
||||
The protection rule is what keeps an unattended agent from writing to main.
|
||||
Verify it rather than assuming it.
|
||||
|
||||
3. nginx vhost on the proxy.
|
||||
The deploy ships the built dashboard to the proxy's web root but does not
|
||||
install the vhost — that is one-time, and it needs a certificate:
|
||||
3. Confirm the proxy is serving the cert that is on disk.
|
||||
The vhost, its cert and the split-horizon DNS were installed above, but
|
||||
`systemctl reload nginx` exits 0 whatever nginx does with the SIGHUP — and
|
||||
these certs live 24 hours, so a reload that silently did not land shows up
|
||||
as an expired cert tomorrow, with every file on disk looking current
|
||||
(architecture/internal-tls.md §3). Verify by serial, not expiry:
|
||||
|
||||
- mint the per-service cert for tireless.internal per
|
||||
architecture/internal-tls.md;
|
||||
- copy asset/nginx/tireless.hanzalova.conf to the proxy's conf.d;
|
||||
- sudo nginx -t && sudo systemctl reload nginx
|
||||
served=$(echo | openssl s_client -servername tireless.internal \
|
||||
-connect hanzalova.internal:443 2>/dev/null \
|
||||
| openssl x509 -noout -serial)
|
||||
disk=$(ssh hanzalova.internal "sudo openssl x509 -noout -serial \
|
||||
-in /etc/nginx/tls/cert/tireless.internal.pem")
|
||||
[ "$served" = "$disk" ] || echo "stale: $served vs $disk"
|
||||
|
||||
The vhost serves the dashboard locally and reverse-proxies /v1 to
|
||||
bob:23296 across the mesh. That is why the API binds 0.0.0.0 rather than
|
||||
loopback, and why asset/firewalld/ opens the port. Those three facts are one
|
||||
decision — if you move ingress onto bob, change all three together.
|
||||
If they disagree, restart nginx rather than reloading it.
|
||||
|
||||
4. Postgres role and ident mapping (architecture/generic.md §5).
|
||||
On magrathea AND frankie:
|
||||
|
||||
Reference in New Issue
Block a user