Skip to content

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.example for the observability service
  • install_hooks.sh becomes 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_PASSWORD to env var in docker-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() in check_adr_gate.py
  • [x] F6: Update .env.example placeholder to use https://
  • [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.log on 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