Skip to content

Secrets Hardening

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.

SecretWhere usedDefault storageRisk if leaked
TAPPASS_ADMIN_API_KEYAdmin API access.env.prodFull admin control
TAPPASS_JWT_SECRETSession token signing.env.prodSession forgery
TAPPASS_VAULT_KEYCredential encryption at rest.env.prodDecrypt all stored OAuth tokens
POSTGRES_PASSWORDDatabase authentication.env.prodFull database access
TAPPASS_LICENSELicense activation.env.prodLicense theft (signature-protected)
OPENAI_API_KEY / ANTHROPIC_API_KEYLLM proxy.env.prodBilling abuse
TAPPASS_SSO_CLIENT_SECRETSSO authentication.env.prodSSO impersonation
TUNNEL_TOKENCloudflare Tunnel.env.prodTraffic interception
ES256 signing keyCapability token signing/app/keys/tappass-token.pem (Docker volume)Token forgery

The install wizard and deploy scripts enforce these automatically:

  • .env.prod file permissions: 600 (owner read/write only). deploy.sh auto-fixes if too open.
  • Credentials backup: install.sh saves a one-time backup with chmod 600 and 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.prod and all secret files are in .gitignore.
  • No secrets in logs: Structured logging truncates errors and never logs secret values.
ThreatDefault mitigationHardened mitigation
Filesystem read (attacker on host)File permissions 600Encrypt .env.prod with SOPS or use Docker secrets
Docker inspect (attacker with Docker socket)Docker group restrictedUse Docker secrets (not env vars)
Process listing (/proc/*/environ)Linux restricts to same UIDUse Docker secrets (mounted as files, not env)
Container escapeNon-root user inside containerEnable SPIRE for mTLS + rotate secrets regularly
Backup exposureNo automated secret backupsEncrypt backups with GPG/age before transfer
Log leakageSecrets not loggedShip 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:

Terminal window
chmod 600 .env.prod # owner-only
chmod 600 .credentials-*.txt # one-time backup
chmod 400 ~/.cloudflared/*.json # tunnel credentials

Verify:

Terminal window
stat -c '%a %n' .env.prod # should be 600
find ~ -name '.env*' -perm /044 # should return nothing
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.

Terminal window
# From .env.prod values
echo "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 -
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: true

Note: TapPass reads *_FILE environment variables automatically. If TAPPASS_ADMIN_API_KEY_FILE is set, the value is read from that file path instead of TAPPASS_ADMIN_API_KEY.

  • docker inspect no longer shows secrets
  • /proc/*/environ no longer contains secrets
  • Secrets are tmpfs-mounted (never written to disk inside the container)

For teams that version-control their deployment config, use SOPS to encrypt .env.prod at rest:

Terminal window
# Encrypt (requires age or GPG key)
sops --encrypt --age age1... .env.prod > .env.prod.enc
# Decrypt at deploy time
sops --decrypt .env.prod.enc > .env.prod
chmod 600 .env.prod
./deploy.sh
rm .env.prod # remove plaintext after deploy

This way .env.prod.enc can be committed to git safely — only holders of the age/GPG key can decrypt.

For enterprise deployments, integrate with an external secret store:

StoreIntegration
HashiCorp VaultUse Vault Agent sidecar to render .env.prod from Vault KV
AWS Secrets ManagerUse aws secretsmanager get-secret-value in a wrapper script
Azure Key VaultUse managed identity + az keyvault secret show
1Password CLIUse op run to inject secrets as env vars

Example with 1Password:

Terminal window
# Store secrets in 1Password vault "TapPass Production"
op run --env-file=.env.prod.tpl -- ./deploy.sh

Where .env.prod.tpl references 1Password items:

TAPPASS_ADMIN_API_KEY=op://TapPass Production/admin-key/credential
TAPPASS_JWT_SECRET=op://TapPass Production/jwt-secret/credential
SecretRotation procedureDowntime
TAPPASS_ADMIN_API_KEYUpdate .env.prod, restart TapPassBrief (restart)
TAPPASS_JWT_SECRETUpdate + restart — all existing sessions invalidatedBrief (users re-login)
TAPPASS_VAULT_KEYCannot rotate without re-encrypting vault. Contact support.None if planned
POSTGRES_PASSWORDUpdate .env.prod + ALTER USER in PostgreSQL, restartBrief
LLM API keysUpdate .env.prod, restartBrief
ES256 signing keyDelete /app/keys/tappass-token.pem volume, restart (auto-regenerates) — all issued capability tokens invalidatedBrief
TUNNEL_TOKENRegenerate in Cloudflare dashboard, update .env.prod, restart tunnelBrief

Run this on your production server periodically:

Terminal window
# 1. No secret files readable by others
find ~ -name '.env*' -perm /044 -ls
# 2. No credentials backup files lingering
find ~ -name '.credentials-*' -ls
# 3. Docker socket access restricted
stat -c '%G' /var/run/docker.sock # should be 'docker', not 'root'
getent group docker # list members — should be minimal
# 4. No secrets in shell history
grep -i 'api_key\|secret\|password\|token' ~/.bash_history ~/.zsh_history 2>/dev/null
# 5. No secrets in git
git log --all -p -- '*.env*' 2>/dev/null | head -1 # should be empty