Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
73f45a1
feat: add deployer host installer
Aug 8, 2026
ed8c5e6
fix: address deployer safety review findings
Aug 9, 2026
777f8c5
fix: harden deployer retained-state validation
Aug 9, 2026
19a0c4e
fix: retain deployed safety evidence
Aug 9, 2026
08e1d5d
fix: close deployer runtime recovery gaps
Aug 9, 2026
37d2962
test: normalize deployer drain marker mode
Aug 9, 2026
773c3ae
fix: serialize runtime policy and recovery state
Aug 9, 2026
61b3d0e
fix: fail closed across deployer recovery paths
Aug 9, 2026
5ddea4d
fix: preserve validated deployer boundaries
Aug 9, 2026
deb781c
fix: pin deployer transaction inputs atomically
Aug 9, 2026
817c0bf
fix: close deployer recovery and audit review gaps
Aug 9, 2026
9d78330
fix: harden deployer runtime and rollback recovery review gaps
Aug 9, 2026
efa5224
fix: close deployer isolation and rollback recovery review gaps
Aug 9, 2026
9e52f59
fix: harden deployer transactions, credentials, and audit identity
Aug 9, 2026
0242374
fix: track deployed pointer and validate state across recovery paths
Aug 9, 2026
c6cffb3
fix: validate deployer identity, isolation, and boundaries end to end
Aug 9, 2026
628791d
fix: preserve deployer recovery invariants across lifecycle paths
Aug 9, 2026
b471007
fix: close deployer publication, rollback, and uninstall safety gaps
Aug 10, 2026
25335bf
test: make drifted-unit uninstall check shellcheck-clean
Aug 10, 2026
9c8c5d3
fix: revalidate trust boundaries before every deployer adapter call
Aug 10, 2026
24c2b4d
fix: fail closed across deployer recovery, signals, and read-only checks
Aug 10, 2026
48c686e
fix: harden deployer publication, recovery manifests, and convergence
Aug 10, 2026
aa7f1db
fix: close deployer recovery, snapshot, inhibition, and checkout race…
Aug 11, 2026
aab1513
fix: drop redundant checkout ownership probe rejected on CI checkouts
Aug 11, 2026
4e0e1f7
fix: harden checkout pinning, promotion, and convergence validation
Aug 11, 2026
8c09079
test: expect credential-reference failure before cross-validation ord…
Aug 11, 2026
6fd3b5f
test: rotate credential fixture identity in deployed-pair drift regre…
Aug 11, 2026
63d5f80
test: match candidate-first credential failure on upgrade path
Aug 11, 2026
aa9b129
test: exercise deployed-pair credential drift through the policy refe…
Aug 11, 2026
0074803
test: clear recovered rollback state after credential drift regression
Aug 11, 2026
d973349
test: substituted live checkout now fails closed at the hash pin
Aug 11, 2026
cf07821
test: clear drain fixture before final convergence check
Aug 11, 2026
913e798
test: drop invalid trailing convergence check after approval rotation
Aug 11, 2026
f61c025
fix: close checkout baseline, pointer, audit, uninstall, and recovery…
Aug 11, 2026
8132bd5
fix: stop timers before removing the activation pointer on uninstall
Aug 11, 2026
22aeef4
fix: validate systemd boundary and finalize dependencies before recov…
Aug 11, 2026
91707e8
test: drifted adapter is caught at candidate validation before recovery
Aug 12, 2026
74fa0ff
test: tolerate absent retained pair in drifted-finalize regression
Aug 12, 2026
fd68749
test: tolerate absent retained pair in deployed-pair drift regressions
Aug 12, 2026
e9e5d7a
test: order deployed-pair drift regressions before finalize recovery
Aug 12, 2026
42254a9
test: rebuild retained rollback pair after finalize recovery fixture
Aug 12, 2026
2467f17
fix: replace-immune checkout pins, marker type checks, idempotent uni…
Aug 12, 2026
4326f6b
fix: propagate tree-read failures, tolerate absent timers, fsync cons…
Aug 12, 2026
3f038bd
fix: writability guard before backups, durable commit marker, stale-m…
Aug 12, 2026
4d22b56
fix: recovery boundary guards, durable journals, honest availability
Aug 12, 2026
26c468e
test: install-root symlink is caught by the existing symlink guard
Aug 12, 2026
6a69612
test: availability regression uses a drifted retained policy digest
Aug 12, 2026
b037938
fix: hook-free checkout validation, backup guards, durable state tran…
Aug 12, 2026
5945492
test: rebuild retained rollback pair before digest/credential drift f…
Aug 12, 2026
48c6c68
test: align rollback fixtures with publish-over-unusable-incumbent se…
Aug 12, 2026
4a5e2f3
fix: durable rollback publication, boundary-ordered recovery, exact c…
Aug 12, 2026
e736b1f
test: round-ten regressions for durability, boundaries, and checkout …
Aug 12, 2026
f4b3df2
fix: durable journal retirement, boundary sync before finalize, hones…
Aug 13, 2026
33d54ab
test: marker-committed rollback reports change; regression coverage
Aug 13, 2026
5e1b765
fix: durable restore retirement, failed-deploy audit, capability bind…
Aug 13, 2026
e574a7d
test: capability cross-installation rejection, reload-failure uninsta…
Aug 13, 2026
41601a3
fix: recovery without deployed dependencies, durable uninstall, key p…
Aug 13, 2026
f0ca8c4
test: uninstall without configuration directory removes the deploymen…
Aug 13, 2026
e5e722a
fix: fail-closed durability for journals, drain markers, consumption,…
Aug 13, 2026
c5097c4
fix: rollback core durable before commit marker, audit inode restore,…
Aug 13, 2026
edede66
test: audit-inode restore, drifted-timer uninstall stop, deployed-bou…
Aug 13, 2026
fc43923
fix: production gate, drift-unit role rejection, snapshot and availab…
Aug 13, 2026
acd287f
test: round-fifteen regressions; state-snapshot helper for recovery f…
Aug 13, 2026
d4f00d6
fix: reject ordinary-CI timers, timer-aware uninstall no-op, document…
Aug 13, 2026
146dd29
test: ordinary-CI drift timer rejection fixture
Aug 13, 2026
48a538d
fix: fail-closed mixed-role helper in runtime, policy snapshot remove…
Aug 13, 2026
1dc18a0
fix: timer enablement scope, Docker pull-in for scheduled services, r…
Aug 13, 2026
376eeae
test: repair replaces a damaged active release from the validated che…
Aug 13, 2026
399cd41
fix: marker after boundary durability, active-marker sync, timer prob…
Aug 13, 2026
42be82d
fix: inhibited adapter call, durable marker clearing, honest uninstal…
Aug 13, 2026
5275e21
fix: last-known-good validation before deploy, production rollback ga…
Aug 13, 2026
3938a7d
test: marker-deleting adapter cannot replay; regressions pass
Aug 13, 2026
b968ff2
fix: full retained-pair validation before deploy, audit prefix integr…
Aug 13, 2026
0ebff52
test: rollback baseline gate, consumption-marker replay guard, audit-…
Aug 13, 2026
53cf72b
fix: directory-level marker restore, full release validation, availab…
Aug 13, 2026
381ac8b
test: directory-level consumption restore, regressions for round twen…
Aug 13, 2026
f9ea431
fix: pointer restore ordering, full release predicate, audit prefix r…
Aug 13, 2026
ae6302c
fix: retire incumbent snapshot only after the durable success audit
Aug 13, 2026
f0b62de
fix: post-publication pointer truth, success-path marker preservation…
Aug 13, 2026
d119c29
fix: incumbent-based retirement, controller-side deployer guard, boun…
Aug 13, 2026
886a95d
test: controller installer rejects a deployer host
Aug 13, 2026
16d5851
fix: controller rollback guard, incumbent content restore, audit orde…
Aug 13, 2026
5c8302c
fix: shared role-admission lock, maintenance Docker sanitization, per…
Aug 13, 2026
d46ed5d
fix: incumbent metadata restore, audit metadata repair, retained-pair…
Aug 13, 2026
abb8dd7
fix: restore recovery metadata after adapter drift
Aug 14, 2026
ee99bfc
test: separate recovery metadata fixture paths
Aug 15, 2026
b58e8d2
fix: preserve deployer rollback across adapter updates
Aug 16, 2026
515b071
fix: retain completed deployment rollback marker
Aug 16, 2026
668213b
fix: accept canonical Debian os-release symlink
Aug 17, 2026
2ad48c7
test: exercise os-release symlink during install
Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions deploy/deployer/ci-fleet-deployer-cleanup.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[Unit]
Description=Run application-owned scoped deployer cleanup
After=docker.service
Comment thread
Nickfost marked this conversation as resolved.

