Skip to content

Commit a205f4a

Browse files
SecAI-Hubclaude
andcommitted
Add fail-closed pipeline, service auth, CSRF, supply chain pinning, and CI hardening (M26)
Phase 1 — Fail-closed quarantine pipeline: - Remote artifacts without pinned hashes now hard-fail (Stage 3) - Missing modelscan hard-fails when require_scan is true (Stage 5) - Scanner versions (modelscan, cosign, llama-server) recorded in all stage results - Watcher sends source URL, source revision, scanner versions, and policy hash to registry on promotion - Model catalog entries gain expected_sha256 and expected_size_bytes fields - Download path validates size/hash and restricts git-clone to allowlisted sources Phase 2 — Localhost binding & service-to-service auth: - Registry, tool-firewall, airlock default bind changed from 0.0.0.0 to 127.0.0.1 - Bootstrap service token generated at first boot (/run/secure-ai/service-token) - Mutating endpoints (promote, delete, reload) require Bearer token; read-only endpoints stay open - Graceful degradation: no token file = dev mode (no auth enforced) Phase 3 — CSRF & session hardening: - Double-submit cookie CSRF protection on all state-changing routes - Transparent fetch() patching in base.html auto-includes X-CSRF-Token header - Exemptions for service-to-service calls (valid Bearer token), pre-auth routes, and first-boot - Session regeneration on login, secret key rotation on passphrase change (invalidates all sessions) - Re-auth required for emergency panic level 2+ Phase 5 — Supply chain pinning: - All Containerfile base images digest-pinned (python, golang, alpine) - Python dependencies locked with pip-compile --generate-hashes - Recipe base image pinning guidance added - Container pin and action pin verification scripts Phase 6 — CI/CD hardening: - All GitHub Actions SHA-pinned with version comments - CodeQL scanning workflow for Go and Python - SBOM generation (CycloneDX) and cosign attestation in build workflow - Pin enforcement CI job Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 95cebed commit a205f4a

29 files changed

Lines changed: 1184 additions & 83 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
# Verify all GitHub Actions use SHA-pinned versions
3+
set -euo pipefail
4+
ERRORS=0
5+
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
6+
[ -f "$f" ] || continue
7+
while IFS= read -r line; do
8+
# Match "uses: owner/repo@" lines that don't have a 40-char hex SHA
9+
if echo "$line" | grep -qE '^\s*-?\s*uses:\s+[^/]+/[^@]+@' && \
10+
! echo "$line" | grep -qE '@[0-9a-f]{40}(\s|$)'; then
11+
echo "ERROR: $f has unpinned action: $(echo "$line" | sed 's/^[[:space:]]*//')"
12+
ERRORS=$((ERRORS + 1))
13+
fi
14+
done < "$f"
15+
done
16+
if [ $ERRORS -gt 0 ]; then
17+
echo "FAIL: $ERRORS unpinned action(s) found"
18+
exit 1
19+
fi
20+
echo "OK: All actions are SHA-pinned"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
# Verify all Containerfiles use digest-pinned base images.
3+
# Usage: .github/scripts/check-container-pins.sh
4+
set -euo pipefail
5+
6+
ERRORS=0
7+
WARNINGS=0
8+
9+
for f in $(find services/ -name 'Containerfile' -o -name 'Dockerfile'); do
10+
while IFS= read -r line; do
11+
# Skip comments and empty lines
12+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
13+
[[ -z "$line" ]] && continue
14+
15+
if echo "$line" | grep -qE '^FROM '; then
16+
# Allow ARG-interpolated tags (e.g. ${COMPUTE}) with a warning
17+
if echo "$line" | grep -q '\${'; then
18+
if ! echo "$line" | grep -q '@sha256:'; then
19+
echo "WARN: $f has dynamic unpinned base image: $line"
20+
WARNINGS=$((WARNINGS + 1))
21+
fi
22+
continue
23+
fi
24+
25+
if ! echo "$line" | grep -q '@sha256:'; then
26+
echo "ERROR: $f has unpinned base image: $line"
27+
ERRORS=$((ERRORS + 1))
28+
fi
29+
fi
30+
done < "$f"
31+
done
32+
33+
if [ $WARNINGS -gt 0 ]; then
34+
echo "WARN: $WARNINGS dynamic base image(s) could not be verified (use per-variant pinning)"
35+
fi
36+
37+
if [ $ERRORS -gt 0 ]; then
38+
echo "FAIL: $ERRORS unpinned base image(s) found"
39+
exit 1
40+
fi
41+
42+
echo "OK: All static base images are digest-pinned"

