The short answer

When you store an APPS, SYSTEM, WebLogic, or SYSADMIN password in TuneVault:

๐Ÿ”’

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.

๐Ÿ–ฅ๏ธ
Your Browser
Types password
โ†’
HTTPS / TLS 1.3
โšก
TuneVault API
Encrypts in-memory
AES-256-GCM
โ†’
Ciphertext only
๐Ÿ—„๏ธ
Neon Postgres
ebs_credentials
(no plaintext ever)
โ†“ At execution time (ADOP, WLS bounce, CM restart) โ†“
๐Ÿ—„๏ธ
Neon Postgres
Ciphertext fetched
โ†’
Ciphertext only
๐Ÿ”‘
TuneVault API
Decrypts in-memory
key from Render env
โ†’
Plaintext in memory
for command duration
๐Ÿ–ฅ๏ธ
Agent / sqlplus
Command runs
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_idUUIDWhich Oracle connection's credential was accessed
credential_typeTEXTapps | system | weblogic_admin | etc.
actionTEXTOperation that needed the credential (e.g. adop_run, wls_bounce, cm_bounce)
user_idINTEGERTuneVault user who triggered the operation
accessed_atTIMESTAMPTZTimestamp 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:

  1. Generate a new key
    Run 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.
  2. Re-encrypt all rows
    Decrypt every row with the old key and re-encrypt with the new key. Update the rotated_at column. This can be done in a maintenance window with a one-off script.
  3. Update Render secret
    Set the new value of ENCRYPTION_KEY in 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.
  4. Verify decryption
    Run a POST to /api/connections/:id/credentials to re-store one credential and confirm it round-trips correctly after the restart.
  5. Revoke old key
    Remove 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.

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.