[Service]
Type=oneshot
User=root
ExecStart=/opt/ci-fleet-deployer/current/scripts/deployer-runtime.sh cleanup
Environment=CI_FLEET_DEPLOYER_CONFIG=/var/lib/ci-fleet-deployer/active-policy.conf
TimeoutStartSec=15min
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/etc/ci-fleet-deployer /opt/ci-fleet-deployer
ReadWritePaths=/var/lib/ci-fleet-deployer /var/log/ci-fleet-deployer /var/lock/ci-fleet-deployer /run/docker.sock
RestrictSUIDSGID=yes
11 changes: 11 additions & 0 deletions deploy/deployer/ci-fleet-deployer-cleanup.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Run scoped ci-fleet deployer cleanup daily

[Timer]
OnCalendar=daily
RandomizedDelaySec=15min
Persistent=true
Unit=ci-fleet-deployer-cleanup.service

[Install]
WantedBy=timers.target
17 changes: 17 additions & 0 deletions deploy/deployer/ci-fleet-deployer-drain.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=Drain the ci-fleet deployer before maintenance

[Service]
Type=oneshot
User=root
ExecStart=/opt/ci-fleet-deployer/current/scripts/deployer-runtime.sh drain
Environment=CI_FLEET_DEPLOYER_CONFIG=/var/lib/ci-fleet-deployer/active-policy.conf
TimeoutStartSec=2min
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/etc/ci-fleet-deployer /opt/ci-fleet-deployer
ReadWritePaths=/var/lib/ci-fleet-deployer /var/log/ci-fleet-deployer /var/lock/ci-fleet-deployer
RestrictSUIDSGID=yes
18 changes: 18 additions & 0 deletions deploy/deployer/ci-fleet-deployer-health.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[Unit]
Description=Check ci-fleet deployer health
After=docker.service

