| Version | Supported |
|---|---|
main branch (latest) |
✅ Active |
| Older tagged releases |
We recommend always running the latest version.
Please do NOT open a public GitHub issue for security vulnerabilities.
To report a vulnerability, use one of:
- GitHub Security Advisories (preferred): Open a private advisory
- Email: Contact the maintainer via their GitHub profile
- A clear description of the vulnerability
- Steps to reproduce (proof of concept if possible)
- The potential impact / attack scenario
- Suggested fix (optional)
- Acknowledgement within 72 hours
- We will work with you to understand and reproduce the issue
- A fix will be prioritised based on severity
- You will be credited in the security advisory (unless you prefer to remain anonymous)
All server credentials (host, username, password, privateKey, passphrase, notes) are encrypted with AES-256-GCM before being written to the database:
plaintext credential
│
▼
AES-256-GCM encrypt (random 96-bit IV)
key = SHA-256(ENCRYPTION_KEY env var)
│
▼
ciphertext:IV stored in PostgreSQL
Decryption happens only in memory, during an active request, and the plaintext is never logged or persisted.
Users can optionally enable a master key for an additional layer of protection:
ENCRYPTION_KEY (server-side AES layer)
+
user's master password
│
▼
PBKDF2 key derivation (600,000 iterations, SHA-256)
│
▼
AES-256-GCM re-encryption of each credential
With a master key enabled, credentials cannot be decrypted even if the database and ENCRYPTION_KEY are both compromised — the user's master password is required.
User passwords are hashed with Argon2id using parameters tuned for server-side security (memory cost, time cost, and parallelism are configurable).
When a terminal session is opened, the web app issues a short-lived JWE token (A256GCM, 5-minute TTL) containing the decrypted credentials. This token is sent to the browser and used to authenticate the WebSocket connection to the gateway. The gateway validates the token and discards it immediately — credentials live in memory only for the duration of the connection.
| Threat | Mitigation |
|---|---|
| Database exfiltration | AES-256-GCM encryption at rest; optional master key |
| Credential theft in transit | TLS (configured via reverse proxy) |
| SSRF attacks | validateHost() blocks private/loopback ranges |
| Brute-force login | Rate limiting on auth endpoints |
| Session hijacking | HttpOnly, SameSite=Strict cookies; session revocation |
| XSS | CSP with nonces; React's built-in escaping |
| Clickjacking | X-Frame-Options: DENY |
| CSRF | SameSite cookies; CORS origin validation |
- Physical access to the host running Termi
- Compromise of the host OS or container runtime
- Weak secrets — you are responsible for generating strong
SESSION_SECRET,ENCRYPTION_KEY, andGATEWAY_JWT_SECRETvalues - Upstream dependencies — we rely on well-maintained libraries (ssh2, jose, argon2, etc.) but cannot guarantee their security
Before exposing Termi to the internet, ensure:
- All secrets (
SESSION_SECRET,ENCRYPTION_KEY,GATEWAY_JWT_SECRET) are ≥32 random bytes - HTTPS is enforced via a reverse proxy (Traefik, Nginx, Caddy)
-
ALLOW_PRIVATE_NETWORKS=falseunless you explicitly need it -
ALLOW_LOCAL_TERMINALis not set unless you need it and understand the implications - The database port is not exposed to the internet
- Docker socket (if used) is not accessible from the containers
- You have enabled 2FA on your Termi account
- You are running a recent version
We are committed to working with security researchers to verify, reproduce, and address any potential vulnerabilities. We will not take legal action against researchers who follow responsible disclosure practices.