# Where are EBS passwords actually stored?

Exact answer: AES-256-GCM encrypted in TuneVault's control-plane Postgres. Decrypted only in-memory at execution time. Never written to disk on any agent or DB node.

### 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_credentials` Postgres 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.

🖥️
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_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:

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.

- 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_credentials` vault 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: support@tunevault.app — we aim to respond promptly to security issues.