[Service]
Type=oneshot
User=root
ExecStart=/opt/ci-fleet-deployer/current/scripts/deployer-runtime.sh health
Environment=CI_FLEET_DEPLOYER_CONFIG=/var/lib/ci-fleet-deployer/active-policy.conf
TimeoutStartSec=2min
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/etc/ci-fleet-deployer /opt/ci-fleet-deployer
ReadWritePaths=/var/lib/ci-fleet-deployer /var/log/ci-fleet-deployer /var/lock/ci-fleet-deployer /run/docker.sock
RestrictSUIDSGID=yes
12 changes: 12 additions & 0 deletions deploy/deployer/ci-fleet-deployer-health.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Check ci-fleet deployer health every five minutes

[Timer]
OnActiveSec=2min
OnUnitActiveSec=5min
RandomizedDelaySec=30s
Persistent=true
Unit=ci-fleet-deployer-health.service

[Install]
WantedBy=timers.target
24 changes: 24 additions & 0 deletions deploy/deployer/ci-fleet-deployer.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[Unit]
Description=Run one approved ci-fleet deployment
After=docker.service network-online.target
Wants=docker.service network-online.target
ConditionPathExists=!/var/lib/ci-fleet-deployer/drained

Comment thread
Nickfost marked this conversation as resolved.
[Service]
Type=oneshot
User=root
Group=root
ExecStart=/opt/ci-fleet-deployer/current/scripts/deployer-runtime.sh deploy
Comment thread
Nickfost marked this conversation as resolved.
Environment=CI_FLEET_DEPLOYER_CONFIG=/var/lib/ci-fleet-deployer/active-policy.conf
Environment=CI_FLEET_DEPLOYER_REQUEST=/var/lib/ci-fleet-deployer/request.conf
TimeoutStartSec=45min
TimeoutStopSec=45min
UMask=0077
Comment thread
Nickfost marked this conversation as resolved.
Outdated
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/etc/ci-fleet-deployer /opt/ci-fleet-deployer
ReadWritePaths=/var/lib/ci-fleet-deployer /var/log/ci-fleet-deployer /var/lock/ci-fleet-deployer /run/docker.sock
RestrictSUIDSGID=yes
LockPersonality=yes
312 changes: 312 additions & 0 deletions docs/DEPLOYER-HOST.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ New operator? Follow the [Quickstart](QUICKSTART.md): what ci-fleet does, instal
| Make a project compliant | [Project CI standard](PROJECT-STANDARD.md) and [compliance checklist](COMPLIANCE-CHECKLIST.md) |
| Split tests across parallel workers | [Project CI standard](PROJECT-STANDARD.md) and the [parallel workflow example](../examples/workflows/parallel-ci.yml.example) |
| Configure automatic updates and cleanup | [Host maintenance](HOST-MAINTENANCE.md) |
| Install or operate a dedicated deployment host | [Deployer host installation](DEPLOYER-HOST.md) |
| Monitor hosts and detect missed reports | [Fleet health monitoring](HEALTH-MONITORING.md) and [authenticated status reporting](STATUS-REPORTING.md) |
| Handle GitHub App, workflow, or deployment secrets | [Secrets model](SECRETS.md) and [security policy](../SECURITY.md) |
| Review accepted implementation scope | [Design decisions](DESIGN-DECISIONS.md) |
Expand All @@ -42,6 +43,7 @@ New operator? Follow the [Quickstart](QUICKSTART.md): what ci-fleet does, instal
| Test shard | One bounded slice of a larger test suite, designed to run independently and usually finish within five minutes. |
| Private delivery configuration | Repository allowlists, logical controllers, capacity budgets, environment policy, and required secret names kept in a private but secret-free repository. |
| Desired state | A reviewed schema-v3 declaration of runner pools and logical controllers stored in a private, secret-free Git repository. |
| Deployer host | A dedicated non-runner Linux host that applies exact-head approved immutable artifacts through application-owned deployment logic. |

