dumbtv
dumbtv가 ADB로 Fire TV에서 광고·추적·업데이트 알림을 제거, 루트·앱 변경 없이, 무료 오픈소스
홈랩·소규모 기업용 원스톱 SSO 관리자와 OIDC 프록시 스택, setup.sh 하나로 자체 ID 제공자와 리버스 프록시를 연결
A unified, one-command SSO Manager + OIDC proxy stack for home labs and small businesses. Wires together a self-hosted identity provider and a reverse proxy with one setup.sh.
보안Shell · JavaScriptGitHub @theta42온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Unified, one-command SSO Manager + OIDC proxy stack for home labs and small businesses
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Theta Suite 2.0 is your production-grade, single-command solution for replacing fragmented identity, gateway, and host management setups with a unified zero-trust infrastructure stack. It seamlessly integrates OIDC identity, LDAP directories, automated host enrollment, WireGuard mesh routing, and centralized secret management in one command.
Theta Suite composes core applications around a shared OpenBao secrets store, brought up with a single ./setup.sh:README 전문 보기 ↗Theta Suite 2.0 is your production-grade, single-command solution for replacing fragmented identity, gateway, and host management setups with a unified zero-trust infrastructure stack. It seamlessly integrates OIDC identity, LDAP directories, automated host enrollment, WireGuard mesh routing, and centralized secret management in one command.
Theta Suite composes core applications around a shared OpenBao secrets store, brought up with a single ./setup.sh:
All applications load their secrets from OpenBao at boot; ./setup.sh automates the first-run glue so components discover each other and the secrets engine automatically.
Site: https://theta42.github.io/theta-suite/ • Community & Chronicles: https://community.theta42.com/
The Theta Directory and Theta Proxy, stood up by one ./setup.sh run:
| Theta Directory Dashboard | Proxy Hosts |
|---|---|
┌───────────────────────────────────────────────────────────┐
│ browser / OIDC apps │ SSH clients │ Linux hosts │
│ │ │ (PAM/SSSD, sudo, keys)│
└───────┬─────────────┴───────┬─────┴────────────┬──────────┘
https (:443) ssh (:2222) ldaps (:636)
│ │ │
┌────────▼─────────┐ ┌────────▼──────────┐ │
│ theta-proxy │ │ theta-gateway │ │
│ OpenResty │ │ SSH Gateway │ │
│ :80/:443/:4443 │ │ WireGuard Mesh │ │
│ mgmt app :3000 │ └────────┬──────────┘ │
└────────┬─────────┘ │ OIDC + LDAP │
│ http:3001 (internal)│ via theta-directory
▼ ▼ ▼
┌───────────────────────────────────────────────────────┐
│ theta-directory (Express + OpenLDAP + Redis) │
│ OIDC provider + LDAP directory + Resource Catalog │
│ web UI :3001 (internal) ldaps :636 (published) │
└───────────────────────────────────────────────────────┘
▲ loads secrets at boot (scoped token each)
┌───────────┴───────────────────┐
│ openbao (KV-v2 at secret/) │ ← central secrets store
│ :8200 (internal) │ per-user + per-app KV
│ :8080 (operator UI/API) │
└───────────────────────────────┘
The proxy fronts the SSO Manager UI (and the jump-host web UI) under TLS and protects them with OIDC login. It is both an OIDC client of the SSO (for login) and a direct LDAP client (for user lookups). Legacy apps can still bind to LDAPS on the SSO directly. See docs/architecture.md for the full diagram (ports, secrets flow, jump-host, ldap-client) and docs/secrets.md for the OpenBao model.
theta-agent, specialized subtype driver, Proxmox hypervisor fallback, unmanaged) for systemd, docker, proxmox, wireguard, database, and k8s resources.Resource -> Host -> Cluster -> Site), guaranteeing strict secret scoping across services and containers.You'll need three things set up ahead of time — a domain, DNS records, and port forwarding. The setup script can't do these for you; they're outside the host.
You need a real DNS domain (e.g. lab.example.com) because the proxy issues
real TLS certificates for it via Let's Encrypt. A .local or made-up name only
gets you a self-signed cert (browsers will warn — fine for testing, painful for
daily use).
The domain is the one value you set in master.env (e.g.
CFG_DOMAIN=lab.example.com) — see Quickstart. The SSO/proxy hostnames
default to sso.<domain> / proxy.<domain>, and the LDAP base DN
(dc=lab,dc=example,dc=com) is built from it automatically.
You need a DNS A record for each hostname you want the stack to serve,
all pointing at the public IP of the host running this stack (or your
router, if the host is behind it). At a minimum:
| Hostname (example) | What it serves |
|---|---|
sso.lab.example.com | The SSO Manager UI (login, user/group/OAuth-client admin). |
proxy.lab.example.com | The proxy's own management UI (add the apps you want to protect). |
Two is the minimum. You'll almost certainly want more: every app you put
behind the proxy needs its own hostname too (e.g. wiki.lab.example.com,
photos.lab.example.com). Add those DNS records as you add apps — create them
now if you already know what you'll front.
Quick local test, no DNS? You can skip real DNS by adding the hostnames to
/etc/hostson the machine you browse from, pointing at the stack host. The proxy will then fall back to a self-signed cert. Fine for kicking the tires, not for real use.
On your router / firewall, forward these from your public IP to the host running the stack:
| Port | Why |
|---|---|
| 80 (HTTP) | Required for Let's Encrypt to verify domain ownership (ACME HTTP-01 challenge). Without it, you get the self-signed fallback. |
| 443 (HTTPS) | The real traffic — all the UIs and proxied apps. |
Both must reach the host. Port 80 must be reachable from the internet even though you'll only use 443 — that's just how Let's Encrypt validates. (If you really can't open 80, the stack still runs on the self-signed cert; you'll just see browser warnings.)
Optional extra ports (only if you need them):
setup.sh → ldap-client) configures its sssd against
ldap://localhost:389 / ldaps://localhost:636, so both ports are published
to the host by default (bind 0.0.0.0; set LDAP_BIND/LDAPS_BIND=127.0.0.1 to
lock to the host). LAN clients (Linux hosts via PAM/SSSD, LDAP-native apps) can
bind over either; the proxy itself reaches LDAP over the internal Docker
network and doesn't need them.
Do not forward 389/636 to the public internet. If you need LAN clients to
bind LDAP, set CFG_LDAPS_HOST=ldap.internal.example.com (or sso-manager for
same-host Docker clients) in master.env and use an internal DNS record / cert
SAN. The default shows the public SSO hostname, which implies a public route.You must use the modern Docker Compose v2 plugin (docker compose). The older
v1 standalone (docker-compose) is not compatible with the BuildKit images
generated by this suite and will fail with a ContainerConfig KeyError during
deployment.
git clone --recursive https://github.com/theta42/theta-suite.git
cd theta-suite
cp master.env.example master.env # then edit master.env: set CFG_DOMAIN to your domain
./setup.sh # first run: generates ./config/ from master.env, builds + bootstraps + starts
Joining an existing site's directory as a spoke instead of seeding a fresh
one? Use spoke.env, not master.env — cp spoke.env.example spoke.env,
set CFG_MASTER_DIRECTORY_URL/CFG_MASTER_DIRECTORY_JOIN_KEY, then
./setup.sh. spoke.env is self-sufficient (no CFG_DOMAIN needed — it's
fetched from the master); setup.sh refuses to run if both master.env and
spoke.env exist, or neither does. See Multi-Site.
Your domain is entered once in master.env (e.g.
CFG_DOMAIN=lab.example.com) — the LDAP base DN (dc=lab,dc=example,dc=com)
is derived from it, however many labels it has. The
first ./setup.sh reads master.env and generates ./config/sso-secrets.js +
./config/proxy-secrets.js with that domain filled in everywhere (hostnames
default to sso.<domain> / proxy.<domain>) plus random secrets, then builds
and brings up the stack in the same run — no edit-and-re-run step.
master.env is used only on that first run; once ./config/*.js exist they are
operator-owned and master.env is ignored.
./setup.sh is idempotent — re-run it any time to converge the stack to
./config/. It:
./backups/<timestamp>/ before rebuilding (config + LDAP
bootstrap/bootstrap.js) inside the SSO container,
which:
cn=ldapclient,ou=people,<base>),app_sso_admin +
app_sso_oauth_admin groups,./config/proxy-secrets.js.<SSO_HOST> and <PROXY_HOST> as Host records in the proxy
(directly via its Host model, inside the proxy container) — the proxy
routes every hostname it serves off a Host record, including its own
management UI and the SSO's UI, so without this step those two URLs
would 404. Idempotent; skips a host that already exists../config/Secrets live in OpenBao (a Vault fork, container openbao:8200 on
theta-net), the single authoritative store. Each app loads them at boot with
@simpleworkjs/bao-conf, which
deep-merges secret/<app>/conf over the file-loaded config — fail-soft, so
if OpenBao is unreachable the app boots from the file fallback. End users get
personal per-user secret storage (secret/users/<uid>/*) in the SSO Vault
UI, and admins mint scoped tokens for external apps (secret/apps/<name>/*).
See docs/secrets.md for the full architecture, policies,
token model, rotation, and the external-app convention.
A bind-mounted ./config/ directory (gitignored) holds the operator-edit
seed files and the fail-soft fallback, read by each app's @simpleworkjs/conf
via the CONF_SECRETS env var:
./config/sso-secrets.js — SSO config: ldap (base, admin password,
user/group bases), oauth (issuer, jwtSecret), smtp, name, plus
orchestrator-only stack (hostnames, base DN), bootstrap (first admin),
and serviceAccountPass (the proxy's LDAP bind password)../config/proxy-secrets.js — proxy config: oidc (endpoints,
clientId/clientSecret — filled in by the bootstrap), ldap (bind creds,
same serviceAccountPass), auth (admin groups/users)../setup.sh generates both on first run from ./master.env (the one place the
domain is entered — see Quickstart) with random secrets, seeds them into
OpenBao, and mints scoped per-app tokens (SSO_VAULT_TOKEN /
PROXY_VAULT_TOKEN / JUMP_VAULT_TOKEN) into ./.env. There is no
.env / proxy.env for app config — edit ./config/*.js directly (and
re-seed into OpenBao, or use the SSO Configuration UI for live edits). Compose
only interpolates port defaults (SSO_PORT, HTTP_PORT, etc.), which you can
override on the command line: SSO_BIND=127.0.0.1 ./setup.sh. See
config.example/ for the full annotated shape, and each submodule's
secrets.js.example.
Migrating from an older
.env-based deployment? If.envand/orproxy.envexist when you first run./setup.sh, it migrates them into./config/preserving your existing secrets (LDAP admin pass, JWT, OAuth client creds, service pass) so your running deployment keeps its directory, tokens, and OAuth client. Afterwards.env/proxy.envare dead weight — delete them.
https://<SSO_HOST> — log in as your bootstrap admin to
add users, groups, and OAuth clients. (First-run fallback: http://<host>:3001,
reachable on the LAN by default.)https://<PROXY_HOST> — add the Host records you want to
protect with OIDC. (First-run fallback: http://<host>:3000, reachable on the
LAN by default.)ldaps://<host>:636 as cn=admin,<base> (admin) or
cn=ldapclient,ou=people,<base> (read-only service account the bootstrap
created). Use LDAPS, not plain LDAP.Both apps support self-service personal access tokens for calling their management APIs from scripts/CI/other services without an OIDC browser session. Each logged-in user mints their own tokens under API Tokens in the UI (the raw token is shown once); a token authenticates as its creator and carries their permissions. Revoke or rotate from the same page (immediate effect). Tokens persist in Redis (AOF) and survive rebuilds.
# SSO Manager — manage users/groups/OAuth clients from a script
curl -H "Authorization: Bearer sso_<id>_<secret>" https://<SSO_HOST>/api/user
# Proxy — manage Host records from a script
curl -H "Authorization: Bearer prx_<id>_<secret>" https://<PROXY_HOST>/api/host
Format is <prefix>_<id>_<secret>; the id is the lookup key, the secret is
bcrypt-hashed and never stored in plaintext. In the proxy, the creator's group
membership is snapshotted at mint time (revoke + re-mint to tighten after group
changes); the SSO re-resolves groups from LDAP live on each call. See each
submodule's DEPLOYMENT (sso-manager-node/DEPLOYMENT.md, proxy/DEPLOYMENT.md)
for details.
The stack runs under Docker Compose with several services — sso-manager,
proxy and openbao (plus its bao-renewer sidecar). Both the Node app and,
for the SSO, OpenLDAP write to the container's stdout/stderr, so
docker compose logs is the primary view.
The gateway is the exception: it runs on the host as the theta-gateway
systemd service, because it is a router and the LAN-facing half of that cannot
work inside a Docker network namespace. Its logs are in the journal
(journalctl -u theta-gateway). See
Where the gateway runs.
# Follow all services live
docker compose logs -f
# One service
docker compose logs -f sso-manager
docker compose logs -f proxy
# The gateway runs on the host, not in compose — its logs are in the journal
journalctl -u theta-gateway -f
# Last 200 lines and keep following
docker compose logs --tail=200 -f proxy
# Only the last 10 minutes
docker compose logs --since=10m sso-manager
The plain container names work too (docker logs -f sso-manager,
docker logs -f proxy) — handy if you started the stack without Compose.
OpenResty writes its access/error logs to files inside the container
(/var/log/nginx, on the proxy-logs volume), so they do not appear in
docker logs proxy. Tail them directly:
docker compose exec proxy tail -f /var/log/nginx/access.log
docker compose exec proxy tail -f /var/log/nginx/error.log
slapd runs with -d 0 and logs to stderr, so LDAP output is already in
docker compose logs sso-manager. For a targeted health check, exec into the
SSO and query the directory directly:
# Your base DN lives in ./config/sso-secrets.js (stack.ldapBaseDn).
docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,<base>" -W -b "<base>"
./setup.sh automatically snapshots state to ./backups/<timestamp>/ before
every rebuild and keeps the last BACKUP_KEEP (default 5; e.g.
BACKUP_KEEP=10 ./setup.sh). Each snapshot has config/ (your secrets),
ldap.ldif (the directory), and sso-manager.rdb + proxy.rdb (Redis). Back
the ./backups/ directory off the host — it holds secrets and the whole
user directory.
| State | Location | Persisted across rebuild? |
|---|---|---|
| LDAP directory (users, groups, policies) | ldap-data volume | yes (volume) |
| SSO Redis (OAuth clients, tokens) | sso-data volume | yes (AOF + RDB) |
| Proxy Redis (Host records, perms, DNS creds, LE certs) | proxy-data volume | yes (AOF + RDB) |
| Secrets (LDAP admin pass, JWT, OAuth client, service pass) | ./config/ | your responsibility — back up off-host |
# LDAP (while slapd is running)
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "$(docker compose exec -T sso-manager node -e 'console.log((require("/config/sso-secrets.js").stack||{}).ldapBaseDn)')" > ldap.ldif
# Redis — hot snapshot each service
docker compose exec sso-manager redis-cli BGSAVE
docker compose cp sso-manager:/data/dump.rdb sso-manager.rdb
docker compose exec proxy redis-cli BGSAVE
docker compose cp proxy:/data/dump.rdb proxy.rdb
# Secrets — ./config/ (the seed/fallback; OpenBao is authoritative)
cp -a ./config config-backup && chmod 700 config-backup
# OpenBao (the authoritative secret store — back up its data volume)
docker run --rm -v theta-suite_openbao-data:/data -v "$PWD":/backup alpine \
tar czf /backup/openbao-data.tgz -C /data .
Note on VM Sizing: The setup script builds the
sso-managerandproxyNode.js containers from source. Thenpm ciinstall step can be memory-intensive. If you are deploying to a VM with 1GB or less of RAM (such as a 512MB droplet), the kernel's OOM killer will likely freeze or kill the build. To prevent this, add a swap file before runningsetup.sh:fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab
# 1. Secrets
cp -a backups/<ts>/config ./config && chmod 700 ./config
./setup.sh # fresh empty volumes
docker compose stop sso-manager
# 2. LDAP — the SSO uses a static slapd.conf (-f, not cn=config -F), so use slapadd -f
docker compose run --rm --no-deps --entrypoint sh sso-manager -c \
'rm -f /var/lib/ldap/* && slapadd -f /etc/openldap/slapd.conf -l /dev/stdin' \
< backups/<ts>/ldap.ldif
docker compose start sso-manager
# 3. Redis — delete the AOF first (see note), then load the RDB
for svc in sso-manager proxy; do
docker compose stop "$svc"
docker compose run --rm --no-deps --entrypoint sh "$svc" -c \
'rm -f /data/appendonly.aof /data/appendonly.aof.*'
docker compose cp "backups/<ts>/${svc}.rdb" "${svc}:/data/dump.rdb"
docker compose start "$svc"
done
AOF vs RDB (important): with
--appendonly yes, Redis loadsappendonly.aofon startup and ignoresdump.rdbif the AOF exists. To restore from an RDB snapshot you must delete the AOF first (step 3 does this); Redis then loads the RDB and writes a fresh AOF. Verify after restoring:docker compose exec sso-manager redis-cli DBSIZE,docker compose exec proxy redis-cli DBSIZE,docker compose exec sso-manager ldapsearch -x -b "<base>".
Restore Redis only = step 3. Restore LDAP only = step 2.
git pull --ff-only
./setup.sh # snapshots, then rebuilds — volumes keep LDAP + Redis state
LDAP, both Redis stores, and the auto-ssl Let's Encrypt certs (in proxy Redis)
all survive the rebuild because they live on named volumes, not in the images.
Note: re-running bootstrap resets the bootstrap-admin and service-account
passwords to the ./config/ values; non-bootstrap OAuth clients live in SSO
Redis and are preserved by the volume.
bootstrap/bootstrap.js runs inside the SSO Manager container (bind-mounted
read-only from this repo) and is deliberately self-contained: it uses only Node
built-ins (child_process, crypto, fs) + global fetch. It reads its inputs
from the bind-mounted ./config/sso-secrets.js + proxy-secrets.js (not from
env). LDAP operations use the openldap-clients binaries
(ldapadd/ldapsearch/ldapmodify) with explicit admin creds from the config;
the OAuth client is created via the SSO's own HTTP API (logging in as the
bootstrapped admin, which also validates that admin's password end-to-end). It
does not require the SSO's internal models, so it never has to fight the
app's config layer.
It's idempotent: re-running converges to your ./config/ values. The LDAP
service account + admin passwords are reset to the config on each run; the OAuth
client is created if missing. If proxy-secrets.js already holds a
clientId+clientSecret matching an existing client, they are kept (the proxy
keeps working); otherwise a new client is created (or the secret rotated if the
client exists but the file has no usable secret) and the creds are written back
into proxy-secrets.js (the SSO mounts ./config read-write for this; the proxy
mounts it read-only).
Passwords are stored as {SSHA512} (the SSO's hashPasswordSSHA512, replicated
exactly in the bootstrap) so the SSO can verify them on bind.
3001) and the proxy mgmt UI (3000) default to 0.0.0.0 for first-run
convenience, so they're reachable on your LAN (they're login-protected, but
it widens the attack surface). The proxy fronts both under TLS in normal
use, so set SSO_BIND=127.0.0.1 and MGMT_BIND=127.0.0.1 to lock them to the
host once you're up and running (override on the command line, e.g.
SSO_BIND=127.0.0.1 MGMT_BIND=127.0.0.1 ./setup.sh). LDAPS (636) is the
only LDAP listener that should cross the network../config/. It holds the LDAP admin password, JWT secret, LDAP
service password, and OAuth client secret. setup.sh writes the files mode
0600 and the dir 0700; the directory is in .gitignore. Back it up
off-host (see Backups and restore).ldap.tlsOptions.rejectUnauthorized=false (in proxy-secrets.js). For strict
trust, this is a two-step change, not config-only: (a) edit
docker-compose.yml to also mount the ldap-certs volume into the proxy
service (it's currently only mounted into sso-manager) — see the
commented-out boilerplate in the proxy service's volumes: block — then
(b) set ldap.tlsOptions.ca=<path> in ./config/proxy-secrets.js to the
mounted cert path and docker compose up -d proxy to pick up the new mount.setup.sh resets the bootstrap admin + service passwords to
the ./config/ values. If you change a user's password in the SSO UI
later, re-running setup.sh will reset the bootstrap admin's password back
to bootstrap.adminPass.theta-suite/
├── master.env.example # fresh/standalone site — cp to master.env, set CFG_DOMAIN
├── spoke.env.example # OR: join an existing site — cp to spoke.env instead (mutually exclusive)
├── config.example/ # committed annotated config templates (copy to ./config/)
├── docker-compose.yml # sso-manager + proxy + openbao on one bridge net (gateway is host-installed)
├── setup.sh # one-command idempotent bring-up (manages ./config/ + backups)
├── bootstrap/
│ └── bootstrap.js # runs in the sso-manager container
├── sso-manager-node/ # git submodule
├── proxy/ # git submodule
├── jump-host/ # git submodule
├── ldap-client/ # git submodule (enrolls Linux hosts; also the opt-in ldap-test-host fixture)
└── theta-agent/ # git submodule
./setup.sh reads the gitignored master.env on first run to generate the
gitignored ./config/ (sso-secrets.js + proxy-secrets.js) and snapshots to
the gitignored ./backups/ before each rebuild.
./setup.sh updates both submodules to their latest vX.Y.Z release tag
before building — not the tip of master — so each run builds the newest
tagged release of each app, not whatever's most recently merged upstream. To
lock to the pinned commits (offline rebuild, or a deliberate pin), run
SKIP_SUBMODULE_UPDATE=1 ./setup.sh.
See CHANGELOG.md for what changed in each theta-suite release
(and each submodule's own CHANGELOG.md —
proxy,
sso-manager-node
— for what changed inside the apps themselves).
Theta Suite is part of the sovereign open-source ecosystem by Theta42.