.github/workflows/build.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,28 @@ jobs:
2929
- recipe.yml
3030
steps:
3131
- name: Build Custom Image
32-
uses: blue-build/github-action@v1.11
32+
uses: blue-build/github-action@24d146df25adc2cf579e918efe2d9bff6adea408 # v1.11.1
3333
with:
3434
recipe: ${{ matrix.recipe }}
3535
cosign_private_key: ${{ secrets.SIGNING_SECRET }}
3636
registry_token: ${{ github.token }}
3737
pr_event_number: ${{ github.event.number }}
3838
maximize_build_space: true
39+
40+
- name: Generate SBOM
41+
if: github.event_name != 'pull_request'
42+
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
43+
with:
44+
image: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
45+
format: cyclonedx-json
46+
output-file: sbom.cdx.json
47+
48+
- name: Attest SBOM
49+
if: github.event_name != 'pull_request'
50+
run: |
51+
cosign attest --type cyclonedx \
52+
--predicate sbom.cdx.json \
53+
--key env://COSIGN_PRIVATE_KEY \
54+
ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
55+
env:
56+
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}

.github/workflows/ci.yml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ jobs:
2828
matrix:
2929
service: [registry, tool-firewall, airlock]
3030
steps:
31-
- uses: actions/checkout@v4
32-
- uses: actions/setup-go@v5
31+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
3333
with:
3434
go-version: "1.23"
3535
cache-dependency-path: services/${{ matrix.service }}/go.sum
@@ -52,8 +52,8 @@ jobs:
5252
permissions:
5353
contents: read
5454
steps:
55-
- uses: actions/checkout@v4
56-
- uses: actions/setup-go@v5
55+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
56+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
5757
with:
5858
go-version: "1.23"
5959
cache-dependency-path: services/registry/go.sum
@@ -68,8 +68,8 @@ jobs:
6868
permissions:
6969
contents: read
7070
steps:
71-
- uses: actions/checkout@v4
72-
- uses: actions/setup-python@v5
71+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
72+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
7373
with:
7474
python-version: "3.12"
7575

@@ -94,7 +94,7 @@ jobs:
9494
permissions:
9595
contents: read
9696
steps:
97-
- uses: actions/checkout@v4
97+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
9898

9999
- name: Lint shell scripts
100100
run: |
@@ -111,8 +111,8 @@ jobs:
111111
permissions:
112112
contents: read
113113
steps:
114-
- uses: actions/checkout@v4
115-
- uses: actions/setup-python@v5
114+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
115+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
116116
with:
117117
python-version: "3.12"
118118

@@ -135,3 +135,12 @@ jobs:
135135
errors += 1
136136
sys.exit(errors)
137137
"
138+
139+
check-pins:
140+
name: Verify action pins
141+
runs-on: ubuntu-latest
142+
permissions:
143+
contents: read
144+
steps:
145+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
146+
- run: bash .github/scripts/check-action-pins.sh

.github/workflows/codeql.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CodeQL Analysis
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: '0 6 * * 1' # Weekly Monday 6am UTC
10+
11+
permissions:
12+
security-events: write
13+
contents: read
14+
15+
jobs:
16+
analyze-go:
17+
name: Analyze Go
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21+
- uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
22+
with:
23+
languages: go
24+
- uses: github/codeql-action/autobuild@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
25+
- uses: github/codeql-action/analyze@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
26+
with:
27+
category: go
28+
29+
analyze-python:
30+
name: Analyze Python
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34+
- uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
35+
with:
36+
languages: python
37+
- uses: github/codeql-action/autobuild@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
38+
- uses: github/codeql-action/analyze@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6
39+
with:
40+
category: python

files/system/usr/lib/systemd/system/secure-ai-airlock.service

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ Environment=BIND_ADDR=127.0.0.1:8490
1010
Environment=POLICY_PATH=/etc/secure-ai/policy/policy.yaml
1111
Environment=SOURCES_ALLOWLIST_PATH=/etc/secure-ai/policy/sources.allowlist.yaml
1212
Environment=AUDIT_LOG_PATH=/var/lib/secure-ai/logs/airlock-audit.jsonl
13+
Environment=SERVICE_TOKEN_PATH=/run/secure-ai/service-token
1314

