The short answer
When you store an APPS, SYSTEM, WebLogic, or SYSADMIN password in TuneVault:
- It is encrypted client-side over TLS and arrives at the API already in transit-encrypted HTTPS.
- The server immediately encrypts the value with AES-256-GCM using a key from Render's secret manager.
- Only the ciphertext, IV, and auth tag are written to the
ebs_credentialsPostgres table. The plaintext is discarded from memory. - When an operation needs the password (e.g. ADOP phase run, WLS bounce), TuneVault decrypts it in-memory for that operation only, then discards it.
- The decryption key lives in Render's secret manager โ not in any code repo, not in any backup, not in any log file.
Passwords are NEVER written to disk on the agent, DB node, or apps node. They live AES-256-GCM encrypted in the TuneVault control-plane Postgres. The encryption key is held in Render's secret manager, not in any code repo, not in any backup.
Credential Types โ Where Each Is Stored
| Credential Type | Username | Where Stored | Encryption | Who Can Decrypt |
|---|---|---|---|---|
apps |
APPS | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
system |
SYSTEM | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
sys |
SYS | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
weblogic_admin |
weblogic (or custom) | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
sysadmin_user |
SYSADMIN | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
xml_gateway |
custom | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at execution time only) |
| Oracle DB connect (host/port) | tunevault_reader | oracle_connections (Postgres) | AES-256-GCM | TuneVault server (in-memory, at health check time) |
ssh_private_key |
oracle / applmgr (OS user) | ebs_credentials (Postgres) | AES-256-GCM | TuneVault server (in-memory, at SSH execution time only) |
| SSH keys (ssh_targets / oracle_connections) | os user | ssh_targets / oracle_connections (Postgres) | AES-256-GCM | TuneVault server (in-memory, at SSH execution time) |
Encryption Architecture
Every credential follows the same path from your browser to the database and back out at execution time. The plaintext password exists in memory only at two moments: when you type it into the UI, and when a command needs it.
AES-256-GCM
(no plaintext ever)
key from Render env
for command duration
via HTTPS proxy
Plaintext is discarded after the command completes. It is never written to disk, logged, or returned in any API response.
Encryption Specification
Algorithm
AES-256-GCM โ authenticated encryption with associated data (AEAD).
GCM mode provides both confidentiality and integrity: if the ciphertext or IV is tampered with,
decryption fails with an AuthTagMismatch error rather than silently returning garbage.
Key
256-bit (32-byte) key stored as a 64-hex-character string in the ENCRYPTION_KEY
environment variable in Render's secret manager. The application crashes at startup if this variable
is unset in production โ there is no fallback to a hardcoded or derived key.
Per-row randomness
Each encrypt call generates a fresh 128-bit random IV via crypto.randomBytes(16).
The IV, auth tag, and ciphertext are stored as separate columns.
Re-encrypting the same plaintext produces a different ciphertext every time โ no two rows are alike,
so ciphertext comparison reveals nothing about plaintext equality.
-- Schema: ebs_credentials id UUID PRIMARY KEY connection_id UUID NOT NULL โ FK to oracle_connections credential_type TEXT โ apps | system | sys | weblogic_admin | sysadmin_user | xml_gateway username TEXT โ stored in plaintext (not sensitive) encrypted_value TEXT โ AES-256-GCM ciphertext (hex) iv TEXT โ 128-bit random IV per row (hex) auth_tag TEXT โ 128-bit GCM auth tag (hex) rotated_at TIMESTAMPTZ โ last upsert timestamp created_at TIMESTAMPTZ โ row creation
What Is NOT Stored
These are never written anywhere by TuneVault:
Plaintext passwords ยท Passwords in log files ยท Passwords in health check results ยท Passwords in PDF/XLSX exports ยท Passwords in API responses ยท Passwords on the proxy agent disk ยท Passwords in environment variables on the Oracle server
Audit Trail
Every decryption event is logged to the credential_access_log table
before the plaintext is used. The log entry is written even if the command that
follows fails โ so the access is always recorded.
| Column | Type | Description |
|---|---|---|
connection_id | UUID | Which Oracle connection's credential was accessed |
credential_type | TEXT | apps | system | weblogic_admin | etc. |
action | TEXT | Operation that needed the credential (e.g. adop_run, wls_bounce, cm_bounce) |
user_id | INTEGER | TuneVault user who triggered the operation |
accessed_at | TIMESTAMPTZ | Timestamp of the decryption event |
Key Rotation Procedure
When the ENCRYPTION_KEY needs to be rotated (e.g., suspected compromise,
periodic compliance requirement), follow this procedure:
-
Generate a new keyRun
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"to produce a new 64-hex key. Store it securely โ do not commit to any repo. -
Re-encrypt all rowsDecrypt every row with the old key and re-encrypt with the new key. Update the
rotated_atcolumn. This can be done in a maintenance window with a one-off script. -
Update Render secretSet the new value of
ENCRYPTION_KEYin Render's environment. The service will restart and pick up the new key. Old ciphertexts are invalid after this point โ ensure re-encryption is complete first. -
Verify decryptionRun a POST to
/api/connections/:id/credentialsto re-store one credential and confirm it round-trips correctly after the restart. -
Revoke old keyRemove the old key from any intermediary storage. The old key no longer decrypts any live rows.
API Reference
All endpoints require a valid TuneVault session (cookie or Bearer token) and connection ownership.
| Method | Path | Description | Returns |
|---|---|---|---|
| POST | /api/connections/:id/credentials |
Store or rotate a credential | metadata only (no plaintext) |
| GET | /api/connections/:id/credentials |
List stored credential types | type, username, rotated_at |
| DELETE | /api/connections/:id/credentials/:type |
Revoke a credential | { deleted: true } |
| GET | /api/connections/:id/credentials/log |
Audit log (admin only) | access log entries |
-- Example: store an APPS password
POST /api/connections/abc123/credentials
Content-Type: application/json
{
"credential_type": "apps",
"username": "APPS",
"value": "your_password_here"
}
-- Response (no plaintext ever returned)
{
"credential_type": "apps",
"username": "APPS",
"rotated_at": "2026-05-18T10:45:00.000Z",
"created_at": "2026-05-18T10:45:00.000Z"
}
SSH Authentication Model
Q: Do I need to share the Oracle OS user's password?
No. A private key (or ssh-agent forwarding) for the oracle / applmgr OS account is sufficient โ and is how 90% of Oracle shops already operate.
- TuneVault never requires, stores, or transmits the Oracle OS user password.
- The private key you paste is encrypted AES-256-GCM before it leaves your browser and is stored in the same
ebs_credentialsvault as your DB passwords. - For maximum security, use ssh-agent forwarding โ the key never leaves your bastion at all.
Supported auth methods (Add Connection wizard)
| Method | How it works | Key stored? | Recommended for |
|---|---|---|---|
| Private key Recommended | Paste an OpenSSH/PEM private key (ed25519, RSA, ECDSA). Optional passphrase. Validated client-side before send. | Yes โ AES-256-GCM in oracle_connections.ssh_db_key_enc |
Most shops. Generate with ssh-keygen -t ed25519 then ssh-copy-id oracle@db-host. |
| ssh-agent forwarding | TuneVault reads SSH_AUTH_SOCK from its server process environment. This requires the TuneVault API process itself to be launched via an SSH session with agent forwarding โ an advanced configuration. No key material is stored in the database. |
No | Advanced: only when TuneVault server process is launched with agent forwarding active. |
| Password | OS user password encrypted at rest. Accepted as fallback only. | Yes โ AES-256-GCM in oracle_connections.ssh_db_key_enc |
Lab / dev environments only. Prefer key-based in production. |
Test SSH button โ pre-save, no persistence
The Test SSH connection button in the Add Connection wizard fires POST /api/connections/test-ssh.
The server opens a transient SSH session, runs a fixed allowlisted command (whoami && hostname && echo $ORACLE_SID && id),
and returns the four output lines. The private key is transmitted over TLS, used in-memory, and
never written to the database during the test โ only after the user clicks Save.
SSH private key in ebs_credentials vault
The SSH private key is stored as credential_type = 'ssh_private_key' in the ebs_credentials table,
using the same AES-256-GCM encryption and IV-per-row scheme as APPS / SYSTEM / WebLogic passwords.
Every in-memory decryption event is appended to credential_access_log before the key is used.
The plaintext key is never returned via any API endpoint.
Threat Model
| Threat | Mitigation | Status |
|---|---|---|
| Postgres database breach | All values are AES-256-GCM encrypted. Without ENCRYPTION_KEY, ciphertext is useless. |
Mitigated |
| Database backup exfiltration | Backups contain only ciphertext. Key is never stored in the database or backup. | Mitigated |
| Log file leakage | Plaintext is never written to any log. Log entries contain only credential_type and action. | Mitigated |
| API response leakage | GET endpoint returns metadata only. The decrypt path is internal-only, never HTTP-accessible. | Mitigated |
| Unauthorized decryption | Decryption only occurs inside whitelisted command execution code paths. Every event is logged to credential_access_log. | Mitigated |
| Key compromise | Key rotation procedure above. Old key can be revoked after re-encryption without service interruption. | Procedural |
| Render platform breach | Out of scope for TuneVault controls. Render provides SOC 2 Type II. Credentials should be rotated if Render is compromised. | Shared responsibility |
Related security pages: Security overview ยท Command whitelist ยท Privilege model
Security contact: [email protected] โ we aim to respond promptly to security issues.