ADR-0007: OWASP CI/CD Security Posture for Cornerstone
Status: accepted Deciders: DeAcero Platform Team, AI Agent Guild Date: 2026-03-19
Context and Problem Statement
A full security review of the Cornerstone template was conducted against the OWASP Top 10 CI/CD Security Risks (2022) and applicable items from the OWASP Top 10 Web Application Security Risks (2021).
16 findings were identified across severity tiers. This ADR documents the architectural decisions about which risks to remediate immediately, which to accept with mitigations, and which to defer to a later phase. It serves as the authority for all security-motivated changes to the template.
Decision Drivers
- Security must not block the primary workflow (SQL Server legacy modernization)
- Generated projects must still work without a running observability server
- Changes to the template must be backward-compatible with existing generated projects
- Secrets must never appear in committed files, logs, or agent context windows
- The skill-sync mechanism must not become a supply-chain attack vector
OWASP CI/CD Top 10 Mapping
| OWASP ID | Name | Assessment |
|---|---|---|
| CICD-SEC-1 | Insufficient Flow Control Mechanisms | ✅ Mitigated — ADR gate at pre-commit, Claude hook, and CI |
| CICD-SEC-2 | Inadequate Identity and Access Management | ✅ Mitigated (POC) — HTTP Basic Auth on dashboard/query; ingest open by design |
| CICD-SEC-3 | Dependency Chain Abuse | ⚠️ Partial — sync_agents.sh clones without signature verification |
| CICD-SEC-4 | Poisoned Pipeline Execution (PPE) | ⚠️ Partial — agent hook prompt uses unsanitized $ARGUMENTS |
| CICD-SEC-5 | Insufficient PBAC | ✅ Mitigated — GitHub Actions jobs scoped to repository |
| CICD-SEC-6 | Insufficient Credential Hygiene | ⚠️ Partial — DB password hardcoded in docker-compose.yml; .env.example leaks internal IP |
| CICD-SEC-7 | Insecure System Configuration | ⚠️ Partial — observability dashboard unauthenticated; HTTP not HTTPS on telemetry |
| CICD-SEC-8 | Ungoverned 3rd Party Services | ⚠️ Partial — SQUIT response content not validated |
| CICD-SEC-9 | Improper Artifact Integrity Validation | ❌ Not addressed — downloaded skills have no checksum or signature |
| CICD-SEC-10 | Insufficient Logging and Visibility | ✅ Mitigated — ADR gate bypasses appended to .adr-gate-bypasses.log |
Findings, Decisions, and Dispositions
Tier 1 — Remediate Immediately (this ADR)
F1: Hardcoded database password in docker-compose.yml
OWASP: CICD-SEC-6, A02:2021 (Cryptographic Failures)
Evidence: POSTGRES_PASSWORD: telemetry_secret hardcoded in plaintext.
Decision: Replace with ${POSTGRES_PASSWORD} environment variable substitution.
Add .env at services/observability/ with a placeholder value and add it to
.gitignore. Document required value in services/observability/README.md.
Implementation: docker-compose.yml → ${POSTGRES_PASSWORD} substitution.
New services/observability/.env.example with POSTGRES_PASSWORD=change_me_in_production.
F2: Internal IP address committed in .env.example
OWASP: CICD-SEC-6 (leaks network topology)
Evidence: AGENTIC_TELEMETRY_URL=http://192.99.38.188:8000 in tracked .env.example.
Decision: Replace with a placeholder. Internal IPs must not appear in any committed file.
Implementation: Replace with AGENTIC_TELEMETRY_URL=http://your-observability-host:8000.
F3: install_hooks.sh silently overwrites existing pre-commit hooks
OWASP: CICD-SEC-7 (insecure system configuration — disabling other security controls)
Evidence: cat > "$PRE_COMMIT" overwrites without checking existing content.
Decision: Check for an existing hook before writing. If one exists, append
Cornerstone's gate as a called script rather than replacing the entire hook file.
Accept overwrite only if the existing hook was written by Cornerstone (detectable by
a # cornerstone banner comment).
Implementation: Update install_hooks.sh with existence check and merge logic.
F4: Path traversal not rejected in check_adr_gate.py
OWASP: A01:2021 (Broken Access Control — path traversal)
Evidence: _normalise() strips ./ but does not reject .. segments.
A staged file named ../../sensitive.py could bypass guarded-pattern matching.
Decision: Add explicit rejection of paths containing .. segments or starting
with / in _normalise().
Implementation: One-line guard in _normalise().
Tier 2 — Mitigate with Compensating Control (this ADR, deferred implementation)
F5: sync_agents.sh has no artifact integrity verification
OWASP: CICD-SEC-3 (Dependency Chain Abuse), CICD-SEC-9 (Improper Artifact Integrity Validation)
Evidence: Skills are downloaded and installed via rsync with no checksum or
GPG verification. A compromised deagentic/cornerstone repository would silently
distribute malicious skill instructions to all generated projects.
Decision: Accept current risk for Phase F1 (MVP) with the following compensating
controls:
1. The 24-hour throttle limits blast radius to one sync per day per project.
2. gh repo clone uses authenticated HTTPS, which reduces DNS spoofing risk.
3. Version-change warnings (added in Fix 5) surface unexpected skill updates.
Deferred to Phase F2: Implement SHA-256 checksum verification of the skills
directory against a signed manifest file published in deagentic/cornerstone. Add
--verify flag to sync_agents.sh.
Accepted risk rationale: The upstream repository is owned by DeAcero. The
primary threat vector (external attacker pushing to deagentic/cornerstone) is
mitigated by GitHub repository access controls, not by client-side verification.
Client-side verification is defense-in-depth and is planned but not blocking.
F6: Telemetry endpoint uses HTTP, not HTTPS
OWASP: CICD-SEC-7, A02:2021 (Cryptographic Failures)
Evidence: AGENTIC_TELEMETRY_URL=http://... in .env.example.
CI metadata (commit SHA, run ID, branch ref) is transmitted in plaintext.
Decision: Mandate HTTPS for all production deployments. Update .env.example
to use https:// in the placeholder. The observability service (services/observability/)
must be deployed behind a TLS-terminating reverse proxy (nginx, Caddy, or GCP Load Balancer).
The template itself cannot enforce this at the SDK level without breaking HTTP-only
local development setups.
Compensating control: emit_ci_event.py uses urllib.request which inherits
the system CA bundle — certificate validation is on by default for HTTPS URLs.
Implementation: Update .env.example placeholder to https://. Add a note
to services/observability/README.md mandating TLS in production.
F7: Observability dashboard has no authentication
OWASP: CICD-SEC-2 (Inadequate IAM), A07:2021 (Identification and Authentication Failures)
Evidence: app.mount("/", StaticFiles(...)) has no auth middleware.
All telemetry events (CI runs, commit SHAs, project metadata) are readable by
anyone with network access to port 8000.
Decision: Implement HTTP Basic Auth at the application layer for the POC phase.
Deployment context is VPS/containers — GCP IAP is not available.
Credentials are read from DASHBOARD_USERNAME / DASHBOARD_PASSWORD environment
variables. When both are empty, auth is silently skipped (local dev convenience).
The ingest endpoint (POST /v1/events) intentionally requires no auth so that CI
agents can push telemetry without credential distribution.
Implementation: secrets.compare_digest in app/main.py middleware + Depends() on
query router. Credentials in .env (never committed). Documented in
services/observability/README.md.
Compensating controls for POC:
- Service is not directly internet-exposed; nginx/Caddy terminates TLS (see F6)
- Docker Compose binds to 127.0.0.1:8000 by default — no direct external access
Production upgrade path (Phase F2): Replace application-level Basic Auth with GCP Identity-Aware Proxy (or equivalent IdP) when the stack migrates off VPS.
F8: ADR gate bypass leaves no auditable trail
OWASP: CICD-SEC-10 (Insufficient Logging and Visibility)
Evidence: [skip-adr] in a commit message allows bypassing the gate.
The bypass is logged only to stdout during the pre-commit run, not persisted.
Decision: Implement structured bypass audit log in check_adr_gate.py.
On every bypass (either --skip-adr flag or [skip-adr] commit token),
append a record to .adr-gate-bypasses.log containing: UTC timestamp, bypass
reason, list of bypassed guarded files, and commit message snippet (truncated
at 120 chars). Log writes are non-fatal — an OSError is silently swallowed
to ensure the log never blocks a commit.
Implementation: _record_bypass() helper in check_adr_gate.py, called in
both bypass branches. .adr-gate-bypasses.log is committed to the project
repository (not gitignored) and is therefore auditable via git log.
Note: The commit message [skip-adr] token remains a valid audit trail on
its own; the structured log adds machine-readable traceability.
Tier 3 — Accept Risk (with documented rationale)
F9: Agent hook prompt uses unsanitized $ARGUMENTS (prompt injection risk)
OWASP: CICD-SEC-4 (Poisoned Pipeline Execution — AI variant)
Evidence: The PreToolUse hook injects $ARGUMENTS directly into the ADR Gate
agent prompt, which could be poisoned by a malicious file path or tool input.
Accepted risk rationale: The ADR Gate agent reads $ARGUMENTS to extract a
file path and make a binary allow/deny decision. The agent's output is constrained
to a JSON structure (permissionDecision: allow/deny). Even if the input is
adversarial, the worst outcome is an incorrect allow or deny decision — not code
execution. The system is Claude Code running locally; the attacker would already
need to control the developer's working directory.
Compensating control: The hook prompt uses a step-by-step decision algorithm that explicitly names the expected fields, reducing susceptibility to instruction injection.
F10: SQUIT response content not schema-validated
OWASP: CICD-SEC-8 (Ungoverned 3rd Party Services)
Evidence: squit_client.py returns response.json() without validating structure.
Accepted risk rationale: SQUIT is an internal DeAcero service. A compromised
SQUIT response would affect discovery tool output quality, not pipeline security.
The client does validate HTTP status via raise_for_status().
Deferred improvement: Add Pydantic schema validation to squit_client.py in
a future tool-writer sprint. This is a reliability improvement, not a security gate.
Consequences
Positive
- All Tier 1 remediations eliminate known OWASP findings from committed code
- Accepted risks are now documented with explicit rationale, preventing surprise discoveries
- The deferred F2 items are formally tracked and cannot be forgotten
Negative
- The template must ship a pre-built
.env.examplefor the observability service install_hooks.shbecomes slightly more complex to handle pre-existing hooks- Teams deploying the observability service must configure TLS themselves
Neutral
- CICD-SEC-9 (artifact integrity) remains partially unaddressed until Phase F2; the 24h throttle and version-change warnings are the primary defenses
Implementation Checklist
- [x] F1: Move
POSTGRES_PASSWORDto env var indocker-compose.yml - [x] F2: Replace internal IP in
{{cookiecutter.project_slug}}/.env.example - [x] F3: Add existence check + merge logic to
install_hooks.sh - [x] F4: Add
..rejection to_normalise()incheck_adr_gate.py - [x] F6: Update
.env.exampleplaceholder to usehttps:// - [x] F6: Add TLS mandate note to
services/observability/README.md - [x] F7: HTTP Basic Auth in
app/main.py+ credentials via env vars - [x] F8:
_record_bypass()appends to.adr-gate-bypasses.logon each bypass - [ ] F5: Skill sync artifact integrity (SHA-256 + signed manifest) — Phase F2
- [ ] F7 (prod): Replace Basic Auth with GCP IAP on production migration — Phase F2
Links
- OWASP Top 10 CI/CD Security Risks
- OWASP Top 10 2021
- ADR-0001 — Observability System
- ADR-0003 — Opt-In Telemetry
- ADR-0006 — Env Scaffold and Secrets at Generation Time