e9050b3518
- docker-compose: mount /var/run/docker.sock into sso-manager so the seeded docker-local plugin can list containers (was ENOENT -> 'Last run: error') - gitlinks: sso-manager-node 49100c9 (v1.28.0), theta-agent 51750d0 (v1.3.1)
314 lines
14 KiB
YAML
314 lines
14 KiB
YAML
# theta-suite — unified SSO Manager + Proxy.
|
|
#
|
|
# Brings up the two all-in-one images on one bridge network so the proxy can
|
|
# reach the SSO internally (http://sso-manager:3001 for token/userinfo,
|
|
# ldaps://sso-manager:636 for LDAP) without exposing the SSO's HTTP port to the
|
|
# internet. The proxy is the public front (80/443); the SSO sits behind it.
|
|
#
|
|
# Each project builds from its git submodule:
|
|
# ./sso-manager-node -> Dockerfile.openldap (app + OpenLDAP + Redis)
|
|
# ./proxy -> Dockerfile (OpenResty + app + Redis)
|
|
# So `git clone --recursive` is required to get the submodules first.
|
|
#
|
|
# Config + secrets live in bind-mounted ./config/ (gitignored):
|
|
# ./config/sso-secrets.js — SSO app + orchestrator config
|
|
# ./config/proxy-secrets.js — proxy OIDC/LDAP/auth config
|
|
# Each app's entrypoint points CONF_SECRETS at its file so @simpleworkjs/conf
|
|
# (>= 1.2.0) reads it directly -- no app_* env is passed (app_* env would
|
|
# override secrets.js), and no write access to /app/conf is needed. The
|
|
# sso-manager mounts ./config read-write so the bootstrap can write the
|
|
# generated OAuth client creds back into proxy-secrets.js; the proxy mounts
|
|
# it read-only.
|
|
#
|
|
# Compose only interpolates the port defaults below — there is no .env file.
|
|
# First-run wiring (LDAP service account, first admin, OAuth client) is
|
|
# automated by ./setup.sh, which runs bootstrap/bootstrap.js inside the
|
|
# sso-manager container.
|
|
|
|
services:
|
|
sso-manager:
|
|
build:
|
|
context: ./sso-manager-node
|
|
dockerfile: Dockerfile.openldap
|
|
args:
|
|
# A submodule's .git is a pointer file, not a real repo — the image
|
|
# can't resolve its own commit hash from inside the build context.
|
|
# setup.sh sets this from the host, where the submodule resolves
|
|
# correctly (git -C sso-manager-node rev-parse --short HEAD).
|
|
GIT_COMMIT: ${SSO_GIT_COMMIT:-}
|
|
# Optional upstream HTTP(S) proxy for npm/apt during the build (NOT
|
|
# the theta42 "proxy" app). Set CFG_HTTP_PROXY in setup.env; empty by
|
|
# default, so this is a no-op unless configured.
|
|
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
|
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
|
NO_PROXY: ${CFG_NO_PROXY:-}
|
|
container_name: sso-manager
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- openbao
|
|
networks: [theta-net]
|
|
ports:
|
|
# SSO web UI. Bind address is configurable via SSO_BIND (default 0.0.0.0 so
|
|
# the UI is reachable on the LAN during setup). Set SSO_BIND=127.0.0.1 to
|
|
# lock it to localhost once the proxy fronts it at https://<SSO_HOST>.
|
|
- "${SSO_BIND:-0.0.0.0}:${SSO_PORT:-3001}:3001"
|
|
# LDAPS (636) + plain LDAP (389) for direct-LDAP clients AND for the stack
|
|
# host's OWN enrollment: setup.sh / ldap-client configure the host's sssd
|
|
# against ldap://localhost and ldaps://localhost, and the LDAP server is
|
|
# co-located on this host, so BOTH ports must be reachable from the host
|
|
# over loopback — not only over the docker network. Bind 0.0.0.0 (default)
|
|
# so LAN clients can use the host's local IP too; set LDAP_BIND and/or
|
|
# LDAPS_BIND=127.0.0.1 to lock either to the host only. Prefer an internal
|
|
# hostname (CFG_LDAPS_HOST) and do NOT forward 389/636 to the public internet.
|
|
- "${LDAP_BIND:-0.0.0.0}:${LDAP_PORT:-389}:389"
|
|
- "${LDAPS_BIND:-0.0.0.0}:${LDAPS_PORT:-636}:636"
|
|
environment:
|
|
# Config (LDAP, OAuth, SMTP, ...) is loaded by @simpleworkjs/conf from
|
|
# ./config/sso-secrets.js (see volumes), then @simpleworkjs/bao-conf
|
|
# deep-merges secret/sso-manager/conf from OpenBao over it at boot
|
|
# (VAULT_ADDR/VAULT_TOKEN below). NODE_ENV/NODE_PORT are the only other
|
|
# env the app reads. VAULT_TOKEN is the scoped SSO_VAULT_TOKEN minted by
|
|
# setup.sh (policy sso-broker) — NOT the root token.
|
|
- NODE_ENV=production
|
|
- NODE_PORT=3001
|
|
- LDAP_SERVER_ID=${LDAP_SERVER_ID:-}
|
|
- LDAP_REPLICATION_HOSTS=${LDAP_REPLICATION_HOSTS:-}
|
|
- VAULT_ADDR=http://openbao:8200
|
|
- VAULT_TOKEN=${SSO_VAULT_TOKEN:-}
|
|
# Optional upstream HTTP(S) proxy for outbound calls (SMTP, etc.) at
|
|
# runtime. See the build args above for the same setting during build.
|
|
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
|
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
|
- NO_PROXY=${CFG_NO_PROXY:-}
|
|
volumes:
|
|
# The host docker socket so the bundled Docker discovery plugin (seeded as
|
|
# 'docker-local' with socketPath /var/run/docker.sock) can list containers.
|
|
# Without this the plugin errors with ENOENT and shows 'Last run: error'.
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
# Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap
|
|
# can write the generated OAuth client creds into proxy-secrets.js. The
|
|
# entrypoint points CONF_SECRETS at /config/sso-secrets.js.
|
|
- ./config:/config
|
|
# Persist the LDAP database across container recreation.
|
|
- ldap-data:/var/lib/ldap
|
|
# Persist the auto-generated self-signed TLS cert so clients don't have to
|
|
# re-trust it on every rebuild.
|
|
- ldap-certs:/etc/openldap/certs
|
|
# Persist Redis (AOF + RDB) so OAuth clients, tokens, and other Redis state
|
|
# survive container recreation.
|
|
- sso-data:/data
|
|
# Bind-mount the bootstrap script so `docker compose exec sso-manager node
|
|
# /bootstrap/bootstrap.js` can run it (read-only).
|
|
- ./bootstrap:/bootstrap:ro
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
proxy:
|
|
build:
|
|
context: ./proxy
|
|
dockerfile: Dockerfile
|
|
args:
|
|
# A submodule's .git is a pointer file, not a real repo — the image
|
|
# can't resolve its own commit hash from inside the build context.
|
|
# setup.sh sets this from the host, where the submodule resolves
|
|
# correctly (git -C proxy rev-parse --short HEAD).
|
|
GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
|
|
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
|
|
# the sso-manager service above for details.
|
|
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
|
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
|
NO_PROXY: ${CFG_NO_PROXY:-}
|
|
container_name: proxy
|
|
restart: unless-stopped
|
|
networks: [theta-net]
|
|
depends_on:
|
|
sso-manager:
|
|
condition: service_healthy
|
|
openbao:
|
|
condition: service_started
|
|
ports:
|
|
- "${HTTP_PORT:-80}:80"
|
|
- "${HTTPS_PORT:-443}:443"
|
|
- "${HTTPS_ALT_PORT:-4443}:4443"
|
|
# Management UI/API. Bind address is configurable via MGMT_BIND (default
|
|
# 0.0.0.0 so it's reachable on the LAN during setup). Set MGMT_BIND=127.0.0.1
|
|
# to lock it to localhost once the proxy fronts it under TLS.
|
|
- "${MGMT_BIND:-0.0.0.0}:${MGMT_PORT:-3000}:3000"
|
|
environment:
|
|
# oidc/ldap/auth config is loaded by @simpleworkjs/conf from
|
|
# ./config/proxy-secrets.js (see volumes), then @simpleworkjs/bao-conf
|
|
# deep-merges secret/proxy/conf from OpenBao over it at boot. The OAuth
|
|
# clientSecret is consumed at require time, so bao-conf.init() runs
|
|
# BEFORE require('../app') in bin/www. NODE_ENV/NODE_PORT are process env
|
|
# the app reads directly. VAULT_TOKEN is the scoped PROXY_VAULT_TOKEN
|
|
# (policy proxy — read only secret/proxy/conf).
|
|
- NODE_ENV=production
|
|
- NODE_PORT=3000
|
|
- VAULT_ADDR=http://openbao:8200
|
|
- VAULT_TOKEN=${PROXY_VAULT_TOKEN:-}
|
|
# Optional upstream HTTP(S) proxy for outbound calls (ACME/Let's
|
|
# Encrypt, DNS providers) at runtime.
|
|
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
|
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
|
- NO_PROXY=${CFG_NO_PROXY:-}
|
|
volumes:
|
|
# Operator-edited proxy secrets (proxy-secrets.js). READ-ONLY — the proxy
|
|
# only reads it; the sso-manager bootstrap writes the OAuth creds. The
|
|
# entrypoint points CONF_SECRETS at /config/proxy-secrets.js. Kept as a
|
|
# fail-soft fallback: bao-conf.init() is fail-soft, so if OpenBao is
|
|
# unreachable the app boots from this file instead.
|
|
- ./config:/config:ro
|
|
# Persist Redis (AOF + RDB) so Host records, permissions, DNS creds, local
|
|
# users, AND the auto-ssl Let's Encrypt certs survive container recreation.
|
|
- proxy-data:/data
|
|
- proxy-cache:/var/cache/nginx/proxy
|
|
- proxy-logs:/var/log/nginx
|
|
# OPTIONAL, for strict LDAPS trust (see README "Security notes"): mount
|
|
# the SSO's self-signed cert into the proxy read-only, then set
|
|
# ldap.tlsOptions.ca=<path-below> in ./config/proxy-secrets.js.
|
|
# - ldap-certs:/etc/ssl/sso-ldap-certs:ro
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
# SSH jump host — a core component, always built + started alongside the
|
|
# SSO and proxy. Authenticates users against the SSO's OpenLDAP, resolves
|
|
# reachable hosts from the directory API, and bridges SSH through.
|
|
jump-host:
|
|
build:
|
|
context: ./jump-host
|
|
dockerfile: Dockerfile
|
|
args:
|
|
GIT_COMMIT: ${JUMP_GIT_COMMIT:-}
|
|
# Optional upstream HTTP(S) proxy for npm/apt during the build. See
|
|
# the sso-manager service above for details.
|
|
HTTP_PROXY: ${CFG_HTTP_PROXY:-}
|
|
HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
|
|
NO_PROXY: ${CFG_NO_PROXY:-}
|
|
container_name: jump-host
|
|
restart: unless-stopped
|
|
networks: [theta-net]
|
|
depends_on:
|
|
sso-manager:
|
|
condition: service_healthy
|
|
openbao:
|
|
condition: service_started
|
|
ports:
|
|
- "${JUMP_SSH_PORT:-2222}:2222" # SSH front door
|
|
- "${JUMP_WEB_BIND:-0.0.0.0}:${JUMP_WEB_PORT:-3002}:3002" # web UI/API
|
|
environment:
|
|
- NODE_ENV=production
|
|
# Secrets are loaded by @simpleworkjs/conf from ./config/jump-secrets.js,
|
|
# then @simpleworkjs/bao-conf deep-merges secret/jump-host/conf from
|
|
# OpenBao over it at boot. VAULT_TOKEN is the scoped JUMP_VAULT_TOKEN
|
|
# (policy jump-host — read only secret/jump-host/conf).
|
|
- VAULT_ADDR=http://openbao:8200
|
|
- VAULT_TOKEN=${JUMP_VAULT_TOKEN:-}
|
|
# Optional upstream HTTP(S) proxy for outbound calls (the directory API
|
|
# client) at runtime.
|
|
- HTTP_PROXY=${CFG_HTTP_PROXY:-}
|
|
- HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
|
|
- NO_PROXY=${CFG_NO_PROXY:-}
|
|
volumes:
|
|
- ./config:/config:ro # jump-secrets.js (written by ensure_config/bootstrap)
|
|
- jump-data:/var/lib/jump-host # generated host keys persist here
|
|
- jump-redis-data:/data # Redis (sessions, OAuth state, API tokens) persists here
|
|
|
|
# A real, LDAP-joined (SSSD + AuthorizedKeysCommand) downstream host for
|
|
# testing jump-host's actual key-injection -> upstream-connect flow --
|
|
# a container with a manually-dropped public key in authorized_keys never
|
|
# exercises the LDAP-key-serving path a real production host does. Built
|
|
# from the theta42/ldap-client submodule -- see ./config/ldap-test-host.vars
|
|
# for setup notes. Opt-in test fixture: bring it up explicitly with
|
|
# `docker compose --profile ldap-test up` (jump-host itself now starts
|
|
# unconditionally, so this only adds a downstream host for it to reach).
|
|
ldap-test-host:
|
|
profiles: ["ldap-test"]
|
|
build:
|
|
context: ./ldap-client
|
|
dockerfile: Dockerfile
|
|
container_name: ldap-test-host
|
|
hostname: ldap-test-host
|
|
restart: unless-stopped
|
|
networks: [theta-net]
|
|
depends_on:
|
|
sso-manager:
|
|
condition: service_healthy
|
|
privileged: false
|
|
volumes:
|
|
- ./config/ldap-test-host.vars:/config/ldap.vars:ro
|
|
- ./config/ldap-ca.crt:/config/ldap-ca.crt:ro
|
|
|
|
# Renews the three periodic service tokens (theta-svc role, 768h period)
|
|
# every 12h. Periodic tokens live forever ONLY while something renews them —
|
|
# this sidecar is that something, so the stack survives arbitrarily long
|
|
# uptimes and the tokens in .env never silently expire. If a token is missing
|
|
# or already dead it just logs and moves on (setup.sh re-mints on next run).
|
|
bao-renewer:
|
|
image: quay.io/openbao/openbao:latest
|
|
container_name: bao-renewer
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- openbao
|
|
environment:
|
|
- BAO_ADDR=http://openbao:8200
|
|
- SSO_VAULT_TOKEN=${SSO_VAULT_TOKEN:-}
|
|
- PROXY_VAULT_TOKEN=${PROXY_VAULT_TOKEN:-}
|
|
- JUMP_VAULT_TOKEN=${JUMP_VAULT_TOKEN:-}
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
renew() {
|
|
if [ -z "$$2" ]; then return 0; fi
|
|
if BAO_TOKEN="$$2" bao token renew > /dev/null 2>&1; then
|
|
echo "[bao-renewer] renewed $$1"
|
|
else
|
|
echo "[bao-renewer] FAILED to renew $$1 (expired/revoked? re-run setup.sh to re-mint)"
|
|
fi
|
|
}
|
|
while true; do
|
|
renew SSO_VAULT_TOKEN "$$SSO_VAULT_TOKEN"
|
|
renew PROXY_VAULT_TOKEN "$$PROXY_VAULT_TOKEN"
|
|
renew JUMP_VAULT_TOKEN "$$JUMP_VAULT_TOKEN"
|
|
sleep 43200
|
|
done
|
|
networks:
|
|
- theta-net
|
|
|
|
openbao:
|
|
image: quay.io/openbao/openbao:latest
|
|
container_name: openbao
|
|
restart: unless-stopped
|
|
cap_add:
|
|
- IPC_LOCK
|
|
command: server -config=/vault/config/openbao.hcl
|
|
environment:
|
|
- BAO_ADDR=http://127.0.0.1:8200
|
|
ports:
|
|
- "8080:8200"
|
|
volumes:
|
|
- ./config/openbao.hcl:/vault/config/openbao.hcl:ro
|
|
- openbao-data:/vault/data
|
|
networks:
|
|
- theta-net
|
|
networks:
|
|
theta-net:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
ldap-data:
|
|
ldap-certs:
|
|
sso-data:
|
|
proxy-data:
|
|
proxy-cache:
|
|
proxy-logs:
|
|
jump-data:
|
|
jump-redis-data:
|
|
openbao-data: |