## Design and boundaries

Expand Down Expand Up @@ -72,6 +74,7 @@ These pages are normative for compatible projects and hosts:
- [Run the live pilot](LIVE-PILOT.md)
- [Add a host](ADDING-A-HOST.md)
- [Install, adopt, check, upgrade, roll back, or remove a controller](DESIRED-STATE.md)
- [Install, check, repair, upgrade, drain, roll back, or remove a deployer](DEPLOYER-HOST.md)
- [Add a project](ADDING-A-PROJECT.md)
- [Deploy the current experimental prototype](DEPLOYMENT-PROTOTYPE.md)
- [Maintain, drain, clean, update, and reboot hosts](HOST-MAINTENANCE.md)
Expand Down
202 changes: 202 additions & 0 deletions scripts/deployer-runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
set -Eeuo pipefail
set +x

operation=${1:-}
case "$operation" in health|cleanup|deploy|rollback|drain) ;; *) printf 'ERROR: usage: deployer-runtime.sh health|cleanup|deploy|rollback|drain\n' >&2; exit 2 ;; esac
Comment thread
Nickfost marked this conversation as resolved.
Outdated

root=${CI_FLEET_DEPLOYER_ROOT:-}
testing=${CI_FLEET_DEPLOYER_TESTING:-0}
[[ -z "$root" || "$testing" == 1 ]] || { printf 'ERROR: alternate root is test-only\n' >&2; exit 2; }
if [[ "$testing" == 1 && -z "$root" ]]; then printf 'ERROR: test mode requires an alternate root\n' >&2; exit 2; fi
root_path() { printf '%s%s' "$root" "$1"; }
config=${CI_FLEET_DEPLOYER_CONFIG:-$(root_path /var/lib/ci-fleet-deployer/active-policy.conf)}
request=${CI_FLEET_DEPLOYER_REQUEST:-$(root_path /var/lib/ci-fleet-deployer/request.conf)}
state_root=$(root_path /var/lib/ci-fleet-deployer)
log_root=$(root_path /var/log/ci-fleet-deployer)
lock_dir=$(root_path /var/lock/ci-fleet-deployer)
evidence_dir=$(root_path /etc/ci-fleet-deployer/evidence)
active=$state_root/active-operation
drained=$state_root/drained
last_request=$state_root/last-request.conf
consumed_root=$state_root/consumed-requests
install_state=$state_root/install-state.json
deployed_policy=$state_root/deployed-policy.conf
deployed_state=$state_root/deployed-state.json
audit_log=$log_root/audit.log
systemd_root=$(root_path /etc/systemd/system)

