-
Notifications
You must be signed in to change notification settings - Fork 0
feat: prepare status receiver deployment #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
c78ebfa
feat: prepare status receiver deployment
Nickfost 3083dc0
fix: harden receiver installation checks
Nickfost bc0cb26
fix: make receiver rollback transactional
Nickfost 78f545f
fix: fail closed during receiver activation
Nickfost 87b3de1
fix: couple receiver activation artifacts
Nickfost c935cc1
fix: bound receiver health and recovery
Nickfost 36bd1d7
Merge remote-tracking branch 'origin/main' into deploy/status-receiver
Nickfost b2f593c
fix: close status deployment preparation gaps
Nickfost 0a93d08
fix: harden required status configuration
Nickfost 18d9ba4
test: exercise required reporting as non-root
Nickfost 40c1892
fix: enforce immutable status releases
Nickfost 407dd7f
fix: verify receiver release contents
Nickfost 1eeed37
test: reject duplicate capability keys
Nickfost d5831f0
fix: close status reporting compatibility gaps
Nickfost b6f50ba
fix: enforce staged reporting adoption
Nickfost a72a947
fix: require proven reporting rollout
Nickfost cd094b5
fix: stage rollout evidence after engine selection
Nickfost 515a056
fix: stage reporting for new controllers
Nickfost 7d0ada2
fix: scope rollout evidence by controller
Nickfost 7b2d2e5
fix: invalidate stale rollout evidence
Nickfost f3ce598
fix: retry interrupted receiver restarts
Nickfost 471ea36
fix: clear completed receiver restarts
Nickfost 96119e7
fix: isolate alternate config evidence
Nickfost d3fa441
fix: reject symlinked policy inputs
Nickfost 317fd88
fix: gate required reporting capability
Nickfost 9e8ff3a
fix: preserve active receiver rollback
Nickfost 0ef34e9
fix: force recorded receiver restart
Nickfost 3fa7151
fix: return unavailable on report storage failure
Nickfost 5784ca3
fix: make receiver storage checks transactional
Nickfost 352cb17
fix: gate reporting on target engine evidence
Nickfost 46bfbca
fix: keep installed checks locally verifiable
Nickfost 53ccdfe
docs: verify receiver after activation
Nickfost 7893d3c
fix: exercise receiver database commits in health
Nickfost 0103a17
fix: retry receiver daemon reloads
Nickfost File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| [Unit] | ||
| Description=ci-fleet authenticated status receiver | ||
| After=network.target | ||
|
|
||
| [Service] | ||
| Type=simple | ||
| User=ci-fleet-status | ||
| Group=ci-fleet-status | ||
| ExecStart=/usr/bin/python3 /opt/ci-fleet-status/current/status_receiver.py --auth-config /etc/ci-fleet-status/auth.json --database /var/lib/ci-fleet-status/status.db --bind 127.0.0.1 --port 8080 | ||
| Restart=on-failure | ||
| RestartSec=5s | ||
| LogRateLimitIntervalSec=30s | ||
| LogRateLimitBurst=20 | ||
| UMask=0077 | ||
| NoNewPrivileges=yes | ||
| ProtectSystem=strict | ||
| ProtectHome=yes | ||
| PrivateTmp=yes | ||
| PrivateDevices=yes | ||
| ProtectKernelTunables=yes | ||
| ProtectKernelModules=yes | ||
| ProtectControlGroups=yes | ||
| RestrictSUIDSGID=yes | ||
| LockPersonality=yes | ||
| MemoryDenyWriteExecute=yes | ||
| CapabilityBoundingSet= | ||
| RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 | ||
| ReadOnlyPaths=/etc/ci-fleet-status | ||
| ReadWritePaths=/var/lib/ci-fleet-status | ||
|
|
||
| [Install] | ||
| WantedBy=multi-user.target | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Terminate TLS in the existing reverse proxy. Keep the application on loopback. | ||
| location / { | ||
| client_max_body_size 32k; | ||
| proxy_connect_timeout 5s; | ||
| proxy_read_timeout 20s; | ||
| proxy_send_timeout 20s; | ||
| proxy_pass http://127.0.0.1:8080; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Forwarded-Proto https; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| # Status receiver deployment | ||
|
|
||
| This runbook prepares the existing authenticated status receiver for a dedicated | ||
| Linux host. It does not create a host, provision credentials, change a controller, | ||
| or deploy anything by itself. | ||
|
|
||
| ## Target and boundaries | ||
|
|
||
| Use a small always-on VM or LXC separate from every runner controller: 2 vCPU, | ||
| 2–4 GiB RAM, and about 20 GiB disk. Confirm the next unused infrastructure ID | ||
| from the live hypervisor inventory immediately before creation; repository state | ||
| is not inventory evidence. | ||
|
|
||
| The receiver host has no Docker socket, runner credentials, deployment | ||
| credentials, or inbound connection to a controller. The Python application binds | ||
| only to `127.0.0.1:8080`; an existing reverse proxy terminates HTTPS. Controllers | ||
| submit outbound HTTPS. The read API is authenticated and has no mutation route. | ||
|
|
||
| ## Install from a reviewed commit | ||
|
|
||
| On the prepared receiver host, use the supported `/usr/bin/python3` version 3.9 | ||
| or newer, check out the exact reviewed commit, and verify a clean tree. The | ||
| installer creates the unprivileged `ci-fleet-status` account, release and state | ||
| directories, a hardened systemd unit, and an atomic `current` link. It does not | ||
| create credentials. | ||
|
|
||
| ```bash | ||
| ref=$(git rev-parse HEAD) | ||
| test -z "$(git status --porcelain)" | ||
| sudo ./scripts/install-status-receiver.sh --install --ref "$ref" | ||
| ``` | ||
|
|
||
| A second identical invocation returns `NO_CHANGE`. Before activation, verify the | ||
| unit definition: | ||
|
|
||
| ```bash | ||
| sudo systemd-analyze verify \ | ||
| /etc/systemd/system/ci-fleet-status-receiver.service | ||
| ``` | ||
|
|
||
| ## One-time secret provisioning boundary | ||
|
|
||
| Provision one independent 32–128 byte signing key per controller and one distinct | ||
| 32–128 byte visible-ASCII read token. Values never belong in Git, command | ||
| arguments, chat, logs, issues, PRs, fixtures, or artifacts. | ||
|
|
||
| An authorized human uses an approved secret manager or controlled provisioning | ||
| workstation to place the same controller key at these host-local paths: | ||
|
|
||
| - receiver: `/etc/ci-fleet-status/controller-keys/<controller-id>.key`; | ||
| - controller: `/etc/ci-fleet/secrets/status-reporting.key`. | ||
|
|
||
| The read token exists only at `/etc/ci-fleet-status/read-api.token`. On the | ||
| receiver, every key, token, and `auth.json` is owned by `ci-fleet-status` with | ||
| mode `0600`; both containing directories are mode `0700`. On the controller, the | ||
| signing key is root-owned mode `0600`. Verify ownership, type, and mode without | ||
| printing content. Stop and remove only the newly provisioned files if any check | ||
| fails. Delete any provisioning-workstation copy after both destinations are | ||
| verified. Do not enable SSH to provision or verify the controller. | ||
|
|
||
| Create receiver-local `auth.json` with an editor that does not log content. It | ||
| contains only controller-to-key-path mappings and the read-token path; use the | ||
| fictional shape in [status reporting](STATUS-REPORTING.md#receiver). Never put a | ||
| secret value in that JSON file. | ||
|
|
||
| ## HTTPS and activation | ||
|
|
||
| Install the location block from | ||
| `deploy/status-receiver/nginx-location.conf.example` in the existing HTTPS | ||
| reverse proxy. Supply the real public certificate and private endpoint only in | ||
| private infrastructure configuration. Do not expose port 8080. | ||
|
|
||
| After receiver-local credential metadata and reverse-proxy configuration pass: | ||
|
|
||
| ```bash | ||
| sudo systemctl daemon-reload | ||
| sudo systemctl enable --now ci-fleet-status-receiver.service | ||
| sudo systemctl is-active --quiet ci-fleet-status-receiver.service | ||
| sudo ./scripts/install-status-receiver.sh --check | ||
| python3 - <<'PY' | ||
| import json | ||
| import urllib.request | ||
|
|
||
| response = urllib.request.urlopen("http://127.0.0.1:8080/healthz", timeout=5) | ||
| assert json.load(response) == {"status": "ok"} | ||
| PY | ||
| ``` | ||
|
|
||
| Configure the controller's private monitoring policy with the HTTPS | ||
| `/v1/status` URL and its host-local signing-key path. Preserve its existing | ||
| identity, routing, capacity, resources, scale-to-zero behavior, and disabled SSH. | ||
| A reporting failure must remain warning-only and must not interrupt runner | ||
| management or reconciliation. | ||
|
|
||
| ## Verification | ||
|
|
||
| 1. Submit one scheduled report and confirm HTTP 202 without printing its body or | ||
| authorization headers. | ||
| 2. Read `/v1/controllers/<controller-id>` with the read token loaded from its | ||
| file by the client process, not placed in an argument or environment dump. | ||
| 3. Confirm an invalid signature, stale timestamp, replayed nonce, wrong | ||
| controller identity, and oversized payload are rejected. | ||
| 4. Restart the receiver and confirm `/healthz`, authenticated reads, and retained | ||
| bounded history recover. | ||
| 5. Stop the receiver for longer than one reporting interval. Confirm the | ||
| controller records only a reporting warning and continues reconciliation and | ||
| runner lifecycle; then restart the receiver and confirm reporting resumes. | ||
| 6. Stop the controller or take it offline. Confirm external monitoring of the | ||
| separate receiver still works and alerts on the latest report age. | ||
| 7. Confirm the receiver listens only on loopback and port 8080 is unreachable | ||
| externally. Confirm SSH remains disabled on the controller. | ||
|
|
||
| The receiver suppresses request logs. Keep systemd journal retention bounded by | ||
| the host's reviewed journald policy and monitor service restart count. SQLite | ||
| retention is enforced independently by age and per-controller count. The status | ||
| database is disposable. For database corruption, stop the service, quarantine the | ||
| database and its journal files outside service-writable state, and start with a | ||
| fresh database: | ||
|
|
||
| ```bash | ||
| sudo systemctl stop ci-fleet-status-receiver.service | ||
| quarantine="/var/lib/ci-fleet-status-installer/quarantine/$(date -u +%Y%m%dT%H%M%SZ)" | ||
| sudo install -d -o root -g root -m 0700 "$quarantine" | ||
| sudo find /var/lib/ci-fleet-status -maxdepth 1 -type f -name 'status.db*' \ | ||
| -exec mv -t "$quarantine" -- {} + | ||
| sudo systemctl start ci-fleet-status-receiver.service | ||
| ``` | ||
|
|
||
| A full host-loss recovery reinstalls the reviewed release and reprovisions | ||
| credentials. Back up the database only if an operator separately decides that | ||
| short status history is durable evidence. | ||
|
|
||
| ## Upgrade and rollback | ||
|
|
||
| From a clean checkout at the newer reviewed commit: | ||
|
|
||
| ```bash | ||
| ref=$(git rev-parse HEAD) | ||
| sudo ./scripts/install-status-receiver.sh --upgrade --ref "$ref" | ||
| sudo ./scripts/install-status-receiver.sh --check | ||
| ``` | ||
|
|
||
| The upgrade stages an immutable release, records the previous release, switches | ||
| the symlink atomically, and restarts only an already-active service. If health, | ||
| ingestion, read access, or retention verification fails: | ||
|
|
||
| ```bash | ||
| sudo ./scripts/install-status-receiver.sh --rollback | ||
| sudo ./scripts/install-status-receiver.sh --check | ||
| ``` | ||
|
|
||
| Rollback restores the selected release's application files and systemd unit. It | ||
| preserves `auth.json`, keys, read token, database, reverse-proxy configuration, | ||
| and journal policy. Reverse-proxy or schema changes require their own reviewed | ||
| compatibility and rollback plan. | ||
|
|
||
| ## Test coverage | ||
|
|
||
| `scripts/test-install-status-receiver.sh` exercises clean install, idempotent | ||
| rerun, upgrade, check, and rollback in an isolated root. Receiver tests cover | ||
| restart/key reload, incorrect secret permissions, authentication, controller | ||
| isolation, replay/freshness, request and payload bounds, strict schema/redaction, | ||
| retention, loopback binding, and read-only routes. Health and installer tests | ||
| cover warning-only reporter outages, disabled SSH reporting, and preservation of | ||
| runner lifecycle during delivery failure. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "schema_version": 1, | ||
| "capabilities": { | ||
| "status_reporting_config": true, | ||
| "required_status_reporting": true | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.