Skip to main content

Secret Management

Encrypted secret storage with multiple provider backends.

Usage

import { SecretManager } from '@xec-sh/ops';

const secrets = new SecretManager({
type: 'local',
config: { dir: '/path/to/secrets' },
});

// Store a secret
await secrets.set('db_password', 'my-secret-value');

// Retrieve a secret
const password = await secrets.get('db_password');

// Delete a secret
await secrets.delete('db_password');

Providers

ProviderDescription
localEncrypted file storage (AES-256-GCM)
envEnvironment variables
dotenv.env file
vaultHashiCorp Vault
aws-secretsAWS Secrets Manager
1password1Password

Configuration

import { ConfigurationManager } from '@xec-sh/ops';

const config = new ConfigurationManager({
secretProvider: {
type: 'local',
config: { dir: '/var/secrets/myapp' },
},
secretsDir: '/var/secrets/myapp',
});

In Config Files

Reference secrets in YAML configuration:

targets:
production:
type: ssh
host: prod.example.com
password: "${secret:ssh_password}"

The ${secret:name} syntax is resolved by the VariableInterpolator during config loading.

Encryption

The local provider encrypts with AES-256-GCM, deriving the key with scrypt from a per-secret random salt. Files are written owner-only (0600) into an owner-only directory (0700).

What the key is made of, and what that protects

By default the key material is a machine identifier — the hardware UUID on macOS, /etc/machine-id on Linux. That is enough to make the stored file useless somewhere else: a backup, a synced folder, a repository it should never have entered, a stolen disk.

It is not a defence against someone who can already read the file on that machine. The identifier is not a secret — /etc/machine-id is world-readable — so anyone with the ciphertext and local access has both halves. The file mode is what stops them, and encryption is the second layer behind it.

Add a passphrase when you need the key to depend on something the machine does not hold:

const secrets = new SecretManager({
type: 'local',
config: { dir: '/var/secrets/myapp', passphrase: process.env.XEC_SECRETS_PASSPHRASE },
});

The passphrase is combined with the machine identifier, so both are needed to decrypt. changePassphrase() re-encrypts every secret; it writes all of them or none, so a wrong passphrase part-way through cannot leave the store split between two keys.

For secrets that must survive the loss of the machine, or be shared by more than one person, use a provider built for that — vault, aws-secrets or 1password.