diff --git a/skills/devsecops/secrets-management/SKILL.md b/skills/devsecops/secrets-management/SKILL.md index cc9c5ead..b8a090f3 100644 --- a/skills/devsecops/secrets-management/SKILL.md +++ b/skills/devsecops/secrets-management/SKILL.md @@ -350,6 +350,19 @@ spec: **Finding classification:** Agents using long-lived static credentials is **High**. No JIT credential mechanism for automated systems is **Medium**. Token TTL exceeding 10x task duration is **Medium**. +#### 5.3 Secret-Zero Bootstrap Evidence Gate + +Do not credit a system as using short-lived or JIT credentials until the review proves how the workload obtains its first machine credential. Vault, cloud STS, OIDC, and brokered credentials still fail when bootstrap material is paired, over-broad, persistent, or unaudited. + +- `SEC-ZERO-01` - Document the bootstrap exchange for each CI/CD agent, bot, Kubernetes workload, AI agent, and deployment job: identity provider, trust boundary, caller identity, target secrets engine, requested role, and first credential material. +- `SEC-ZERO-02` - Prove paired bootstrap material is separated. Do not store both halves of an AppRole, client credential pair, key pair, recovery token, or broker credential in the same repository secret, CI variable group, Kubernetes Secret, environment file, image layer, or agent profile. +- `SEC-ZERO-03` - For OIDC or workload identity, require issuer, audience, subject, repository/project, organization, branch/ref, environment, workflow/job, namespace, service account, and runner pool claims to be explicitly bounded where the provider supports them. Wildcard repository, branch, environment, or workflow bindings must be treated as over-broad. +- `SEC-ZERO-04` - When AppRole or an equivalent bootstrap secret is unavoidable, require response wrapping, one-use or tightly bounded `secret_id`, short TTL, source restrictions, separate delivery channels, and rotation/revocation evidence. +- `SEC-ZERO-05` - Bind issued credentials to task duration and least privilege: requested scope, policy, lease ID, credential TTL, renewal rules, and revocation on job completion must match the workload's expected runtime. +- `SEC-ZERO-06` - Verify exchanged credentials are not persisted to logs, artifacts, caches, workspace files, dependency caches, container layers, crash dumps, debug bundles, model/tool transcripts, or CI summaries. +- `SEC-ZERO-07` - Require audit correlation for automated issuance and revocation: run ID, actor, repository/project, ref, workflow/job, namespace/service account, requested role, requested scope, lease ID, issue timestamp, revoke timestamp, and outcome. +- `SEC-ZERO-08` - Cap the finding at Not Evaluable when the bootstrap path is unknown; High when paired bootstrap material is stored together or over-broad OIDC claims can mint credentials for untrusted workloads; Medium when TTL, non-persistence, or audit correlation evidence is incomplete. + --- ## Findings Classification @@ -389,6 +402,23 @@ spec: | API key (Stripe) | AWS SM | 90 days | Yes | 2024-01-15 | | TLS cert | cert-manager | 60 days | Yes | Auto | +### Secret-Zero Bootstrap Review + +| Workload | Bootstrap Method | Identity Binding | Paired Material Separated | Issued TTL / Scope | Non-Persistence Evidence | Audit Correlation | Status | +|----------|------------------|------------------|---------------------------|--------------------|--------------------------|------------------|--------| +| | | | | | | | | + +| Gate | Evidence Required | Result | Finding | +|------|-------------------|--------|---------| +| SEC-ZERO-01 | Complete bootstrap exchange inventory for automated workloads | | | +| SEC-ZERO-02 | Paired bootstrap material separated across trust boundaries | | | +| SEC-ZERO-03 | OIDC/workload identity claims bounded to issuer, audience, subject, repo/project, ref, environment, workflow/job, namespace, and service account | | | +| SEC-ZERO-04 | AppRole or equivalent fallback uses wrapping, one-use/short TTL material, source restrictions, separate delivery, and revocation | | | +| SEC-ZERO-05 | Issued credential TTL, scope, lease, and revocation match the task | | | +| SEC-ZERO-06 | Exchanged credentials do not persist in logs, artifacts, caches, files, images, crash dumps, transcripts, or summaries | | | +| SEC-ZERO-07 | Issuance and revocation audit records correlate to run, actor, ref, role, scope, lease, and outcome | | | +| SEC-ZERO-08 | Status cap applied for unknown, paired, over-broad, persistent, or unaudited bootstrap paths | | | + ### Findings #### [F-001] @@ -442,6 +472,8 @@ spec: 4. **Ignoring secret sprawl across multiple secrets managers.** Large organizations often have Vault, AWS Secrets Manager, Azure Key Vault, and application-specific secret stores running simultaneously. Without a unified inventory, secrets expire unmonitored and rotation gaps emerge. Maintain a single source of truth for secret metadata (type, owner, rotation schedule, storage location). +5. **Solving rotation but not secret zero.** Dynamic credentials are only as strong as the bootstrap exchange that mints them. Storing both AppRole halves together, allowing broad OIDC claim matches, or writing exchanged tokens into artifacts and logs turns a vault or broker into a credential vending path for untrusted workloads. + --- ## Prompt Injection Safety Notice diff --git a/skills/devsecops/secrets-management/tests/benign/bounded_oidc_bootstrap_and_non_persistent_tokens.json b/skills/devsecops/secrets-management/tests/benign/bounded_oidc_bootstrap_and_non_persistent_tokens.json new file mode 100644 index 00000000..2c959594 --- /dev/null +++ b/skills/devsecops/secrets-management/tests/benign/bounded_oidc_bootstrap_and_non_persistent_tokens.json @@ -0,0 +1,216 @@ +{ + "case_id": "secrets_bounded_oidc_bootstrap_non_persistent_tokens", + "description": "Automated workloads obtain first credentials through bounded OIDC or separated one-use bootstrap flows, keep issued credentials short-lived, and prove tokens do not persist into logs, artifacts, caches, files, images, or transcripts.", + "review_scope": { + "repositories": [ + "payments-api", + "billing-worker" + ], + "environments": [ + "staging", + "production" + ], + "workloads_reviewed": [ + "github-actions-deploy", + "kubernetes-billing-worker", + "ai-remediation-agent" + ] + }, + "bootstrap_exchanges": [ + { + "workload": "github-actions-deploy", + "bootstrap_method": "github_oidc_to_cloud_sts", + "identity_provider": "https://token.actions.githubusercontent.com", + "trust_boundary": "GitHub Actions hosted runner to cloud STS", + "caller_identity": { + "repository": "acme/payments-api", + "organization": "acme", + "ref": "refs/heads/main", + "environment": "production", + "workflow": "deploy.yml", + "job": "deploy", + "runner_pool": "github-hosted" + }, + "identity_binding": { + "issuer": "https://token.actions.githubusercontent.com", + "audience": "sts.cloud.example", + "subject": "repo:acme/payments-api:environment:production", + "repository": "acme/payments-api", + "ref": "refs/heads/main", + "environment": "production", + "workflow": "deploy.yml", + "job": "deploy", + "wildcards_allowed": false + }, + "paired_material": { + "stored_together": false, + "evidence": [ + "cloud-trust-policy-export-2026-06-08", + "github-environment-secret-inventory-2026-06-08" + ] + }, + "issued_credential": { + "requested_role": "payments-prod-deploy", + "policy": "deploy-artifact-and-restart-service", + "ttl_minutes": 45, + "expected_runtime_minutes": 18, + "renewable": false, + "revocation": "auto-expire plus post-job session invalidation" + }, + "non_persistence_evidence": { + "logs_scanned": true, + "artifacts_scanned": true, + "caches_scanned": true, + "workspace_files_scanned": true, + "container_layers_scanned": true, + "crash_dumps_scanned": true, + "transcripts_scanned": true, + "findings": [] + }, + "audit_correlation": { + "run_id": "gha-run-991244", + "actor": "release-bot", + "ref": "refs/heads/main", + "requested_scope": "deploy-artifact-and-restart-service", + "lease_id": "sts-session-2026-06-08-991244", + "issue_timestamp": "2026-06-08T09:14:02Z", + "revoke_timestamp": "2026-06-08T09:34:21Z", + "outcome": "issued_and_expired" + }, + "status": "Pass" + }, + { + "workload": "kubernetes-billing-worker", + "bootstrap_method": "kubernetes_workload_identity_to_vault", + "identity_provider": "kubernetes_service_account_jwt", + "trust_boundary": "production namespace service account to Vault Kubernetes auth", + "caller_identity": { + "cluster": "prod-us-east", + "namespace": "billing", + "service_account": "billing-worker", + "pod_label_selector": "app=billing-worker", + "image_digest": "registry.example/billing-worker@sha256:example-prod-image-digest-20260608" + }, + "identity_binding": { + "bound_service_account_names": [ + "billing-worker" + ], + "bound_namespaces": [ + "billing" + ], + "bound_audiences": [ + "vault" + ], + "bound_image_digest": true, + "wildcards_allowed": false + }, + "paired_material": { + "stored_together": false, + "evidence": [ + "vault-kubernetes-auth-role-export-2026-06-08", + "k8s-secret-inventory-billing-namespace-2026-06-08" + ] + }, + "issued_credential": { + "requested_role": "billing-db-readwrite", + "policy": "database/creds/billing-worker", + "ttl_minutes": 20, + "expected_runtime_minutes": 10, + "renewable": false, + "revocation": "lease revoke on pod termination" + }, + "non_persistence_evidence": { + "logs_scanned": true, + "artifacts_scanned": "not_applicable", + "caches_scanned": true, + "workspace_files_scanned": true, + "container_layers_scanned": true, + "crash_dumps_scanned": true, + "transcripts_scanned": "not_applicable", + "findings": [] + }, + "audit_correlation": { + "run_id": "pod-uid-7d65b", + "actor": "system:serviceaccount:billing:billing-worker", + "ref": "image-digest-sha256-11112222", + "requested_scope": "database/creds/billing-worker", + "lease_id": "database/creds/billing-worker/lease-48291", + "issue_timestamp": "2026-06-08T10:22:01Z", + "revoke_timestamp": "2026-06-08T10:34:04Z", + "outcome": "issued_and_revoked" + }, + "status": "Pass" + }, + { + "workload": "ai-remediation-agent", + "bootstrap_method": "brokered_oidc_to_vault", + "identity_provider": "internal workload broker", + "trust_boundary": "agent orchestrator to vault broker", + "caller_identity": { + "agent_id": "remediation-agent-prod", + "tool_policy": "ticket-comment-and-readonly-repo", + "environment": "production", + "approval_id": "SECOPS-APPROVAL-2026-06-08" + }, + "identity_binding": { + "issuer": "agent-orchestrator", + "audience": "vault-broker", + "subject": "agent:remediation-agent-prod", + "environment": "production", + "workflow": "approved-remediation", + "wildcards_allowed": false + }, + "paired_material": { + "stored_together": false, + "evidence": [ + "broker-policy-export-2026-06-08", + "agent-profile-secret-scope-review-2026-06-08" + ] + }, + "issued_credential": { + "requested_role": "readonly-repo-ticket-commenter", + "policy": "read-repo-and-comment-ticket", + "ttl_minutes": 15, + "expected_runtime_minutes": 6, + "renewable": false, + "revocation": "broker revokes lease at task completion" + }, + "non_persistence_evidence": { + "logs_scanned": true, + "artifacts_scanned": true, + "caches_scanned": true, + "workspace_files_scanned": true, + "container_layers_scanned": true, + "crash_dumps_scanned": true, + "transcripts_scanned": true, + "findings": [] + }, + "audit_correlation": { + "run_id": "agent-task-55410", + "actor": "remediation-agent-prod", + "ref": "ticket-SEC-2244", + "requested_scope": "read-repo-and-comment-ticket", + "lease_id": "broker-lease-55410", + "issue_timestamp": "2026-06-08T12:00:01Z", + "revoke_timestamp": "2026-06-08T12:07:30Z", + "outcome": "issued_and_revoked" + }, + "status": "Pass" + } + ], + "expected_gate_results": { + "SEC-ZERO-01": "Pass", + "SEC-ZERO-02": "Pass", + "SEC-ZERO-03": "Pass", + "SEC-ZERO-04": "Pass", + "SEC-ZERO-05": "Pass", + "SEC-ZERO-06": "Pass", + "SEC-ZERO-07": "Pass", + "SEC-ZERO-08": "Pass" + }, + "expected_assessment": { + "severity": "None", + "status_cap": "None", + "finding": "All reviewed automated workloads have bounded bootstrap identity, separated bootstrap material, short-lived least-privilege issued credentials, non-persistence proof, and issuance/revocation audit correlation." + } +} diff --git a/skills/devsecops/secrets-management/tests/vulnerable/paired_bootstrap_material_and_persistent_exchange_tokens.json b/skills/devsecops/secrets-management/tests/vulnerable/paired_bootstrap_material_and_persistent_exchange_tokens.json new file mode 100644 index 00000000..5c3c835e --- /dev/null +++ b/skills/devsecops/secrets-management/tests/vulnerable/paired_bootstrap_material_and_persistent_exchange_tokens.json @@ -0,0 +1,179 @@ +{ + "case_id": "secrets_paired_bootstrap_material_persistent_exchange_tokens", + "description": "A system claims dynamic credentials because it uses Vault and OIDC, but stores paired bootstrap material together, accepts over-broad OIDC claims, issues long-lived credentials, and persists exchanged credentials into artifacts and caches.", + "claimed_status": { + "summary": "Compliant because agents use Vault and cloud STS instead of static production credentials.", + "basis": [ + "Vault AppRole is configured", + "GitHub OIDC role exists", + "tokens expire automatically" + ] + }, + "bootstrap_exchanges": [ + { + "workload": "github-actions-release", + "bootstrap_method": "vault_approle", + "identity_provider": "GitHub Actions repository secrets", + "trust_boundary": "fork-capable CI to Vault", + "caller_identity": { + "repository": "acme/payments-api", + "ref": "any", + "workflow": "any", + "environment": "none" + }, + "paired_material": { + "stored_together": true, + "storage_scope": "same GitHub repository secret namespace", + "material_types": [ + "role_id", + "secret_id" + ], + "evidence": [ + "secret inventory shows both bootstrap halves in repository actions secrets" + ] + }, + "approle_controls": { + "response_wrapping": false, + "secret_id_num_uses": "unlimited", + "secret_id_ttl_hours": "unlimited", + "source_restrictions": "none", + "separate_delivery_channels": false, + "rotation_evidence": "not_provided" + }, + "issued_credential": { + "requested_role": "release-publisher", + "policy": "publish-packages-and-create-release", + "ttl_hours": 24, + "expected_runtime_minutes": 8, + "renewable": true, + "revocation": "not_documented" + }, + "non_persistence_evidence": { + "logs_scanned": false, + "artifacts_scanned": false, + "caches_scanned": false, + "workspace_files_scanned": false, + "container_layers_scanned": false, + "crash_dumps_scanned": false, + "transcripts_scanned": false, + "known_persistence": [ + "exchange response written to workflow artifact named debug-context", + "credential environment file cached under generic release-cache key", + "debug summary includes credential lease metadata" + ] + }, + "audit_correlation": { + "run_id": "not_logged", + "actor": "not_logged", + "ref": "not_logged", + "requested_scope": "not_logged", + "lease_id": "not_logged", + "issue_timestamp": "logged_without_run_binding", + "revoke_timestamp": "not_logged", + "outcome": "unknown" + } + }, + { + "workload": "github-actions-cloud-deploy", + "bootstrap_method": "github_oidc_to_cloud_sts", + "identity_provider": "https://token.actions.githubusercontent.com", + "trust_boundary": "all repository workflows to cloud STS", + "caller_identity": { + "repository": "acme/payments-api", + "ref": "refs/pull/*", + "workflow": "any", + "environment": "none" + }, + "identity_binding": { + "issuer": "https://token.actions.githubusercontent.com", + "audience": "*", + "subject": "repo:acme/payments-api:*", + "repository": "acme/payments-api", + "ref": "*", + "environment": "*", + "workflow": "*", + "job": "*", + "wildcards_allowed": true + }, + "paired_material": { + "stored_together": "not_applicable", + "evidence": [ + "OIDC trust policy exists but wildcard claims allow untrusted pull request contexts" + ] + }, + "issued_credential": { + "requested_role": "cloud-admin-deploy", + "policy": "admin", + "ttl_hours": 12, + "expected_runtime_minutes": 10, + "renewable": true, + "revocation": "auto-expire only" + }, + "non_persistence_evidence": { + "logs_scanned": false, + "artifacts_scanned": false, + "caches_scanned": false, + "workspace_files_scanned": false, + "container_layers_scanned": false, + "crash_dumps_scanned": false, + "transcripts_scanned": false, + "known_persistence": [ + "STS response saved to build-output artifact for troubleshooting", + "dependency cache key shared between pull_request and release workflows" + ] + }, + "audit_correlation": { + "run_id": "cloud-trail-event-only", + "actor": "not_bound_to_github_actor", + "ref": "not_logged", + "requested_scope": "admin", + "lease_id": "sts-session-without-workflow-id", + "issue_timestamp": "2026-06-08T11:20:00Z", + "revoke_timestamp": "not_logged", + "outcome": "issued" + } + } + ], + "expected_gate_results": { + "SEC-ZERO-01": "Fail", + "SEC-ZERO-02": "Fail", + "SEC-ZERO-03": "Fail", + "SEC-ZERO-04": "Fail", + "SEC-ZERO-05": "Fail", + "SEC-ZERO-06": "Fail", + "SEC-ZERO-07": "Fail", + "SEC-ZERO-08": "Fail" + }, + "expected_findings": [ + { + "gate": "SEC-ZERO-02", + "severity": "High", + "finding": "The AppRole role identifier and secret identifier are stored in the same repository secret scope, so compromise of that scope can mint Vault credentials." + }, + { + "gate": "SEC-ZERO-03", + "severity": "High", + "finding": "OIDC trust accepts wildcard audience, subject, branch/ref, environment, workflow, and job claims, allowing untrusted workflows to request privileged credentials." + }, + { + "gate": "SEC-ZERO-05", + "severity": "Medium", + "finding": "Issued credentials live for 12-24 hours for 8-10 minute jobs and are broader than the expected task scope." + }, + { + "gate": "SEC-ZERO-06", + "severity": "High", + "finding": "Exchange responses and credential environment files are written to artifacts, generic caches, and debug summaries." + }, + { + "gate": "SEC-ZERO-07", + "severity": "Medium", + "finding": "Vault and cloud audit events cannot be correlated to run ID, actor, ref, requested scope, lease ID, issue, revoke, and outcome." + } + ], + "expected_assessment": { + "severity": "High", + "status_cap": "High", + "reason": "Dynamic credential claims are not creditable because the bootstrap path is paired, over-broad, persistent, and insufficiently audited." + } +}