redoubt

module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: AGPL-3.0

README

Redoubt

The self-hosted PaaS you can actually pass an audit with.

Redoubt is a security-hardened, open-source deployment platform for a single VPS: git push → build → hardened container → HTTPS, with managed Postgres/Redis, encrypted backups, a tamper-evident audit log and role-based access control — in one statically compiled Go binary (platformd) plus a CLI (redoubt), all running as hardened containers behind Traefik.

It exists because the convenience of a self-hosted PaaS should not cost you the host. Every design decision in Redoubt removes a class of vulnerability rather than sanitising an instance of it, and every one of those decisions is enforced by a regression test that runs in CI.

Redoubt is not SOC 2 certified and never implies it. It produces the evidence (audit trail, RBAC, encryption at rest, SBOMs, signed releases, a published threat model) that makes an audit tractable. See What "compliance-ready" honestly means.

Why: the security wedge

In 2025–2026 the most popular self-hosted PaaS, Coolify, disclosed a run of critical vulnerabilities — eleven of them OS command injection (CWE-78, CVSS 9.4–10.0), one root SSH key readable by low-privilege members (CWE-522), one stored XSS — while roughly 52,000 of its dashboards sat reachable on the public internet. The root causes were structural: shell strings built from user input, a raw Docker socket in reach of the web tier, secrets in logs, dashboards exposed by default, and a silent auto-updater.

Redoubt's answer is structural too. Each row of the threat model maps a Coolify CVE to the mechanism that makes the class impossible here and to the test that keeps it that way:

Class Redoubt mechanism Enforced by
Command injection (CWE-78) No shell strings, anywhere. Every Docker operation is a typed call to the Engine API; there is no os/exec in the control plane. Backups, restores and scans are one-shot hardened containers with an argv slice. TestNoShellExecFromUserInput
Raw Docker socket Only the socket-proxy holds docker.sock; every request passes a method + path allow-list that denies exec, privileged containers and host mounts — even for a fully compromised platformd. TestNoContainerGetsTheDockerSocket, TestSocketProxyDeniesExec
Credential exposure (CWE-522) No privileged host credential exists in app scope: no SSH keys, age-encrypted secrets that are write-only for every role, redacted from every log. TestViewerCannotDeployOrRevealSecrets, TestSecretsNeverInLogs
Exposed dashboards The control plane binds 127.0.0.1:8443; exposure is an explicit operator action. TOTP, lockout and rate limits regardless. TestDashboardBindsLocalhostByDefault
Silent auto-update Updates are opt-in and cosign-verified before anything is swapped; enabling the timer is itself an audited action. TestUpdateVerifiesCosignSignature
Stored XSS (CWE-116) Compile-time-checked templ templates with contextual escaping, a strict CSP, no inline scripts. TestCSRFAndSecurityHeaders, TestNoExternalScriptTags

Quickstart (a fresh Ubuntu 24.04 VPS, ~10 minutes)

  1. Point a wildcard DNS record (*.example.com) at the server.
  2. Download and verify the signed release, then install:
VERSION=v1.0.0   # see https://github.com/BetV3/redoubt/releases
curl -fsSLO "https://github.com/BetV3/redoubt/releases/download/${VERSION}/redoubt-server_${VERSION#v}_linux_amd64.tar.gz"
curl -fsSLO "https://github.com/BetV3/redoubt/releases/download/${VERSION}/checksums.txt"
curl -fsSLO "https://github.com/BetV3/redoubt/releases/download/${VERSION}/checksums.txt.sig"
curl -fsSLO "https://github.com/BetV3/redoubt/releases/download/${VERSION}/checksums.txt.pem"

cosign verify-blob --certificate checksums.txt.pem --signature checksums.txt.sig \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity "https://github.com/BetV3/redoubt/.github/workflows/release.yml@refs/tags/${VERSION}" \
  checksums.txt
sha256sum -c checksums.txt --ignore-missing

tar xzf "redoubt-server_${VERSION#v}_linux_amd64.tar.gz" && cd "redoubt-server_${VERSION#v}_linux_amd64"
sudo ./install/install.sh --domain example.com --email ops@example.com --version "${VERSION}"

