How OpenShip Implements Credential Encryption for SSH Keys and Secrets
OpenShip encrypts SSH keys and secrets at rest with AES-256-GCM, wrapping each credential in a versioned enc1: envelope and deriving the encryption key from the BETTER_AUTH_SECRET environment variable.
The oblien/openship repository stores sensitive automation credentials—including SSH passwords and private keys—using an application-level encryption stack. OpenShip credential encryption for SSH keys and secrets relies on deterministic AES-256-GCM helpers and a versioned envelope format, ensuring that passwords and private-key material remain protected inside the database and are only decrypted transiently in memory during active connections.
Core AES-256-GCM Primitives in apps/api/src/lib/encryption.ts
The foundation of the system sits in apps/api/src/lib/encryption.ts, which exports low-level encrypt() and decrypt() helpers powered by Node.js crypto. A deterministic 256-bit key is derived from the instance-wide BETTER_AUTH_SECRET via SHA-256, eliminating per-field secret management and simplifying host-level deployment security.
The encrypt() function generates a random IV for each operation, then returns a Base64 string encoding the IV, authentication tag, and ciphertext in sequence. Because AES-256-GCM provides authenticated encryption, any tampering with the payload causes decrypt() to throw an exception.
// low-level encryption API (used internally)
import { encrypt, decrypt } from "./lib/encryption";
const sealed = encrypt("my secret");
// sealed → "base64(iv||authTag||ciphertext)"
const plain = decrypt(sealed); // returns "my secret"
Versioned Credential Envelope in apps/api/src/lib/credential-encryption.ts
To distinguish encrypted secrets from legacy plaintext, apps/api/src/lib/credential-encryption.ts wraps sensitive fields with a versioned "enc1:" prefix. The encryptSecretField() helper applies this prefix to the encrypted payload, while decryptSecretField() detects it, strips it away, and invokes the core decrypt() routine.
This design makes the system backward compatible: values without the prefix pass through unchanged, but any newly written SSH password, key passphrase, or private key material automatically receives the enc1: envelope.
// encrypt a new SSH password before storing it
import { encryptSecretField } from "./lib/credential-encryption";
const newPassword = "s3cr3tPass!";
const storedValue = encryptSecretField(newPassword);
// storedValue → "enc1:<base64-ciphertext>"
Database Storage for Encrypted SSH Credentials
The servers table definition in packages/db/src/schema/servers.ts stores encrypted material as plain text columns named sshPassword, sshKeyPassphrase, and sshPrivateKey. The schema itself does not perform cryptographic operations; instead, the API layer writes pre-encrypted strings into these columns.
An isLocal flag on the server record indicates a local OpenShip instance that bypasses SSH entirely. For these records, the SSH fields exist as placeholders and are not used for remote authentication.
Runtime Decryption and Memory Handling
When the dashboard or background workers need to open an SSH connection, they retrieve the stored row and pass the column value through decryptSecretField(). The resulting plaintext PEM string or password is fed directly into the ssh2 client and exists only in memory for the duration of the session.
OpenShip never writes decrypted secrets to logs or persists them outside the encrypted database columns, ensuring the plaintext exposure window is minimized to the active connection lifetime.
// decrypt a stored SSH private key for use with ssh2
import { decryptSecretField } from "./lib/credential-encryption";
const storedKey = serverRow.sshPrivateKey; // e.g. "enc1:ABCD..."
const privateKey = decryptSecretField(storedKey);
// privateKey now contains the raw PEM string for the SSH session
Security Guarantees and Key Derivation
According to the SECURITY_GUIDE.md in the oblien/openship repository, the encryption strategy delivers three concrete guarantees. Encryption-at-rest is enforced by the mandatory enc1: envelope for all new secrets, while authenticated encryption via AES-256-GCM protects both confidentiality and integrity.
The deterministic key derivation strategy binds the encryption key to the host environment through BETTER_AUTH_SECRET. This eliminates extra key-management infrastructure, provided the environment variable remains secret and inaccessible to unauthorized processes.
Summary
- OpenShip derives a deterministic AES-256-GCM key from
BETTER_AUTH_SECRETinsideapps/api/src/lib/encryption.ts. - The
enc1:versioned envelope inapps/api/src/lib/credential-encryption.tsmarks encrypted SSH secrets and maintains backward compatibility with legacy plaintext. - Database columns
sshPassword,sshKeyPassphrase, andsshPrivateKeyinpackages/db/src/schema/servers.tsstore ciphertext at rest. - Plaintext is only held in memory during active SSH sessions and is never logged to disk.
Frequently Asked Questions
What encryption algorithm does OpenShip use for SSH credentials?
OpenShip uses AES-256-GCM as implemented in apps/api/src/lib/encryption.ts. This provides authenticated symmetric encryption, ensuring both confidentiality and integrity for every secret stored with the enc1: prefix.
How is the encryption key generated and managed?
The application derives a single deterministic 256-bit key from the instance-wide BETTER_AUTH_SECRET environment variable using SHA-256. This design avoids external key-management dependencies, but requires operators to protect the environment variable as a critical secret.
Can OpenShip read legacy SSH credentials that were not encrypted?
Yes. The decryptSecretField() helper in apps/api/src/lib/credential-encryption.ts detects the absence of the enc1: prefix and returns the value unchanged. This allows the system to operate with mixed legacy and encrypted data during gradual migrations.
Are decrypted SSH passwords or private keys ever written to logs?
No. According to the source and SECURITY_GUIDE.md, decrypted plaintext exists only in memory for the lifetime of the SSH connection. The material is passed directly to the ssh2 client and is never persisted to logs or secondary storage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →