# How OpenShip Implements Credential Encryption for SSH Keys and Secrets

> Learn how OpenShip encrypts SSH keys and secrets with AES-256-GCM using the enc1 envelope and BETTER_AUTH_SECRET for robust security. Secure your credentials.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: how-to-guide
- Published: 2026-08-19

---

**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`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/encryption.ts)

The foundation of the system sits in [`apps/api/src/lib/encryption.ts`](https://github.com/oblien/openship/blob/main/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.

```typescript
// 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`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/credential-encryption.ts)

To distinguish encrypted secrets from legacy plaintext, [`apps/api/src/lib/credential-encryption.ts`](https://github.com/oblien/openship/blob/main/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.

```typescript
// 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`](https://github.com/oblien/openship/blob/main/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.

```typescript
// 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`](https://github.com/oblien/openship/blob/main/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_SECRET` inside [`apps/api/src/lib/encryption.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/encryption.ts).
- The `enc1:` versioned envelope in [`apps/api/src/lib/credential-encryption.ts`](https://github.com/oblien/openship/blob/main/apps/api/src/lib/credential-encryption.ts) marks encrypted SSH secrets and maintains backward compatibility with legacy plaintext.
- Database columns `sshPassword`, `sshKeyPassphrase`, and `sshPrivateKey` in [`packages/db/src/schema/servers.ts`](https://github.com/oblien/openship/blob/main/packages/db/src/schema/servers.ts) store 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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.