install.sh installs Docker with a hardened daemon.json, loads the AppArmor profile, writes the compose bundle to /etc/redoubt, starts it as a systemd unit, hardens the host (key-only SSH, ufw default-deny, fail2ban, unattended security upgrades, auditd) and prints a self-audit summary. It never pipes anything through a shell and never prints a secret.

  1. From your workstation, tunnel to the localhost-only dashboard and create the first Owner:
ssh -L 8443:127.0.0.1:8443 you@server
redoubt bootstrap --email you@example.com      # uses the one-time bootstrap token, then disables it
redoubt login --email you@example.com
redoubt totp enroll                             # second factor
  1. Deploy something:
redoubt apps create shop --host shop.example.com --port 8080
redoubt secrets set shop DATABASE_PASSWORD        # value from stdin, write-only from now on
redoubt git set shop --provider github --url https://github.com/you/shop.git --branch main
# add the printed webhook URL + secret to the repository; every push now builds and deploys
redoubt addons create shop postgres db            # hardened Postgres on the app's network, URL injected as a secret
redoubt addons backup shop db --wait              # age-encrypted dump → S3-compatible storage
redoubt audit verify-chain                        # the tamper-evident log verifies

Or try everything locally first (Docker required, no real domain needed): make demo-phase0make demo-phase3 run the end-to-end checkpoints against a Pebble ACME server and MinIO.

Secure by default

Every app container gets, automatically:

  • non-root user, no-new-privileges, cap_drop: ALL with a minimal add-back list;
  • read-only root filesystem with explicit noexec,nosuid,nodev tmpfs mounts;
  • the default seccomp profile and an AppArmor profile;
  • memory, CPU and pids limits;
  • its own Docker network, with Traefik as the only way in.

Relaxing a default needs an explicit override flag, logs a warning and writes an audit event.

The platform itself:

  • runs as non-root, read-only-rootfs containers with no Docker socket (only the socket-proxy holds it, behind an allow-list);
  • keeps secrets age-encrypted at rest, injects them at runtime, redacts them from every log and passes them to builds as BuildKit secret mounts only — never into image layers;
  • writes every state change to a SHA-256 hash-chained, append-only audit log you can export and verify offline (redoubt audit export, redoubt audit verify-export);
  • enforces Owner / Admin / Deployer / Viewer in the service layer, not the UI;
  • scans every image with Trivy (block or warn per app) and stores an SBOM per release;
  • binds the dashboard to localhost, requires TOTP, locks out brute force and rate-limits;
  • backs up addons age-encrypted to S3-compatible storage and replicates its own SQLite database continuously with Litestream;
  • ships cosign-signed releases and verifies them before any update (redoubt update).

redoubt self-audit scores the running installation against this list, and the dashboard's Security page maps the evidence to SOC 2 Trust Services Criteria.

Architecture

                 ┌────────────────────────── VPS ───────────────────────────┐
  *.example.com  │  Traefik (80/443, ACME)  ── per-app network ── app ctr    │
  ──────────────▶│      file provider ◀── platformd (127.0.0.1:8443)         │
                 │                            │  SQLite + River + age key    │
                 │                            │  ──▶ socket-proxy ──▶ docker │
                 │                            │        (allow-list)   .sock  │
                 │  rootless buildkitd ◀──────┘   litestream ──▶ S3          │
                 │  one-shot jobs: pg_dump / trivy / syft (hardened, argv)   │
                 └───────────────────────────────────────────────────────────┘
  • platformd — control plane: chi + templ/HTMX dashboard, /api/v1 JSON API, River jobs on SQLite, deployment state machine with health-gated cutover and rollback.
  • socket-proxy — the only holder of the Docker socket; least-privilege endpoint allow-list.
  • Traefik — file-provider only, zero Docker access, ACME certificates per app hostname.
  • rootless BuildKit — builds from git checkouts with secret mounts; never sees a secret value.

Details: docs/ARCHITECTURE.md. Local-simulation vs production gaps: docs/DEPLOY_NOTES.md. Decisions: docs/DECISIONS.md.