die() { printf 'ERROR: %s\n' "$*" >&2; exit 2; }
expected_uid=0
[[ "$testing" != 1 ]] || expected_uid=$(id -u)
secure_file() {
local path=$1 description=$2 mode=${3:-600}
[[ ! -L "$path" && -f "$path" ]] || die "$description must be a regular file, not a symlink"
[[ $(realpath -e -- "$path") == $(realpath -m -- "$path") ]] || die "$description path contains a symlink"
[[ $(stat -c '%u:%a' "$path") == "$expected_uid:$mode" ]] || die "$description has unsafe owner or mode"
Comment thread
Nickfost marked this conversation as resolved.
}
secure_directory() {
local path=$1 description=$2
[[ ! -L "$path" && -d "$path" && $(stat -c '%u:%a' "$path") == "$expected_uid:700" ]] || die "$description has unsafe owner, mode, or type"
}
reject_mixed_role() {
local unit runner_unit line output expected="deployer|${cfg[DEPLOYER_IDENTITY]}"
for unit in ci-fleet-health.service ci-fleet-reconcile.service ci-fleet-cleanup.service actions.runner.service; do
[[ ! -e "$systemd_root/$unit" ]] || die 'ordinary CI controller or runner state is present'
done
shopt -s nullglob
for runner_unit in "$systemd_root"/actions.runner.*.service "$systemd_root"/multi-user.target.wants/actions.runner.*.service; do
shopt -u nullglob
[[ -n "$runner_unit" ]] && die 'ordinary GitHub Actions runner service is present'
done
shopt -u nullglob
for path in "$(root_path /etc/ci-fleet/ci-fleet.env)" "$(root_path /opt/ci-fleet/current)" "$(root_path /var/lib/ci-fleet/install-state.json)"; do
[[ ! -e "$path" && ! -L "$path" ]] || die 'ordinary CI controller or runner state is present'
done
output=$(docker ps -a --format '{{.ID}}|{{.Label "io.randomdevelopment.ci-fleet.role"}}|{{.Label "io.randomdevelopment.ci-fleet.identity"}}') || die 'Docker workload inventory failed'
while IFS= read -r line; do [[ -z "$line" || ${line#*|} == "$expected" ]] || die 'unrelated Docker workload is present'; done <<<"$output"
output=$(docker network ls --filter type=custom --format '{{.ID}}|{{.Label "io.randomdevelopment.ci-fleet.role"}}|{{.Label "io.randomdevelopment.ci-fleet.identity"}}') || die 'Docker network inventory failed'
while IFS= read -r line; do [[ -z "$line" || ${line#*|} == "$expected" ]] || die 'incompatible custom Docker network is present'; done <<<"$output"
output=$(docker volume ls --format '{{.Name}}|{{.Label "io.randomdevelopment.ci-fleet.role"}}|{{.Label "io.randomdevelopment.ci-fleet.identity"}}') || die 'Docker volume inventory failed'
while IFS= read -r line; do [[ -z "$line" || ${line#*|} == "$expected" ]] || die 'incompatible Docker volume is present'; done <<<"$output"
}
inside() {
local path=$1 base=$2
[[ $(realpath -m -- "$path") == "$(realpath -m -- "$base")/"* ]]
}
parse_file() {
local path=$1 prefix=$2 kind=$3 allowed=$4 line key value
declare -gA "$prefix=()"
local -n output=$prefix
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -n "$line" && ${line:0:1} != '#' ]] || continue
[[ "$line" == *=* ]] || die "malformed $kind line"
key=${line%%=*}; value=${line#*=}
[[ "$key" =~ ^[A-Z][A-Z0-9_]*$ && -n "$value" && "$value" != *$'\r'* ]] || die "malformed $kind line"
[[ " $allowed " == *" $key "* ]] || die "unknown $kind key"
[[ ! -v "output[$key]" ]] || die "duplicate $kind key"
# key indexes a nameref to an associative array.
# shellcheck disable=SC2004
output[$key]=$value
done <"$path"
}

secure_file "$config" 'deployer configuration'
config_keys='SCHEMA_VERSION CORE_REF ENVIRONMENT TARGET_ID DEPLOYER_IDENTITY ADAPTER_PATH ADAPTER_SHA256 CREDENTIAL_PROVIDER CREDENTIAL_REF CREDENTIAL_SCOPE APPROVAL_PROVIDER APPROVAL_EVIDENCE_PATH APPROVAL_CAPABILITY_EVIDENCE_PATH PRODUCTION_AUTHORIZATION_EVIDENCE_PATH CHECKPOINT_EVIDENCE_PATH SOURCE_COMMIT ARTIFACT_IMAGE NETWORK_HOST MIN_DISK_GIB REQUIRE_COMPOSE'
parse_file "$config" cfg configuration "$config_keys"
Comment thread
Nickfost marked this conversation as resolved.
for key in ENVIRONMENT TARGET_ID DEPLOYER_IDENTITY ADAPTER_PATH ADAPTER_SHA256 APPROVAL_PROVIDER CHECKPOINT_EVIDENCE_PATH SOURCE_COMMIT ARTIFACT_IMAGE; do [[ -v "cfg[$key]" ]] || die "configuration is missing $key"; done
[[ ${cfg[ENVIRONMENT]} =~ ^[a-z][a-z0-9-]{0,31}$ && ${cfg[TARGET_ID]} =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] || die 'invalid environment or target identity'
[[ ${cfg[ADAPTER_SHA256]} =~ ^[0-9a-f]{64}$ ]] || die 'invalid adapter digest'
secure_file "${cfg[ADAPTER_PATH]}" 'application adapter' 700
Comment thread
Nickfost marked this conversation as resolved.
[[ $(sha256sum "${cfg[ADAPTER_PATH]}" | cut -d' ' -f1) == "${cfg[ADAPTER_SHA256]}" ]] || die 'application adapter digest mismatch'
Comment thread
Nickfost marked this conversation as resolved.
Outdated

secure_directory "$state_root" 'deployer state directory'
secure_directory "$log_root" 'deployer log directory'
secure_directory "$lock_dir" 'deployer lock directory'
exec 9<"$lock_dir"
flock -n 9 || die 'another deployer operation is running'

case "$operation" in
drain)
[[ ! -e "$active" ]] || die 'active deployment prevents drain'
temporary=$(mktemp "$state_root/.drained.XXXXXX")
chmod 0600 "$temporary"
mv -Tf "$temporary" "$drained"
;;
health|rollback)
"${cfg[ADAPTER_PATH]}" "$operation"
Comment thread
Nickfost marked this conversation as resolved.
Outdated
;;
cleanup)
[[ ! -e "$drained" ]] || die 'deployer is drained'
"${cfg[ADAPTER_PATH]}" cleanup
Comment thread
Nickfost marked this conversation as resolved.
Outdated
;;
deploy)
[[ ! -e "$drained" ]] || die 'deployer is drained'
Comment thread
Nickfost marked this conversation as resolved.
Outdated
reject_mixed_role
Comment thread
Nickfost marked this conversation as resolved.
secure_file "$request" 'deployment request'
Comment thread
Nickfost marked this conversation as resolved.
request_keys='SCHEMA_VERSION ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE APPROVAL_IDENTITY POLICY_IDENTITY APPROVAL_ID APPROVED_AT'
parse_file "$request" req request "$request_keys"
for key in SCHEMA_VERSION ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE APPROVAL_IDENTITY POLICY_IDENTITY APPROVAL_ID APPROVED_AT; do
[[ -v "req[$key]" ]] || die "deployment request is missing $key"
done
[[ ${req[SCHEMA_VERSION]} == 1 ]] || die 'unsupported deployment request schema'
for key in ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE; do
[[ ${req[$key]} == "${cfg[$key]}" ]] || die "deployment request $key does not match installed policy"
done
Comment thread
Nickfost marked this conversation as resolved.
[[ ${req[SOURCE_COMMIT]} =~ ^[0-9a-f]{40}$ && ${req[ARTIFACT_IMAGE]} =~ ^[a-z0-9][a-z0-9.-]*(:[0-9]{1,5})?/[a-z0-9][a-z0-9._:/-]*@sha256:[0-9a-f]{64}$ ]] || die 'deployment request is not immutable and qualified'
for key in APPROVAL_IDENTITY POLICY_IDENTITY APPROVAL_ID; do [[ ${req[$key]} =~ ^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$ ]] || die "deployment request has an unsafe $key"; done
[[ ${req[APPROVED_AT]} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || die 'deployment request has an invalid approval time'
Comment thread
Nickfost marked this conversation as resolved.
Outdated
if [[ -e "$last_request" || -L "$last_request" ]]; then
secure_file "$last_request" 'last completed deployment request'
parse_file "$last_request" completed 'last completed deployment request' "$request_keys"
Comment thread
Nickfost marked this conversation as resolved.
replay=1
for key in $request_keys; do
[[ -v "completed[$key]" && ${req[$key]} == "${completed[$key]}" ]] || replay=0
done
((replay == 0)) || die 'deployment request was already completed'
fi
inside "${cfg[CHECKPOINT_EVIDENCE_PATH]}" "$evidence_dir" || die 'checkpoint evidence is outside the protected evidence directory'
secure_file "${cfg[CHECKPOINT_EVIDENCE_PATH]}" 'checkpoint evidence'
checkpoint_keys='SCHEMA_VERSION ENVIRONMENT TARGET_ID CHECKPOINT_ID RECORDED_AT'
parse_file "${cfg[CHECKPOINT_EVIDENCE_PATH]}" checkpoint 'checkpoint evidence' "$checkpoint_keys"
for key in $checkpoint_keys; do [[ -v "checkpoint[$key]" ]] || die "checkpoint evidence is missing $key"; done
[[ ${checkpoint[SCHEMA_VERSION]} == 1 && ${checkpoint[ENVIRONMENT]} == "${req[ENVIRONMENT]}" && ${checkpoint[TARGET_ID]} == "${req[TARGET_ID]}" ]] || die 'checkpoint evidence does not match the deployment target'
[[ ${checkpoint[CHECKPOINT_ID]} =~ ^[A-Za-z0-9._:@/-]{1,128}$ && ${checkpoint[RECORDED_AT]} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || die 'checkpoint evidence is malformed'
[[ -v 'cfg[APPROVAL_EVIDENCE_PATH]' ]] || die 'installed policy is missing approval evidence'
inside "${cfg[APPROVAL_EVIDENCE_PATH]}" "$evidence_dir" || die 'approval evidence is outside the protected evidence directory'
secure_file "${cfg[APPROVAL_EVIDENCE_PATH]}" 'approval evidence'
approval_keys='SCHEMA_VERSION ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE APPROVAL_IDENTITY POLICY_IDENTITY APPROVAL_ID APPROVED_AT'
parse_file "${cfg[APPROVAL_EVIDENCE_PATH]}" approved 'approval evidence' "$approval_keys"
Comment thread
Nickfost marked this conversation as resolved.
for key in $approval_keys; do
[[ -v "approved[$key]" && ${req[$key]} == "${approved[$key]}" ]] || die "deployment request does not match protected approval $key"
done
if [[ ${cfg[APPROVAL_PROVIDER]} == github-environment ]]; then
Comment thread
Nickfost marked this conversation as resolved.
Outdated
[[ -v 'cfg[APPROVAL_CAPABILITY_EVIDENCE_PATH]' ]] || die 'GitHub Environment approval is missing capability evidence'
inside "${cfg[APPROVAL_CAPABILITY_EVIDENCE_PATH]}" "$evidence_dir" || die 'capability evidence is outside the protected evidence directory'
secure_file "${cfg[APPROVAL_CAPABILITY_EVIDENCE_PATH]}" 'GitHub capability evidence'
capability_keys='SCHEMA_VERSION ENVIRONMENT_PROTECTION EXACT_HEAD CAPABILITY_ID CHECKED_AT'
parse_file "${cfg[APPROVAL_CAPABILITY_EVIDENCE_PATH]}" capability 'capability evidence' "$capability_keys"
for key in $capability_keys; do [[ -v "capability[$key]" ]] || die "capability evidence is missing $key"; done
[[ ${capability[SCHEMA_VERSION]} == 1 && ${capability[ENVIRONMENT_PROTECTION]} == verified && ${capability[EXACT_HEAD]} == "${req[SOURCE_COMMIT]}" ]] || die 'GitHub Environment capability evidence is not exact-head verified'
[[ ${capability[CAPABILITY_ID]} =~ ^[A-Za-z0-9._:@/-]{1,128}$ && ${capability[CHECKED_AT]} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || die 'GitHub Environment capability evidence is malformed'
fi
if [[ ${cfg[ENVIRONMENT]} == production ]]; then
[[ -v 'cfg[PRODUCTION_AUTHORIZATION_EVIDENCE_PATH]' ]] || die 'production policy is missing separate authorization evidence'
Comment thread
Nickfost marked this conversation as resolved.
Outdated
inside "${cfg[PRODUCTION_AUTHORIZATION_EVIDENCE_PATH]}" "$evidence_dir" || die 'production authorization evidence is outside the protected evidence directory'
secure_file "${cfg[PRODUCTION_AUTHORIZATION_EVIDENCE_PATH]}" 'production authorization evidence'
production_keys='SCHEMA_VERSION ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE AUTHORIZED_BY GATE_ID AUTHORIZED_AT'
parse_file "${cfg[PRODUCTION_AUTHORIZATION_EVIDENCE_PATH]}" production 'production authorization evidence' "$production_keys"
for key in $production_keys; do [[ -v "production[$key]" ]] || die "production authorization evidence is missing $key"; done
[[ ${production[SCHEMA_VERSION]} == 1 && ${production[ENVIRONMENT]} == production ]] || die 'production authorization evidence has the wrong scope'
[[ ${production[AUTHORIZED_BY]} =~ ^[A-Za-z0-9._:@/-]{1,128}$ && ${production[GATE_ID]} =~ ^[A-Za-z0-9._:@/-]{1,128}$ && ${production[AUTHORIZED_AT]} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || die 'production authorization evidence is malformed'
for key in ENVIRONMENT TARGET_ID SOURCE_COMMIT ARTIFACT_IMAGE; do
[[ -v "production[$key]" && ${req[$key]} == "${production[$key]}" ]] || die "deployment request does not match production authorization $key"
done
fi
if [[ -e "$consumed_root" || -L "$consumed_root" ]]; then secure_directory "$consumed_root" 'consumed request directory'; else install -d -m 0700 "$consumed_root"; fi
request_id=$(for key in $request_keys; do printf '%s=%s\0' "$key" "${req[$key]}"; done | sha256sum | cut -d' ' -f1)
consumed_marker=$consumed_root/$request_id
[[ ! -e "$consumed_marker" && ! -L "$consumed_marker" ]] || die 'deployment request was already consumed'
if [[ -e "$audit_log" || -L "$audit_log" ]]; then secure_file "$audit_log" 'deployer audit log'; else install -m 0600 /dev/null "$audit_log"; fi
: >>"$audit_log"
secure_file "$install_state" 'deployer install state'
for path in "$deployed_policy" "$deployed_state"; do [[ ! -e "$path" && ! -L "$path" ]] || secure_file "$path" 'deployed rollback state'; done
install -m 0600 "$config" "$state_root/.deployed-policy.new"
install -m 0600 "$install_state" "$state_root/.deployed-state.new"
install -m 0600 /dev/null "$consumed_marker"
Comment thread
Nickfost marked this conversation as resolved.
Outdated
Comment thread
Nickfost marked this conversation as resolved.
Outdated
umask 077
temporary=$(mktemp "$state_root/.active.XXXXXX")
Comment thread
Nickfost marked this conversation as resolved.
printf 'pid=%s\nstarted_at=%s\n' "$$" "$(date +%s)" >"$temporary"
mv -Tf "$temporary" "$active"
Comment thread
Nickfost marked this conversation as resolved.
Comment thread
Nickfost marked this conversation as resolved.
trap 'rm -f "$active" "$state_root/.deployed-policy.new" "$state_root/.deployed-state.new"' EXIT INT TERM
systemd-inhibit --what=shutdown:sleep --mode=block --who=ci-fleet-deployer \
--why='approved deployment is active' -- "${cfg[ADAPTER_PATH]}" deploy
Comment thread
Nickfost marked this conversation as resolved.
Outdated
Comment thread
Nickfost marked this conversation as resolved.
Outdated
Comment thread
Nickfost marked this conversation as resolved.
Outdated
mv -Tf "$state_root/.deployed-policy.new" "$deployed_policy"
mv -Tf "$state_root/.deployed-state.new" "$deployed_state"
Comment thread
Nickfost marked this conversation as resolved.
Outdated
mv -Tf "$request" "$last_request"
printf 'time=%s environment=%s target=%s source=%s artifact=%s approval=%s policy=%s result=success\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${req[ENVIRONMENT]}" "${req[TARGET_ID]}" \
"${req[SOURCE_COMMIT]}" "${req[ARTIFACT_IMAGE]#*@}" "${req[APPROVAL_ID]}" "${req[POLICY_IDENTITY]}" \
>>"$audit_log"
Comment thread
Nickfost marked this conversation as resolved.
Outdated
;;
esac
Loading