Promote to production #252

Merged
jfraeys merged 14 commits from main into prod 2026-10-16 20:33:34 -04:00
Owner

Promotion: staging passed. Approve with ./infra approve

It re-checks this PR and CI, runs the production plan, and merges after you type the PR number. Merging is the approval record; it deploys nothing. Then git pull (or check out the validated commit) and run ./infra deploy (./infra deploy --tags ... --limit services for an Ansible-only change); the gate refuses any other commit.

Staging validation: passed on commit d7127bcf30 (this run)
Production drift check: clean

Ansible preview (staging clone, synthetic secrets — approximate, not exact prod values)


PLAY [Deploy the Docker-based services stack] **********************************
skipping: no hosts matched

PLAY RECAP *********************************************************************
### Promotion: staging passed. Approve with `./infra approve` It re-checks this PR and CI, runs the production plan, and merges after you type the PR number. Merging is the approval record; it deploys nothing. Then `git pull` (or check out the validated commit) and run `./infra deploy` (`./infra deploy --tags ... --limit services` for an Ansible-only change); the gate refuses any other commit. **Staging validation:** passed on commit d7127bcf30c620a9ea097740f255bc139220e812 (this run) **Production drift check:** clean ### Ansible preview (staging clone, synthetic secrets — approximate, not exact prod values) ``` PLAY [Deploy the Docker-based services stack] ********************************** skipping: no hosts matched PLAY RECAP ********************************************************************* ```
fix(headscale): only admin devices and tagged nodes reach SSH/LLDAP/Alertmanager/Forgejo; a client's PC reaches web:443
Some checks failed
Lint / lint (pull_request) Failing after 2m1s
Secret scan / scan (pull_request) Successful in 14s
5295aa2cfb
Eight ACL rules used src '*', so any device a user enrolled (any LLDAP user passes the Headscale OIDC login) could reach SSH on both hosts, the
LLDAP admin page, Alertmanager's UI (no login), Forgejo's git-SSH and web port and the CI cache. The onboarding runbook claimed 443 only.
Seven rules now take group:admin (headscale_admin_users) plus tag:web and tag:services (servers, ephemeral CI runners and staging nodes, which only
the infra user can create); web:443 stays open to everyone. The admin list accepts a user name or an email, because Headscale may identify an OIDC
user with an email by that email; unmatched entries are skipped, not errors.
Test: test_headscale_acl.py (5 cases; fails if a wildcard is put back). Policy checked with 'headscale policy check' on the live server (valid; it does not
validate membership). Runbook: control-server URL and the corrected access description.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
fix(authelia): only admins and client users can sign in to Headscale (and so enroll a tailnet device)
All checks were successful
Lint / lint (pull_request) Successful in 2m29s
Secret scan / scan (pull_request) Successful in 16s
cb5d288f01
The headscale OIDC client used the default one_factor policy, which lets any LLDAP user in. New authorization policy 'headscale': default deny;
allow group admin, authelia_headscale_extra_groups, and client-<slug>-admin / client-<slug>-employee for every authelia-gated client_sites entry
(the same groups the admin-panel rules already use). Existing devices keep working; only new logins are checked.
Validated with the real image: authelia/authelia:4.39.28 'validate-config' accepts the rendered config (only the existing deprecation warnings),
lists 'headscale' among the valid policy names, and rejects a client naming an undefined policy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
feat(manage): user roles/access/grant/revoke (LLDAP roles tied to the Authelia and Headscale access)
All checks were successful
Lint / lint (pull_request) Successful in 2m32s
Secret scan / scan (pull_request) Successful in 17s
2fd56cdb8d
Roles are LLDAP groups: admin; client-admin = client-<site>-admin; client = client-<site>-employee (the groups the sites' /admin/ rules and the
Headscale login policy already use). 'manage user roles' lists them and their holders; 'access <user>' shows groups, roles, admin panels, whether
they may sign in to Headscale, their tailnet user and devices, and whether the loaded ACL treats them as an admin; 'grant'/'revoke' change the group
(confirmation for admin, last admin cannot be revoked, dry run supported). Tailnet admin devices stay declarative (headscale_admin_users) and 'access'
reports when they disagree. user_access.sh is added to the manage_cli role's file list.
Tests: test-user-access.sh (stubbed LLDAP/Headscale; mutations of the last-admin guard and the role mapping are caught). Report run on the real data
(correctly says the live ACL has no admin group yet). shellcheck -S error clean.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Prometheus showed 'STOP (stopped, data preserved)' while running: the generic service loop compared container names exactly, but Compose names
them <project>-<service>-<n> (prometheus-prometheus-1), and /opt/prometheus made it 'deployed but not running'. The discovery functions now map
a Compose name to its service. Prometheus and Alertmanager (no Docker HEALTHCHECK) are added to the monitored table with /-/healthy probes run
inside their containers (both verified on the live host). Grafana and Loki, when enabled, would have hit the same bug.
Test: test-health-service-state.sh (8 cases; fails without the normalization).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
ci: run the health service-state test
All checks were successful
Lint / lint (pull_request) Successful in 2m30s
Secret scan / scan (pull_request) Successful in 15s
7769acddee
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
feat(alertmanager): UI behind Authelia (admin only); check that admin tools redirect to the login
Some checks failed
Lint / lint (pull_request) Has started running
Secret scan / scan (pull_request) Has been cancelled
970057b14a
Alertmanager's UI had no login of its own and was published on the tailnet address. alertmanager_hostname (alertmanager.<domain>, covered by the proxied
wildcard) now routes through Traefik with the authelia middleware and the admin-only access rule, like Prometheus, and no port is published;
external-url becomes the https link so alert links work from a phone after login. Unsetting the hostname restores the tailnet port. Prometheus reaches
Alertmanager by container name, so delivery is unaffected.
Tests: test_admin_tools_authelia.py renders the real Traefik, Authelia and compose templates (every admin-tool route has the authelia middleware; the
only routes without it are authelia, demo, forgejo; alertmanager listed admin-only; no port when it has a hostname); removing the middleware fails it.
scripts/checks/check-exposure.py now also checks that prometheus, alertmanager and vault /admin send an anonymous visitor to the Authelia login
(Cloudflare 403s Python's default user agent; Authelia answers non-browsers with 401 + Location).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
docs+ci: Alertmanager role notes match the new routing; run the admin-tools Authelia test in lint
All checks were successful
Lint / lint (pull_request) Successful in 2m31s
Secret scan / scan (pull_request) Successful in 18s
abd4cbd6ca
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
fix(headscale): drop the Alertmanager ACL rule (UI is behind Authelia, #250); fix yamllint blank line
All checks were successful
Lint / lint (pull_request) Successful in 2m30s
Secret scan / scan (pull_request) Successful in 15s
dee4cacc4c
Alertmanager's UI moves to alertmanager.<domain> behind Traefik + Authelia and no tailnet port is published, so the services:9093 rule is not needed. A test asserts
no rule targets it. The CI failure on this PR was yamllint: a blank line at the end of roles/headscale/defaults/main.yml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Merge pull request 'fix(headscale): restrict admin ports to an admin group and tagged nodes; client PCs reach web:443 only' (#246) from jfraeys/fix/headscale-acl-admin-group into main
All checks were successful
Staging Validation / validate (push) Successful in 11s
Staging Validation / promote (push) Has been skipped
e2a417f7e0
Merge pull request 'feat(alertmanager): UI behind Authelia, admin only; verify admin tools redirect to login' (#250) from jfraeys/fix/alertmanager-behind-authelia into main
All checks were successful
Staging Validation / validate (push) Successful in 5s
Staging Validation / promote (push) Has been skipped
1a3f148bad
Merge pull request 'fix(manage): health shows Prometheus as stopped while it runs; monitor Prometheus and Alertmanager' (#249) from jfraeys/fix/health-compose-names into main
All checks were successful
Staging Validation / validate (push) Successful in 5s
Staging Validation / promote (push) Has been skipped
1877cef0c3
Merge pull request 'fix(authelia): restrict the Headscale login to admin and client groups' (#247) from jfraeys/fix/headscale-oidc-groups into main
All checks were successful
Staging Validation / validate (push) Successful in 14m32s
Staging Validation / promote (push) Successful in 21s
e79afe7442
Merge main into jfraeys/feat/manage-user-access (lint.yml: keep both new test steps)
All checks were successful
Lint / lint (pull_request) Successful in 3m14s
Secret scan / scan (pull_request) Successful in 19s
ec3dcee5b7
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Merge pull request 'feat(manage): per-user access: roles, access, grant, revoke' (#248) from jfraeys/feat/manage-user-access into main
All checks were successful
Staging Validation / validate (push) Successful in 13m13s
Lint / lint (pull_request) Successful in 3m3s
Secret scan / scan (pull_request) Successful in 16s
Staging Validation / promote (push) Successful in 38s
d7127bcf30
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!252
No description provided.