Threat Model: Otterdog (CLI + WebApp)¶
1. Purpose¶
This document models threats for operating GitHub organizations using otterdog in two modes:
- otterdog CLI used from admin laptops (
plan/apply) - Otterdog WebApp deployed in Kubernetes (webhooks + sync/validation + persistence)
Goal: protect GitHub organization integrity, minimize credential blast radius, ensure strong governance over configuration changes, and provide adequate auditability for all applies.
2. System overview¶
Otterdog is a stateless GitHub infrastructure-as-code tool. It reads org-level configuration (JSON/Jsonnet), builds an in-memory desired model, fetches live state from GitHub via providers, computes a diff as “LivePatch” operations, then prints (plan) or executes (apply) changes. (otterdog.readthedocs.io)
Important behavior: apply does not consume a previously generated plan; it recomputes the diff at runtime and does not keep a persistent state/plan file. (otterdog.readthedocs.io)
Otterdog fetches data via multiple provider paths, including GitHub REST API, GitHub GraphQL API, and GitHub Web UI (for settings not accessible via APIs). (otterdog.readthedocs.io)
Operational model (typical): configuration is hosted in a repository; contributors propose changes via pull requests; changes are approved and then applied manually using the CLI. (otterdog.readthedocs.io)
3. Scope¶
In scope¶
- Integrity of GitHub org settings managed by otterdog configuration
- Confidentiality and integrity of credentials used by CLI and WebApp
- Configuration repository governance and CI validation/policy checks
- WebApp deployment hardening (ingress, internal endpoints, persistence dependencies)
- Auditability of “who approved what” and “who applied what”
- Availability risks that prevent managing orgs or cause disruption
Out of scope (explicitly)¶
- Exploits in GitHub itself (treated as external dependency)
- Physical threats beyond standard enterprise endpoint controls
- Unrelated SDLC risks not interacting with otterdog (unless you extend this model)
4. Assumptions and constraints¶
A1. Applies run from admin laptops (not a controlled CI runner). This makes endpoint compromise a primary risk driver.
A2. Some settings require GitHub Web UI access; otterdog “Quick Setup” explicitly calls for username/password and a 2FA TOTP seed to access settings not available via REST/GraphQL. (otterdog.readthedocs.io)
A3. The documented GitHub PAT scopes include delete_repo (high blast radius). (otterdog.readthedocs.io)
A4. WebApp dependencies include MongoDB and Redis/Valkey; the install guide states Redis/Valkey “must be configured without authentication” (GHProxy limitation). (otterdog.readthedocs.io)
A5. WebApp exposes internal management endpoints under /internal/, including an initialization endpoint /internal/init, and the install guide recommends restricting internal endpoints via ingress rules. (otterdog.readthedocs.io)
A6. Otterdog runs with the credentials of an organization owner (CLI) or of the GitHub App (WebApp). Whoever controls a trusted input below controls what otterdog does with these credentials.
Trusted and untrusted inputs¶
Trusted inputs are provided by the operators of otterdog and are equivalent to code:
| Input | Why it is trusted |
|---|---|
otterdog.json |
Written by the operator. It selects the organizations, the credential providers and their parameters, and the default configuration (base_template). |
Default configuration: the repository and ref referenced by base_template |
Provides the inherited value of every setting of every organization that uses it. Its Python scripts (validate-org-settings.py, validate-team.py, pre-add-object-hook.py, post-add-objects-hook.py) are executed by otterdog during validate, plan, apply and the validation of pull requests, with the permissions and credentials of the process, see Organization-wide policies. |
| Local working directory | The configurations of the organizations and their vendor/ directory. With --local, the default configuration in vendor/ is used as is, without fetching it. |
| Installation | The otterdog package and its dependencies, the credential tools it calls (pass, bw, Vault). |
| WebApp deployment | Helm values and environment variables, the GitHub App private key, the token of the configuration repository. |
Untrusted inputs must not lead to code execution, to file access outside the directory of the organization, or to the disclosure of credentials:
| Input | Handling |
|---|---|
| Pull requests to the configuration repository of an organization (WebApp) | Only otterdog/<github-id>.jsonnet is fetched from the pull request and written to a fixed path. It is evaluated with imports restricted to the directory of the organization. The default configuration comes from otterdog.json, never from the pull request. |
| Webhook deliveries | Rejected unless their signature matches the webhook secret. |
| Data returned by GitHub (APIs and Web UI) | Processed as data. |
Not considered vulnerabilities¶
Issues that require control of a trusted input, e.g. a malicious otterdog.json, a compromised default configuration repository, or files placed in the local working directory, are not vulnerabilities of otterdog: controlling these inputs already gives control over the managed organizations. Their protection is part of operating otterdog, see T5 below. Report an issue anyway if an untrusted party can reach a trusted input in a documented setup. Hardening suggestions are welcome as regular issues.
5. Assets (what must be protected)¶
A. GitHub organization integrity (highest priority)¶
- Org owners/admins, custom roles, and membership
- Teams and team permissions
- Repository settings, branch protections/rulesets, environments
- Organization and repository webhooks
- Organization/repository secrets and variables (note: secret values are not retrievable from GitHub after creation; configuration must reference real values via a credential provider). (otterdog.readthedocs.io)
B. Credential material (“tier-0 secrets”)¶
CLI side:
- GitHub username/password and 2FA TOTP seed used for Web UI scraping (otterdog.readthedocs.io)
- GitHub PAT and its scopes (notably
delete_repo) (otterdog.readthedocs.io) - Credential provider session tokens (Bitwarden/pass)
WebApp side:
- GitHub App private key, webhook secret, config token (base64 in example
values.yaml) (otterdog.readthedocs.io) - DB/cache connectivity secrets (MongoDB URI, Redis URI)
C. Configuration repository integrity¶
- Jsonnet/JSON files defining desired state
- Default configuration referenced by
base_template: its Jsonnet defaults, and the Python scripts that otterdog executes - CI policy/validation logic
D. Audit evidence¶
- Records linking: config commit → plan/apply output → operator identity → resulting org changes
E. Availability¶
- Ability to run plan/apply when needed
- WebApp availability (webhook handling, sync, persistence)
6. Trust boundaries and data flows¶
Key trust boundaries¶
TB1. Admin laptop boundary (endpoint integrity, local filesystem, credential tooling)
TB2. Config repository boundary (PRs, reviews, CI checks, merge protection)
TB3. Network boundary to GitHub (API + Web UI automation)
TB4. Kubernetes cluster boundary (WebApp ingress, internal endpoints, service-to-service traffic)
TB5. Persistence boundary (MongoDB, Redis/Valkey without auth) (otterdog.readthedocs.io)
TB6. Default configuration boundary (repository referenced by base_template, its ref, who can push to it)
Data flow diagram (DFD)¶
flowchart LR
subgraph Dev["Config Repo / PR Workflow"]
PR[Pull Request]
MAIN[Main branch config]
end
subgraph Admin["Admin Laptop (Apply Control Plane)"]
WD["Working directory / checkout"]
CLI["otterdog CLI plan/apply"]
VAULT["Credential provider (Bitwarden/pass)"]
end
subgraph GH["GitHub (External)"]
API["REST/GraphQL APIs"]
WEB[Web UI]
ORG[Org state]
DEFAULTS["Default configuration repo (base_template)"]
end
subgraph K8S["Kubernetes (Otterdog WebApp)"]
INGRESS[Ingress]
APP[WebApp]
INT["/internal/* endpoints"]
MONGO[(MongoDB)]
REDIS["Redis/Valkey - no auth"]
GHP[GHProxy]
end
PR --> MAIN
MAIN --> WD
VAULT --> CLI
WD --> CLI
CLI --> API
CLI --> WEB
API --> ORG
WEB --> ORG
DEFAULTS -->|"clone of the ref: Jsonnet + Python scripts"| CLI
DEFAULTS -->|"clone of the ref: Jsonnet + Python scripts"| APP
GH -->|webhooks| INGRESS --> APP
APP --> MONGO
APP --> REDIS
APP --> GHP
Admin -->|mgmt access| INT
Implementation notes:
- CLI fetches state using REST/GraphQL/Web UI providers. (otterdog.readthedocs.io)
- WebApp requires
/internal/initafter deployment and on some updates. (otterdog.readthedocs.io)
7. Entry points and attack surfaces¶
Admin laptop / CLI:
otterdogexecution environment and local repo checkout- Local secrets handling and credential provider integrations
- Any storage of credentials in
otterdog.json(risk: “plain” provider) - Network access from laptop to GitHub, and potentially to WebApp internal endpoints
Config repo:
- Pull requests and merges (governance and CI enforcement)
- Jsonnet evaluation and imports (in the WebApp, imports of a pull request are restricted to the directory of the organization)
Default configuration repository:
- Pushes to the ref referenced by
base_template; a branch is pulled on every run - Python scripts executed by otterdog with its credentials
WebApp:
- Public ingress endpoints (webhooks, UI)
/internal/*endpoints (must be restricted) (otterdog.readthedocs.io)- Redis/Valkey unauthenticated access (compensate with network isolation) (otterdog.readthedocs.io)
- MongoDB credentials and exposure
8. Threat analysis (STRIDE)¶
This section lists realistic threats for your setup and recommended controls. Severity assumes admin laptops are used for apply.
8.1 Spoofing (impersonation)¶
Threat S1: Theft of CLI credentials (PAT, password, TOTP seed) enables attacker to impersonate an org admin and apply arbitrary changes. Otterdog explicitly documents username/password/2FA seed usage for Web UI settings. (otterdog.readthedocs.io) Controls:
- Prohibit storing credentials via
provider: "plain"beyond experimentation; docs warn not to use it broadly due to leakage risk. (otterdog.readthedocs.io) - Store credentials only in vault-backed providers (Bitwarden/pass) and enforce vault MFA and device posture checks.
- Treat TOTP seed as tier-0: minimize who can retrieve it, do not export to disk, rotate on any suspicion of endpoint compromise.
- Use a dedicated service account for otterdog operations; do not reuse personal admin accounts for apply.
Threat S2: WebApp GitHub App private key compromise enables attacker to mint installation tokens and operate with app permissions. WebApp install guide instructs storing GitHub App private key in values.yaml (base64). (otterdog.readthedocs.io) Controls:
- Store app private key in a proper secret manager / sealed secrets; never commit values containing secrets.
- Rotate webhook secret and app private key after incidents; restrict access to the secret material to cluster operators only.
Threat S3: Forged/replayed webhooks to WebApp Controls:
- Ensure webhook signature verification is enabled and secrets are rotated.
- Add replay protections if you have an edge proxy (timestamp tolerance, rate limits).
8.2 Tampering (unauthorized modification)¶
Threat T1: Malicious config PR weakens org security (e.g., new webhook, reduced ruleset/branch requirements, elevated bypass actors) Controls:
-
Treat config merges as privileged production changes:
-
CODEOWNERS requiring security/admin reviews
- required CI checks + branch protection
- no self-approval on high-impact paths
-
Add CI “policy checks” that fail PRs which:
-
add org/repo webhooks without explicit allowlist approval
- reduce branch protection/ruleset requirements
- broaden bypass actor lists
- Require “apply operator” separation for sensitive changes (approver ≠ applier).
Threat T2: Local tampering on admin laptop (malware modifies config checkout or otterdog binary) Controls:
- Use a dedicated hardened apply environment (separate OS user / VM / laptop) with minimal software.
- Prefer pinned versions / checksums for otterdog installation (package locking and signed releases where available).
- Require
applyto reference an explicit config commit SHA (not “whatever is in the working tree”).
Threat T3: Redis/Valkey manipulation in-cluster (queue/cache poisoning) The install guide states Redis/Valkey must be deployed without authentication. (otterdog.readthedocs.io) Controls (compensating):
- Strict NetworkPolicies: only WebApp/GHProxy pods can reach Redis/Valkey.
- Namespace isolation and RBAC: prevent arbitrary pod creation in the same namespace.
- Monitor and alert on unusual Redis command patterns / spikes.
Threat T4: Abuse of /internal/* management endpoints (init/maintenance actions)
Docs instruct restricting /internal/ endpoints and show ingress allowlist examples; init uses /internal/init. (otterdog.readthedocs.io)
Controls:
- Restrict
/internal/*at ingress to a management CIDR/VPN only. - Add another auth layer (mTLS or an auth proxy) if feasible.
- Alert on any access to
/internal/*.
Threat T5: Compromised or mutable default configuration
The default configuration referenced by base_template defines the inherited values of every organization using it, and otterdog executes its Python scripts with its credentials: organization owner credentials on admin laptops, the GitHub App private key in the WebApp. Whoever can push to the referenced ref controls all these organizations, and can run code at the next validate, plan or pull request validation, without any change to the configuration of an organization. A branch, e.g. @main, is pulled on every run, so every push to it takes effect immediately.
Controls:
- Reference a tag or a commit SHA in
base_template, not a branch. A tag or a commit is checked out once and not updated afterwards; otterdog logs a warning whenbase_templatereferences a branch. - Prefer a commit SHA when the repository of the default configuration is not under your control; otherwise protect its tags with a ruleset that prevents their update and deletion.
- Protect the repository of the default configuration like code: restricted push access, required reviews, no force pushes.
- Review the Python scripts with every new version of the default configuration, and run
planfor the affected organizations before switching to it.
8.3 Repudiation (denying actions)¶
Threat R1: No reliable evidence of who ran apply from an admin laptop
Controls:
-
Standardize a wrapper script for applies that logs:
-
operator identity
- config repo and commit SHA
- timestamp
- command-line args
- plan/apply output (or hash + storage location)
- Send logs to a central, immutable store (SIEM).
- Correlate with GitHub audit log events for key changes (admins, webhooks, rulesets).
8.4 Information disclosure¶
Threat I1: Credential leakage via plain provider in otterdog.json
Docs explicitly warn: do not use plain beyond quick setup to avoid leakage if the file is shared. (otterdog.readthedocs.io)
Controls:
- Ban
provider: plainin repo policy checks; enforce via CI. - Require vault-backed providers.
Threat I2: Leakage of TOTP seed (MFA bypass)
Otterdog documents storing twofa_seed / totp for GitHub Web UI access. (otterdog.readthedocs.io)
Controls:
- Tier-0 handling: least access, audit retrieval, rotate on compromise.
- Avoid printing or logging secrets; ensure wrapper scripts redact.
Threat I3: Secrets embedded in WebApp Helm values (configToken/webhookSecret/appPrivateKey shown in example) (otterdog.readthedocs.io) Controls:
- Externalize secrets: do not commit them; use Kubernetes external secrets or sealed secrets.
- Scan repos for accidental secret commits (secret scanning + pre-commit hooks).
Threat I4: Disclosure of the secrets of another organization
The configuration of an organization, changed by pull requests, references secrets in the credential providers of the operator, who usually has access to the secrets of all organizations, e.g. pass:bots/<other organization>/token. Applying it copies the secret of another organization into a secret of this organization, readable by its workflows.
Controls:
- Restrict the references of each organization with a secret access policy, checked when validating pull requests and before resolving secrets during
apply. - Review the references to secrets in configuration pull requests.
8.5 Denial of service¶
Threat D1: Broad PAT scopes allow destructive operations (delete_repo) causing org-wide disruption (otterdog.readthedocs.io)
Controls:
- Remove
delete_reposcope unless you explicitly need repo deletions. -
Split credentials:
-
“routine” token: minimal scopes needed for normal operation
- “break-glass” token: destructive scopes; time-limited access and extra approvals
Threat D2: Webhook flooding / GitHub rate limiting affects WebApp and/or admin workflows Controls:
- Rate-limit ingress, validate signatures early.
- Monitor GitHub API rate usage and backoff handling.
8.6 Elevation of privilege¶
Threat E1: Anyone who can merge config effectively controls org state (because apply enforces desired state) Controls:
- Tighten merge permissions and enforce least privilege.
- Require security review for changes to roles/admins, webhooks, bypass settings, and rulesets.
Threat E2: Admin laptop compromise becomes org-admin compromise (because apply runs there) Controls:
- Harden apply endpoints (dedicated system, EDR, patching, minimal apps).
- Do not use apply endpoint for daily browsing/email.
- Device posture requirements for vault access.
9. Risk register (starter)¶
| ID | Risk | Likelihood | Impact | Priority | Key mitigations |
|---|---|---|---|---|---|
| R-01 | Admin laptop compromise steals PAT + password + TOTP seed | High | Critical | P0 | Dedicated apply endpoint, vault-only creds, tier-0 controls, EDR |
| R-02 | Malicious config PR weakens org protections | Medium | Critical | P0 | CODEOWNERS, policy checks, separation of duties |
| R-03 | /internal/* endpoint reachable beyond mgmt plane |
Medium | High | P1 | Ingress allowlist + auth proxy + alerting |
| R-04 | Redis/Valkey abuse in-cluster (no auth) | Medium | High | P1 | NetworkPolicies + namespace isolation + monitoring |
| R-05 | Over-scoped PAT includes delete_repo |
Medium | Critical | P0 | Remove scope, split tokens, break-glass process |
| R-06 | Lack of apply audit trail | High | Medium | P1 | Wrapper logging + immutable central store + correlation |
| R-07 | Default configuration referenced by a branch or compromised runs code with otterdog credentials | Low | Critical | P1 | Tag or commit SHA in base_template, protected repository and tags, review of scripts |
10. Security requirements (opinionated baseline)¶
10.1 Credential handling (MUST)¶
- Do not use the
plaincredential provider outside of ephemeral experimentation; enforce via CI/policy checks. (otterdog.readthedocs.io) - Vault-backed credentials only (Bitwarden/pass).
- TOTP seed treated as tier-0 secret: least access, audited retrieval, fast rotation procedure. (otterdog.readthedocs.io)
- Remove
delete_reposcope unless required; use separate “break-glass” credentials for destructive actions. (otterdog.readthedocs.io)
10.2 Admin laptop apply controls (MUST)¶
- Applies must run on a dedicated apply environment (separate machine/VM or hardened OS profile).
- Applies must reference an explicit config commit SHA and clean working tree.
- Operator must run
planimmediately beforeapply(same commit, same environment). - Store apply logs centrally (operator, commit SHA, output hash, timestamp).
10.3 Config repo governance (MUST)¶
-
Branch protection on main:
-
required reviews (CODEOWNERS)
- required CI status checks (syntax + policy)
- restrict who can merge
-
CI policy rules must block:
-
new webhooks without explicit approval
- reductions in ruleset/protection strictness
- expansions of bypass actors and admin roles without security approval
-
Default configuration:
-
base_templatereferences a tag or a commit SHA, never a branch - the repository of the default configuration and its tags are protected like the configuration repository
- changes to its Python scripts are reviewed like code
10.4 WebApp hardening (MUST)¶
- Restrict
/internal/*endpoints to management network only (ingress allowlist); document the management access path. (otterdog.readthedocs.io) - Treat
/internal/initas a privileged operation; alert on execution. (otterdog.readthedocs.io) - Network isolate Redis/Valkey given “no auth” constraint; only allow access from required pods/services. (otterdog.readthedocs.io)
- Keep secrets out of Git: use external secrets, sealed secrets, or equivalent.
11. Operational playbooks (minimum)¶
11.1 Suspected admin laptop compromise¶
- Revoke GitHub PAT(s) used by otterdog; rotate password; rotate TOTP seed. (otterdog.readthedocs.io)
- Rotate WebApp secrets if laptop had any access (webhook secret, app key) (otterdog.readthedocs.io)
- Review GitHub audit log for role/webhook/ruleset changes
-
Re-establish known-good state:
-
pull last approved config commit
- run
planandapplyfrom a clean trusted environment
11.2 Suspicious WebApp behavior¶
- Block ingress access to
/internal/*and verify allowlists (otterdog.readthedocs.io) - Inspect cluster logs for
/internal/initcalls and anomalous Redis traffic - Rotate GitHub App private key and webhook secret if tampering suspected (otterdog.readthedocs.io)
12. Residual risks (explicitly accepted or to be reduced)¶
- Storing/using a TOTP seed as automation input is inherently high risk because it can bypass MFA if stolen. This is required for Web UI-only settings in the documented setup. (otterdog.readthedocs.io)
- Redis/Valkey without authentication is an architectural constraint in the documented WebApp deployment; accept only with strong network isolation. (otterdog.readthedocs.io)
- “Apply from laptops” remains higher risk than “apply from a controlled CI runner.” If requirements change, migrating apply into a hardened CI environment is the single biggest risk reducer.
13. Appendix: Security invariants to encode in CI (examples)¶
These are intentionally strict “guardrails” you can implement as CI policy checks:
- Webhooks: no new org/repo webhooks without allowlisted destinations and security approval.
-
Rulesets/branch protections:
-
cannot reduce required checks
- cannot add bypass actors beyond an allowlist
- cannot enable force pushes / deletions (unless explicitly approved)
-
Admin roles:
-
cannot add org owners/admins except via an allowlisted process
-
Tokens/scopes:
-
configs must not reference
provider: plain(otterdog.readthedocs.io) - break-glass credentials must not be used by default in daily workflows
-
Default configuration:
-
every
base_templateinotterdog.jsonends with a tag or a commit SHA, not a branch