Security
2FA, encrypted macaroons, password handling, throttling, and master-password override.
ThunderHub layers a few safety nets on top of node credentials. Pick the ones that match your threat model.
Two-factor authentication
ThunderHub supports TOTP-based 2FA per YAML account. Enable it from the user menu — the UI shows a QR code, you scan it into an authenticator app, and ThunderHub writes the secret back to the YAML file via the twofaSecret field.
After 2FA is on, the login screen asks for the token alongside the password.
Disable 2FA globally
DISABLE_TWOFA=trueSetting this skips the TOTP verification at login even if accounts have a twofaSecret. Useful as an emergency override when an operator loses their authenticator app — and a clear signal that you should reset the secret afterwards.
DISABLE_TWOFA=true is an anyone can log in with just the password switch.
Don't leave it on once you've recovered access.
Database users don't currently use TOTP — DB authentication is email + Argon2 password.
Password hashing
- YAML accounts — bcryptjs hashes both
masterPasswordand per-accountpassword. On first boot, ThunderHub rewrites the YAML in place with the hashed values (prefixed withthunderhub-). The cleartext is gone after that. - Database users — passwords go through
@node-rs/argon2(hash()). Stored aspassword_hashon theuserstable.
Master-password override
For deployments where you'd rather not put a password in the YAML at all:
MASTER_PASSWORD_OVERRIDE='secret-master-password'ThunderHub hashes the override at boot and uses it for every YAML account that doesn't have its own password. The override beats the YAML's own masterPassword.
Treat this like any other secret — Docker secrets, Fly secrets, etc. The benefit is that the YAML file can stay password-free.
Encrypted macaroons
If you don't want plaintext macaroons in the YAML, encrypt each one with CryptoJS AES and mark the account encrypted: true. ThunderHub decrypts the macaroon in memory the first time you log in (using the account password as the passphrase) — nothing decrypted ever hits disk.
accounts:
- name: 'Encrypted'
serverUrl: 'url:port'
macaroon: 'U2FsdGVkX19...' # output from CryptoJS.AES.encrypt(...)
encrypted: trueFull walkthrough in YAML accounts → Encrypted macaroon.
Encrypted macaroons only work in production (NODE_ENV=production). The dev
server clears decrypted state on every restart, which breaks the in-memory
flow.
At-rest encryption for DB nodes
When the database is enabled, node macaroons and certs added through the UI are encrypted with AES-256-GCM using DB_ENCRYPTION_KEY. The IV and auth tag are stored alongside the ciphertext; rotating the key requires re-encrypting each record.
DB_ENCRYPTION_KEY='<64-char hex>'Throttling
NestJS Throttler guards the unauthenticated login mutations:
THROTTLE_TTL=10 # window in seconds
THROTTLE_LIMIT=10 # requests per window per IPEach public auth mutation has its own decorator-level limit on top of the global default (e.g. get_session_token allows 4 attempts per 10s). Increase the global limit if your environment NATs many users behind one IP.
Cookie and session
- Session cookie is
httpOnly,sameSite: true, and markedsecurewhenUSE_HTTPS=true. - JWTs are signed with a per-process random secret in production (regenerated on every restart, which invalidates outstanding sessions).
- Sessions expire after 24 hours.
Logout
Hitting Log out clears the JWT cookie. For SSO setups, set LOGOUT_URL so users are redirected to the platform's own logout flow afterward.
Recommended baseline
A reasonable production setup looks like:
NODE_ENV=production
USE_HTTPS=true
DB_TYPE=sqlite
DB_SQLITE_PATH=/data/thunderhub.db
DB_ENCRYPTION_KEY=<64-char hex>
DISABLE_TWOFA=false
THROTTLE_LIMIT=10
THROTTLE_TTL=10Plus 2FA enabled per YAML account if you still use them.