1415
# Filesystem isolation
1516
DynamicUser=yes
1617
LogsDirectory=secure-ai
1718
ReadOnlyPaths=/etc/secure-ai
19+
ReadOnlyPaths=/run/secure-ai
1820
ReadWritePaths=/var/lib/secure-ai/logs
1921
# NOTE: NO PrivateNetwork — airlock is the ONLY service with outbound access
2022
PrivateTmp=yes

files/system/usr/lib/systemd/system/secure-ai-quarantine-watcher.service

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ Environment=POLICY_PATH=/etc/secure-ai/policy/policy.yaml
1313
Environment=MODELS_LOCK_PATH=/etc/secure-ai/policy/models.lock.yaml
1414
Environment=AUDIT_LOG_PATH=/var/lib/secure-ai/logs/quarantine-audit.jsonl
1515
Environment=LLAMA_SERVER_BIN=/usr/bin/llama-server
16+
Environment=SERVICE_TOKEN_PATH=/run/secure-ai/service-token
1617

1718
# Filesystem isolation
1819
DynamicUser=yes
1920
LogsDirectory=secure-ai
2021
ReadOnlyPaths=/etc/secure-ai
22+
ReadOnlyPaths=/run/secure-ai
2123
ReadWritePaths=/var/lib/secure-ai/quarantine
2224
ReadWritePaths=/var/lib/secure-ai/registry
2325
ReadWritePaths=/var/lib/secure-ai/logs

files/system/usr/lib/systemd/system/secure-ai-registry.service

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ Environment=BIND_ADDR=127.0.0.1:8470
1010
Environment=REGISTRY_DIR=/var/lib/secure-ai/registry
1111
Environment=REGISTRY_LOCK_PATH=/etc/secure-ai/policy/models.lock.yaml
1212
Environment=AUDIT_LOG_PATH=/var/lib/secure-ai/logs/registry-audit.jsonl
13+
Environment=SERVICE_TOKEN_PATH=/run/secure-ai/service-token
1314

1415
# Filesystem isolation
1516
DynamicUser=yes
1617
StateDirectory=secure-ai/registry
1718
LogsDirectory=secure-ai
1819
ReadOnlyPaths=/etc/secure-ai
20+
ReadOnlyPaths=/run/secure-ai
1921
ReadWritePaths=/var/lib/secure-ai/registry
2022
ReadWritePaths=/var/lib/secure-ai/logs
2123
PrivateNetwork=yes

files/system/usr/lib/systemd/system/secure-ai-tool-firewall.service

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ ExecStart=/usr/libexec/secure-ai/tool-firewall
99
Environment=BIND_ADDR=127.0.0.1:8475
1010
Environment=POLICY_PATH=/etc/secure-ai/policy/policy.yaml
1111
Environment=AUDIT_LOG_PATH=/var/lib/secure-ai/logs/tool-firewall-audit.jsonl
12+
Environment=SERVICE_TOKEN_PATH=/run/secure-ai/service-token
1213

1314
# Filesystem isolation
1415
DynamicUser=yes
1516
LogsDirectory=secure-ai
1617
ReadOnlyPaths=/etc/secure-ai
18+
ReadOnlyPaths=/run/secure-ai
1719
ReadOnlyPaths=/var/lib/secure-ai/vault
1820
ReadWritePaths=/var/lib/secure-ai/logs
1921
PrivateNetwork=yes

files/system/usr/lib/systemd/system/secure-ai-ui.service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Environment=QUARANTINE_DIR=/var/lib/secure-ai/quarantine
1818
Environment=SECURE_AI_ROOT=/var/lib/secure-ai
1919
Environment=AUTH_DATA_DIR=/var/lib/secure-ai/auth
2020
Environment=AUDIT_LOG_PATH=/var/lib/secure-ai/logs/ui-audit.jsonl
21+
Environment=SERVICE_TOKEN_PATH=/run/secure-ai/service-token
2122

2223
# Filesystem isolation
2324
DynamicUser=yes

0 commit comments

Comments
 (0)