| 1 | --- |
| 2 | archetype: devops-infra |
| 3 | primary_gate_families: [1, 7, 8, 2] |
| 4 | --- |
| 5 | |
| 6 | # Archetype - Self-Hosted DevOps & Infra Services |
| 7 | |
| 8 | Targets: Jenkins, Grafana, Gitea/Forgejo, Redis, Jupyter, Argo CD/Workflows, MinIO, Metabase, |
| 9 | Prometheus, Vault-adjacent tooling, Consul, Nomad, Harbor, Nexus. The thesis: these run with |
| 10 | **high privilege at the center of an org** (they hold cloud creds, signing keys, DB access, and the |
| 11 | deploy keys to everything), they are *meant* to execute things (pipelines, queries, dashboards), and |
| 12 | self-hosters routinely misconfigure auth. A single unauth RCE here is a foothold into the entire |
| 13 | estate - [[../00-doctrine/SEVERITY-GATE#family-8-exposed-critical-service-trust-infrastructure-compromise|Family 8]] / [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] / Family 7 / Family 2. |
| 14 | |
| 15 | See: [[cicd-supplychain]] (Jenkins/Argo overlap), [[ai-agent-mcp]] (often co-hosted). |
| 16 | |
| 17 | --- |
| 18 | |
| 19 | ## Principals & trust boundaries |
| 20 | |
| 21 | - **Principals:** unauthenticated internet/intranet caller, low-priv authenticated user, the service |
| 22 | process (holding cloud/DB/signing creds), the host & the orchestrator (Docker/K8s) it runs on. |
| 23 | - **Core boundaries:** *unauth → service* (the exposed-service problem), *low-priv user → admin |
| 24 | capability*, *service → host/orchestrator* (a query/pipeline that reaches the OS or the K8s API), |
| 25 | *service → the credentials it holds* (SSRF/template/file-read that dumps the cloud creds). |
| 26 | |
| 27 | **Boundary map template:** |
| 28 | ``` |
| 29 | principals: unauth-caller | low-priv-user | service-process(holds creds) | host/orchestrator |
| 30 | trust inputs: HTTP API params, dashboard/query definitions, pipeline/job configs, datasource |
| 31 | configs, template strings, uploaded plugins, webhook payloads, the auth token itself |
| 32 | enforcement pts: authn/authz on each route, the query/datasource sandbox, template sandbox, |
| 33 | SSRF allowlist, the service-account scope, RBAC |
| 34 | cross-boundary |
| 35 | sinks: groovy/script console (Jenkins), datasource URL fetch (Grafana SSRF), |
| 36 | SQL/native-query (Metabase), MODULE LOAD / CONFIG SET (Redis), |
| 37 | notebook exec (Jupyter), workflow templates (Argo), git hooks (Gitea), |
| 38 | the cloud-cred env/metadata the service can reach |
| 39 | confusion |
| 40 | hypotheses: "an admin route is reachable unauth via path normalization"; |
| 41 | "the SSRF allowlist misses 169.254.169.254 / a redirect / DNS-rebind"; |
| 42 | "a low-priv role can reach a script/query sink"; |
| 43 | "default creds / no creds on the management port" |
| 44 | ``` |
| 45 | |
| 46 | --- |
| 47 | |
| 48 | ## Kill chains (each lands on a Gate family) |
| 49 | |
| 50 | ### KC-1 - Unauth or default-cred RCE on the exposed service (Family 8 → 1) |
| 51 | The bread and butter. The service is reachable without (real) auth and exposes an exec primitive. |
| 52 | - **Per-target sinks:** |
| 53 | - **Jenkins:** the Groovy *script console* (`/script`), the CLI deserialization channel, agent |
| 54 | protocols; pipeline `sh` steps. (Cite the Jenkins CLI/Groovy RCE class.) |
| 55 | - **Redis:** unauth `CONFIG SET dir/dbfilename` → write cron / `authorized_keys` / a webroot file; |
| 56 | `MODULE LOAD` → native RCE; replication-based module load. (Cite the Redis-unauth-RCE class.) |
| 57 | - **Jupyter:** an open notebook server (no token) → `os.system` in a cell. |
| 58 | - **Metabase:** unauth setup-token → DB connection → H2/native-query RCE (cite the Metabase |
| 59 | pre-auth RCE class). |
| 60 | - **Argo CD/Workflows:** an exposed API → run a workflow template with a container we control. |
| 61 | - **Gitea/Forgejo:** git hooks / a malicious migration URL (SSRF) / a webhook→exec path. |
| 62 | - **MinIO/Harbor/Nexus:** default creds, an SSRF in the remote-fetch, an admin API exposed. |
| 63 | - **Oracle:** the unauth/default path lands command output / a callback / a written canary → Family 1. |
| 64 | |
| 65 | ### KC-2 - SSRF → cloud metadata → credential theft → cloud ATO (Family 7 → 3) |
| 66 | These services constantly fetch URLs the user supplies (Grafana datasource proxy, Gitea migrate, |
| 67 | webhook tests, image registries). An SSRF that reaches `169.254.169.254` (AWS), the GCP/Azure metadata |
| 68 | endpoints, or an internal admin API steals the service's IAM credentials. |
| 69 | - **Sinks:** the datasource/proxy fetch, the avatar/webhook/import URL fetch, any "test connection". |
| 70 | - **The gate discipline:** a *bare* SSRF callback is **not** a finding (Family 7 trap). You must |
| 71 | **capture the metadata credential** and minimally prove it works → that's the Family 3 ATO. |
| 72 | - **Oracle:** the IMDS token captured + a benign `GetCallerIdentity` confirming the role → Family 3. |
| 73 | |
| 74 | ### KC-3 - Template / query injection → file read or RCE (Family 1 / 7) |
| 75 | Grafana template variables, Metabase/Superset SQL, Prometheus/Alertmanager templates, Argo workflow |
| 76 | templates - expression/template engines that can reach the FS or OS. |
| 77 | - **Sinks:** the template render, the native-query executor, the alerting template (SSTI). |
| 78 | - **Oracle:** template/query reads our canary secret (Family 7) or executes (Family 1). |
| 79 | |
| 80 | ### KC-4 - Service → orchestrator takeover (Family 2) |
| 81 | The service runs in a container/pod with a mounted Docker socket or a powerful K8s service-account. |
| 82 | RCE in the service (KC-1) then pivots: read the SA token, hit the K8s API, schedule a pod, or use the |
| 83 | Docker socket to escape to the node. |
| 84 | - **Sinks:** `/var/run/docker.sock` mounted in, the SA token at |
| 85 | `/var/run/secrets/kubernetes.io/serviceaccount/token`, an over-scoped RBAC role. |
| 86 | - **Oracle:** an action on the node / cross-namespace, per [[../00-doctrine/SEVERITY-GATE#family-2-container-vm-sandbox-hypervisor-escape-orchestrator-takeover|Family 2]]. |
| 87 | |
| 88 | --- |
| 89 | |
| 90 | ## The dynamic oracle, concretely |
| 91 | |
| 92 | 1. `docker run` the exact version in a disposable env with **planted canary cloud creds** (a fake IMDS |
| 93 | or a real throwaway role) and a canary secret file. Pin version. |
| 94 | 2. `test_unauth_access` / `discover_paths` / `discover_api_endpoints` to map the real auth surface |
| 95 | (path-normalization unauth bypasses are common - try `..;/`, `%2e`, double-slash, case). |
| 96 | 3. For SSRF: stand up a fake metadata endpoint + `start_callback_server`; require the **credential |
| 97 | capture**, not just the callback. |
| 98 | 4. REPLICA_CONFIRMED on the artifact; LIVE_CANARY only within explicit program scope (these are often |
| 99 | internet-exposed third-party installs - **do not** touch non-consenting ones; replica-first). |
| 100 | |
| 101 | --- |
| 102 | |
| 103 | ## BORING traps - do NOT spend time here |
| 104 | |
| 105 | - **"The service is exposed to the internet without auth."** That is a *port-scan observation*, the |
| 106 | start of Family 8, not a finding. The capability must fire (data read / RCE). v1 loved reporting |
| 107 | these as criticals - don't. |
| 108 | - **Bare blind SSRF.** Per the gate and Zion's standing rule, never standalone - chain to metadata |
| 109 | cred or internal-admin or RCE. |
| 110 | - **Default creds with nothing behind them / a read-only dashboard.** Prove the privileged action. |
| 111 | - **Version-banner "vulnerabilities".** "Old Jenkins/Grafana" is not a finding; demonstrate it. |
| 112 | - **Re-finding the public CVE (Metabase/Redis/Jenkins) on an unpatched box.** Sibling-CVE - that's a |
| 113 | patching observation, not novel research. Hunt fresh code/routes or a misconfig that yields *new* |
| 114 | impact. |
| 115 | - **Self-DoS / crashing the service.** Not in scope of the gate families. |