Security posture and threat model

  • docs/SECURITY.md — posture, secrets, audit log, supply chain, how to verify a release, disclosure policy and supported versions.
  • docs/THREAT_MODEL.md — assets, trust boundaries, attacker personas, STRIDE-lite, and the CVE-class → mitigation → regression-test table.
  • Report a vulnerability: see SECURITY.md §11.

Roadmap

docs/ROADMAP.md. v1 is Dockerfile-first on a single VPS; SSO, preview environments and multi-server are candidates for v1.x, prioritised by pilot feedback.

License

The core is licensed under the GNU AGPL v3. A commercial edition with additional enterprise features (SSO, compliance reporting) may be offered separately; the security properties described here are and will remain in the open-source core.

Contributing

  • make tools && make generate && make build
  • make test (unit + security regression tests, no Docker) and make test-integration (real bundle: build → hardened run → route → rollback; requires Docker)
  • make lint (golangci-lint, gosec, govulncheck, shellcheck) must pass; conventional commits.
  • Read CLAUDE.md for the golden rules every change is held to. Anything that weakens one of them needs a decision record in docs/DECISIONS.md first.

Directories

Path Synopsis
cmd
platformd command
Command platformd is the Redoubt control-plane daemon.
Command platformd is the Redoubt control-plane daemon.
redoubt command
Command redoubt is the Redoubt CLI client.
Command redoubt is the Redoubt CLI client.
socket-proxy command
Command socket-proxy is Redoubt's least-privilege Docker socket proxy (D-002, D-003).
Command socket-proxy is Redoubt's least-privilege Docker socket proxy (D-002, D-003).
Package install is the operator-facing install layer: install.sh, the compose bundle and its dev/demo overlay, the hardened daemon.json, the systemd unit, the seccomp and AppArmor profiles and the demo scripts.
Package install is the operator-facing install layer: install.sh, the compose bundle and its dev/demo overlay, the hardened daemon.json, the systemd unit, the seccomp and AppArmor profiles and the demo scripts.
internal
addons
Package addons manages Postgres and Redis addon containers for apps (E2.3): hardened containers on the owning app's network, generated credentials stored age-encrypted and injected into the app as ordinary write-only secrets, and encrypted backups to S3-compatible storage (see backup.go).
Package addons manages Postgres and Redis addon containers for apps (E2.3): hardened containers on the owning app's network, generated credentials stored age-encrypted and injected into the app as ordinary write-only secrets, and encrypted backups to S3-compatible storage (see backup.go).
audit
Package audit defines the control plane's audit-log interface (golden rule 5: every state-changing action writes an event).
Package audit defines the control plane's audit-log interface (golden rule 5: every state-changing action writes an event).
auth
Package auth is part of the Redoubt control plane.
Package auth is part of the Redoubt control plane.
backup
Package backup implements encrypted addon backups to S3-compatible storage and the minimal AWS Signature V4 client they need.
Package backup implements encrypted addon backups to S3-compatible storage and the minimal AWS Signature V4 client they need.
build
Package build turns a validated git checkout into a tagged Docker image through rootless BuildKit (docs/BUILD_PLAN.md E1.2, docs/DECISIONS.md D-018).
Package build turns a validated git checkout into a tagged Docker image through rootless BuildKit (docs/BUILD_PLAN.md E1.2, docs/DECISIONS.md D-018).
config
Package config loads and validates platformd's configuration from the environment.
Package config loads and validates platformd's configuration from the environment.
deploy
Package deploy is part of the Redoubt control plane.
Package deploy is part of the Redoubt control plane.
docker
Package docker is part of the Redoubt control plane.
Package docker is part of the Redoubt control plane.
docker/dockertest
Package dockertest provides a minimal fake Docker Engine API for unit tests in other packages.
Package dockertest provides a minimal fake Docker Engine API for unit tests in other packages.
git
Package git clones application repositories for the deploy pipeline using the pure-Go go-git library (D-013).
Package git clones application repositories for the deploy pipeline using the pure-Go go-git library (D-013).
goldenrules
Package goldenrules holds repository-wide static regression tests for the golden rules in CLAUDE.md.
Package goldenrules holds repository-wide static regression tests for the golden rules in CLAUDE.md.
httpapi
Package httpapi is part of the Redoubt control plane.
Package httpapi is part of the Redoubt control plane.
proxy
Package proxy is part of the Redoubt control plane.
Package proxy is part of the Redoubt control plane.
queue
Package queue runs the River job queue on the control-plane SQLite database, with the workers embedded in platformd (E1.3; golden rule 7: no Redis, no separate worker process).
Package queue runs the River job queue on the control-plane SQLite database, with the workers embedded in platformd (E1.3; golden rule 7: no Redis, no separate worker process).
scan
Package scan is part of the Redoubt control plane.
Package scan is part of the Redoubt control plane.
secrets
Package secrets implements golden rule 3 for the Redoubt control plane: secret values are age-encrypted at rest and never logged, printed or displayed.
Package secrets implements golden rule 3 for the Redoubt control plane: secret values are age-encrypted at rest and never logged, printed or displayed.
selfaudit
Package selfaudit scores a running Redoubt installation against its own secure-by-default checklist, in the spirit of docker-bench-security (E2.7): daemon configuration, the compose bundle (socket-proxy, Traefik, platformd, buildkitd), every platform-managed container, the control-plane's own state (Owner TOTP, bootstrap token, key file mode, audit chain) and, when it runs on the host, a few daemon files.
Package selfaudit scores a running Redoubt installation against its own secure-by-default checklist, in the spirit of docker-bench-security (E2.7): daemon configuration, the compose bundle (socket-proxy, Traefik, platformd, buildkitd), every platform-managed container, the control-plane's own state (Owner TOTP, bootstrap token, key file mode, audit chain) and, when it runs on the host, a few daemon files.
store
Package store is part of the Redoubt control plane.
Package store is part of the Redoubt control plane.
update
Package update implements `redoubt update`: opt-in, never silent, cosign-verified updates of the platform images (plan §3, golden guardrail "auto-update is an attack vector").
Package update implements `redoubt update`: opt-in, never silent, cosign-verified updates of the platform images (plan §3, golden guardrail "auto-update is an attack vector").
update/sigtest
Package sigtest is a miniature Sigstore for tests and demos: a Fulcio-like CA that issues short-lived code-signing certificates carrying the OIDC identity extensions, a Rekor-like key that signs entry timestamps, and an in-memory OCI registry that serves images and their cosign signature manifests.
Package sigtest is a miniature Sigstore for tests and demos: a Fulcio-like CA that issues short-lived code-signing certificates carrying the OIDC identity extensions, a Rekor-like key that signs entry timestamps, and an in-memory OCI registry that serves images and their cosign signature manifests.
update/sigtest/demosign command
demosign signs images in a local (plain-HTTP, loopback) registry with a throwaway Sigstore (sigtest) and writes the matching trust root, so `make demo-phase3` can exercise `redoubt update` end to end without GitHub OIDC.
demosign signs images in a local (plain-HTTP, loopback) registry with a throwaway Sigstore (sigtest) and writes the matching trust root, so `make demo-phase3` can exercise `redoubt update` end to end without GitHub OIDC.
webhooks
Package webhooks receives Git push webhooks from GitHub, GitLab and Gitea and turns them into deploy triggers.
Package webhooks receives Git push webhooks from GitHub, GitLab and Gitea and turns them into deploy triggers.
Package migrations embeds the goose SQL migrations so a single binary can bring any data directory up to date.
Package migrations embeds the goose SQL migrations so a single binary can bring any data directory up to date.
web
Package web embeds the dashboard's static assets (hand-written CSS/JS plus the vendored, version-pinned HTMX, HTMX SSE extension and Alpine.js builds — golden rule 7: no CDN, no SPA framework) and serves them with content-hashed, immutable cache URLs.
Package web embeds the dashboard's static assets (hand-written CSS/JS plus the vendored, version-pinned HTMX, HTMX SSE extension and Alpine.js builds — golden rule 7: no CDN, no SPA framework) and serves them with content-hashed, immutable cache URLs.
templates
templ: version: v0.3.1020
templ: version: v0.3.1020

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL