Secrets Hardening
Secrets Hardening Guide
Section titled “Secrets Hardening Guide”TapPass manages several categories of secrets. This guide covers how they’re protected by default and how to harden them for regulated or high-security environments.
Secret Inventory
Section titled “Secret Inventory”| Secret | Where used | Default storage | Risk if leaked |
|---|---|---|---|
TAPPASS_ADMIN_API_KEY | Admin API access | .env.prod | Full admin control |
TAPPASS_JWT_SECRET | Session token signing | .env.prod | Session forgery |
TAPPASS_VAULT_KEY | Credential encryption at rest | .env.prod | Decrypt all stored OAuth tokens |
POSTGRES_PASSWORD | Database authentication | .env.prod | Full database access |
TAPPASS_LICENSE | License activation | .env.prod | License theft (signature-protected) |
OPENAI_API_KEY / ANTHROPIC_API_KEY | LLM proxy | .env.prod | Billing abuse |
TAPPASS_SSO_CLIENT_SECRET | SSO authentication | .env.prod | SSO impersonation |
TUNNEL_TOKEN | Cloudflare Tunnel | .env.prod | Traffic interception |
| ES256 signing key | Capability token signing | /app/keys/tappass-token.pem (Docker volume) | Token forgery |
Default Protections
Section titled “Default Protections”The install wizard and deploy scripts enforce these automatically:
.env.prodfile permissions:600(owner read/write only).deploy.shauto-fixes if too open.- Credentials backup:
install.shsaves a one-time backup withchmod 600and prompts you to move it to a password manager and delete the file. - Secret generation: All secrets are generated with
secrets.token_urlsafe()(cryptographic randomness). - License signature: Ed25519 — cannot be forged without the private key.
- ES256 key: Auto-generated inside the container on first boot. Never leaves the Docker volume.
- No secrets in git:
.env.prodand all secret files are in.gitignore. - No secrets in logs: Structured logging truncates errors and never logs secret values.
Threat Model
Section titled “Threat Model”| Threat | Default mitigation | Hardened mitigation |
|---|---|---|
| Filesystem read (attacker on host) | File permissions 600 | Encrypt .env.prod with SOPS or use Docker secrets |
| Docker inspect (attacker with Docker socket) | Docker group restricted | Use Docker secrets (not env vars) |
Process listing (/proc/*/environ) | Linux restricts to same UID | Use Docker secrets (mounted as files, not env) |
| Container escape | Non-root user inside container | Enable SPIRE for mTLS + rotate secrets regularly |
| Backup exposure | No automated secret backups | Encrypt backups with GPG/age before transfer |
| Log leakage | Secrets not logged | Ship logs to SIEM with redaction rules |
Level 1: File Permission Hardening (Default)
Section titled “Level 1: File Permission Hardening (Default)”This is what install.sh does automatically:
chmod 600 .env.prod # owner-onlychmod 600 .credentials-*.txt # one-time backupchmod 400 ~/.cloudflared/*.json # tunnel credentialsVerify:
stat -c '%a %n' .env.prod # should be 600find ~ -name '.env*' -perm /044 # should return nothingLevel 2: Docker Secrets (Recommended for Production)
Section titled “Level 2: Docker Secrets (Recommended for Production)”Docker secrets are mounted as files inside the container, never exposed via docker inspect or /proc/*/environ.
Step 1: Create secrets
Section titled “Step 1: Create secrets”# From .env.prod valuesecho "your-admin-key" | docker secret create tappass_admin_key -echo "your-jwt-secret" | docker secret create tappass_jwt_secret -echo "your-vault-key" | docker secret create tappass_vault_key -echo "your-db-password" | docker secret create tappass_pg_password -echo "your-license-key" | docker secret create tappass_license -Step 2: Reference in compose
Section titled “Step 2: Reference in compose”services: tappass: secrets: - tappass_admin_key - tappass_jwt_secret - tappass_vault_key - tappass_pg_password - tappass_license environment: - TAPPASS_ADMIN_API_KEY_FILE=/run/secrets/tappass_admin_key - TAPPASS_JWT_SECRET_FILE=/run/secrets/tappass_jwt_secret - TAPPASS_VAULT_KEY_FILE=/run/secrets/tappass_vault_key - TAPPASS_LICENSE_FILE=/run/secrets/tappass_license
secrets: tappass_admin_key: external: true tappass_jwt_secret: external: true tappass_vault_key: external: true tappass_pg_password: external: true tappass_license: external: trueNote: TapPass reads
*_FILEenvironment variables automatically. IfTAPPASS_ADMIN_API_KEY_FILEis set, the value is read from that file path instead ofTAPPASS_ADMIN_API_KEY.
What this solves
Section titled “What this solves”docker inspectno longer shows secrets/proc/*/environno longer contains secrets- Secrets are tmpfs-mounted (never written to disk inside the container)
Level 3: Encrypted Env Files (SOPS)
Section titled “Level 3: Encrypted Env Files (SOPS)”For teams that version-control their deployment config, use SOPS to encrypt .env.prod at rest:
# Encrypt (requires age or GPG key)sops --encrypt --age age1... .env.prod > .env.prod.enc
# Decrypt at deploy timesops --decrypt .env.prod.enc > .env.prodchmod 600 .env.prod./deploy.shrm .env.prod # remove plaintext after deployThis way .env.prod.enc can be committed to git safely — only holders of the age/GPG key can decrypt.
Level 4: External Secret Stores
Section titled “Level 4: External Secret Stores”For enterprise deployments, integrate with an external secret store:
| Store | Integration |
|---|---|
| HashiCorp Vault | Use Vault Agent sidecar to render .env.prod from Vault KV |
| AWS Secrets Manager | Use aws secretsmanager get-secret-value in a wrapper script |
| Azure Key Vault | Use managed identity + az keyvault secret show |
| 1Password CLI | Use op run to inject secrets as env vars |
Example with 1Password:
# Store secrets in 1Password vault "TapPass Production"op run --env-file=.env.prod.tpl -- ./deploy.shWhere .env.prod.tpl references 1Password items:
TAPPASS_ADMIN_API_KEY=op://TapPass Production/admin-key/credentialTAPPASS_JWT_SECRET=op://TapPass Production/jwt-secret/credentialSecret Rotation
Section titled “Secret Rotation”| Secret | Rotation procedure | Downtime |
|---|---|---|
TAPPASS_ADMIN_API_KEY | Update .env.prod, restart TapPass | Brief (restart) |
TAPPASS_JWT_SECRET | Update + restart — all existing sessions invalidated | Brief (users re-login) |
TAPPASS_VAULT_KEY | Cannot rotate without re-encrypting vault. Contact support. | None if planned |
POSTGRES_PASSWORD | Update .env.prod + ALTER USER in PostgreSQL, restart | Brief |
| LLM API keys | Update .env.prod, restart | Brief |
| ES256 signing key | Delete /app/keys/tappass-token.pem volume, restart (auto-regenerates) — all issued capability tokens invalidated | Brief |
TUNNEL_TOKEN | Regenerate in Cloudflare dashboard, update .env.prod, restart tunnel | Brief |
Audit Checklist
Section titled “Audit Checklist”Run this on your production server periodically:
# 1. No secret files readable by othersfind ~ -name '.env*' -perm /044 -ls
# 2. No credentials backup files lingeringfind ~ -name '.credentials-*' -ls
# 3. Docker socket access restrictedstat -c '%G' /var/run/docker.sock # should be 'docker', not 'root'getent group docker # list members — should be minimal
# 4. No secrets in shell historygrep -i 'api_key\|secret\|password\|token' ~/.bash_history ~/.zsh_history 2>/dev/null
# 5. No secrets in gitgit log --all -p -- '*.env*' 2>/dev/null | head -1 # should be empty