| Asset | Sensitivity |
|---|---|
| Git forge PATs / SSH keys | Critical |
| Deployment webhook URLs/tokens | High |
| Runtime API tokens (Portainer) | High |
| Secret provider tokens | Critical |
| Session secrets / encryption master key | Critical |
| Repository source (may include non-secret config) | Medium |
| Runtime logs (may leak secrets) | High |
| Audit trail | Medium (must not contain secrets) |
Browser UI --HTTPS--> stack-manager --HTTPS--> Git forge
|-----------> Deploy webhooks
|-----------> Runtime APIs (RO)
|-----------> Secret APIs
|----file----> Data volume (SQLite, clones)
Assume: operator places stack-manager on a trusted homelab network behind auth and TLS termination; still design for stolen-backup and SSRF cases.
| Threat | Control |
|---|---|
| Credential theft from DB dump | AEAD encryption; master key separate from backup |
| Credential leakage to browser | API contract omits secrets after save |
| Token in logs | Structured logging redaction middleware; forbid logging Authorization headers / webhook URLs raw |
| Cleartext credential transport | HTTPS with certificate validation required for credential-bearing webhooks, forge APIs, runtime APIs, and secret APIs; HTTPS/SSH for Git remotes; reject plaintext before sending secrets |
| Audit leakage | Redaction policy; URL sanitisation |
| Forge webhook forgery | HMAC/signature verify; ignore unsigned |
| Webhook replay | Idempotency keys |
| SSRF via webhook URL config | Validate URL scheme; deny localhost/metadata by default with documented RFC1918 allow option for homelab |
| Malicious Compose aiming at host | No Docker socket; no exec; deploy happens in external system |
| Privilege escalation via Portainer token | Document least privilege; app allowlists read-only API calls |
| Supply chain | Lockfiles; minimal deps; container non-root where practical |
| CSRF | SameSite sessions + origin checks on mutating routes |
| XSS in log viewer | Treat logs as untrusted text; strict encoding |
| Path traversal in git file APIs | Repo paths normalised and scoped to the stack folder; .., .git and control characters rejected; content read from Git objects (symlinks are listed, never followed) |
| Secret files in repositories | .env* (except templates), keys/keystores and secrets/ paths are shown as locked; contents are never returned to the browser or stored as drafts |
| XSS via repository Markdown | Rendered without raw HTML; unsafe URLs dropped; relative links mapped to stack routes; only https images, no referrer |
- No arbitrary Docker exec
- No destructive runtime mutations through RuntimeProvider
- No storing secret values from Infisical into Git
- Encryption key required at boot (PR #2 — process exits on missing/malformed key)
- Secret redaction tests (PR #2 — logger, audit, job payloads, API responses, data dir scan)
- Webhook signature tests
- SSRF unit tests (PR #2 — Git remote host policy; deploy webhook URLs in PR #5)
- Log viewer XSS tests
- Dependency vulnerability scanning in CI (PR #2 —
pnpm audit --prod; Trivy onpnpm-lock.yaml) - Secret scanning of the full Git history on every PR, push and nightly (gitleaks,
.gitleaks.toml) - Container image scanning: Trivy gate on fixable HIGH/CRITICAL for PRs touching the image, weekly, and before every release publish; Dockerfile misconfiguration scan; runtime image ships without npm/npx/corepack/yarn
- Pinned GitHub Actions (commit SHAs) with Dependabot updates; release images carry SBOM + provenance
- Git invocation:
gitis spawned with an argv array (no shell),--before URLs, validated branch names (no leading-), a protocol allowlist (httpsonly), hooks disabled, no system/global config, an isolatedHOME, no credential helpers or prompts, and redirects not followed (auth headers are never replayed to another host). Tokens travel as anhttp.extraHeaderviaGIT_CONFIG_*environment variables — never in argv, remote URLs or.git/config— and are scrubbed from any git error output. - Remote URL policy: HTTPS only; embedded credentials, query strings and traversal segments are rejected.
Resolved addresses must not be loopback, link-local/metadata or multicast — including IPv4 embedded in IPv6
(mapped, compatible, translated and NAT64 forms, dotted or hex); private ranges require
STACK_MANAGER_ALLOW_PRIVATE_NETWORKS=true. Known gap: the address is checked beforegitresolves the host itself (DNS rebinding / TOCTOU); pinning the resolved address (http.curloptResolve) is a planned follow-up. Mitigations today: HTTPS only, redirects not followed, private networks denied by default. - Paths: clone directories are derived from server-generated ids and resolved with containment checks
(including symlinks) under
<data dir>/repos. - Headers:
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy,Permissions-Policy, COOP and a CSP limited toframe-ancestors/base-uri/form-action/object-src(script nonces: PR #8). - Container: non-root, read-only root filesystem, all capabilities dropped,
no-new-privileges, no Docker socket.
If a Portainer token with excess privileges is configured, the application must still refuse to expose destructive actions. Compromised tokens remain a Portainer-side risk — document rotation steps.