Secret Providers
Secret providers fetch sensitive configuration values from external sources and inject them into the secrets layer, which sits above environment variables and manifests in the precedence order. Fields marked sensitive: true in their @AIEnv declaration are resolved exclusively through secret providers.
Key Concepts
- Separation of concerns — secret providers keep credentials out of environment variables and source-controlled files.
- Composable —
CompositeSecretProviderchains multiple providers; the first one that returns a non-undefinedvalue wins. - Automatic redaction —
EnvironmentConfigManagercallsprovider.getSecret(key)for each sensitive field but stores only the redacted form in layers and provenance output.
SecretProvider Interface
Every provider implements this interface:
interface SecretProvider {
name: string;
getSecret(key: string): Promise<string | undefined> | string | undefined;
describe(): string; // Human-readable description used in provenance output
}
The key passed to getSecret is the environment variable name computed for the field (e.g., DATABASE_PASSWORD). Providers are free to transform the key before looking it up.
Built-in Providers
EnvSecretProvider
Reads secrets from process.env. An optional prefix is prepended to every key lookup.
import { EnvSecretProvider } from '@tagna/udiot';
// No prefix: looks up DATABASE_PASSWORD directly
const plain = new EnvSecretProvider();
// With prefix: looks up SECRET_DATABASE_PASSWORD
const prefixed = new EnvSecretProvider('SECRET');
Constructor
new EnvSecretProvider(prefix?: string)
| Parameter | Type | Default | Description |
|---|---|---|---|
prefix |
string |
'' |
Prepended to every key after normalisation |
Key normalisation: dots and hyphens are replaced with underscores, then the string is upper-cased. If a prefix is provided, it is prepended with an underscore separator.
FileSecretProvider
Reads secrets from a JSON file on disk. The file must export a flat Record<string, string>. Parsed content is cached in memory after the first read.
import { FileSecretProvider } from '@tagna/udiot';
const provider = new FileSecretProvider('/run/secrets/app-secrets.json');
Constructor
new FileSecretProvider(filePath: string)
| Parameter | Type | Description |
|---|---|---|
filePath |
string |
Absolute path to the JSON secrets file |
File format
{
"DATABASE_PASSWORD": "super-secret",
"API_KEY": "abc123"
}
If the file does not exist the provider silently returns undefined for all keys. If the file exists but cannot be parsed as JSON, an error is thrown.
CompositeSecretProvider
Tries a list of providers in order and returns the first non-undefined value. Useful for layering a high-security source over a fallback.
import { CompositeSecretProvider, FileSecretProvider, EnvSecretProvider } from '@tagna/udiot';
const provider = new CompositeSecretProvider([
new FileSecretProvider('/run/secrets/app-secrets.json'),
new EnvSecretProvider('SECRET'),
]);
Constructor
new CompositeSecretProvider(providers: SecretProvider[])
The describe() method returns a comma-separated list of each child provider's description, which appears in provenance output.
Default Secret Provider
When no secretProvider is supplied to EnvironmentConfigManager, a default is created automatically:
// Equivalent to what the manager constructs internally
import { createDefaultSecretProvider } from '@tagna/udiot';
const provider = createDefaultSecretProvider(process.cwd());
// → CompositeSecretProvider([
// EnvSecretProvider('SECRET'), // reads SECRET_* env vars
// FileSecretProvider('{root}/config/secrets.json'),
// ])
API Reference
| Export | Description |
|---|---|
SecretProvider |
Interface for all providers |
EnvSecretProvider |
Reads from process.env with optional prefix |
FileSecretProvider |
Reads from a JSON file; caches on first load |
CompositeSecretProvider |
Tries multiple providers in order |
createDefaultSecretProvider(rootDir) |
Factory for the built-in composite default |
Basic Usage Example
import 'reflect-metadata';
import {
AIConfig,
AIEnv,
buildConfigSchema,
EnvironmentConfigManager,
CompositeSecretProvider,
FileSecretProvider,
EnvSecretProvider,
} from '@tagna/udiot';
@AIConfig({ namespace: 'database', secrets: 'auto' })
class DatabaseConfig {
@AIEnv({ required: true })
host!: string;
@AIEnv({ sensitive: true, required: true })
password!: string;
}
const secretProvider = new CompositeSecretProvider([
new FileSecretProvider('/run/secrets/db.json'),
new EnvSecretProvider('SECRET'),
]);
const manager = new EnvironmentConfigManager({
schemas: [buildConfigSchema(DatabaseConfig)],
environment: 'production',
workspaceRoot: process.cwd(),
secretProvider,
});
const result = await manager.load();
// result.values['database'].password is the actual secret value
// result.provenance['database.password'][*].value is '[REDACTED]'
Common Patterns
Docker secrets (file-based)
Docker Swarm and Kubernetes both mount secrets as files under /run/secrets/. The file name is the secret name, but FileSecretProvider expects a single JSON file with all secrets. Wrap the individual files in a sidecar or init container that combines them:
{
"DATABASE_PASSWORD": "<contents of /run/secrets/db_password>",
"API_KEY": "<contents of /run/secrets/api_key>"
}
Then:
const provider = new FileSecretProvider('/run/secrets/combined.json');
Custom provider (e.g., HashiCorp Vault)
import { SecretProvider } from '@tagna/udiot';
class VaultSecretProvider implements SecretProvider {
readonly name = 'vault';
constructor(private readonly client: VaultClient, private readonly path: string) {}
async getSecret(key: string): Promise<string | undefined> {
const secret = await this.client.read(`${this.path}/${key.toLowerCase()}`);
return secret?.data?.value;
}
describe(): string {
return `HashiCorp Vault (${this.path})`;
}
}
Security Notes
- Sensitive values are never logged.
EnvironmentConfigManagercallsredactValue(value, true)before writing to layer snapshots and provenance entries. The raw value only exists inConfigLoadResult.values. - File caching.
FileSecretProvidercaches the parsed file in memory on the first read. Secrets rotated on disk without restarting the process are not picked up. For high-rotation secrets use a provider that fetches on each call. - Prefix collisions. When using
EnvSecretProviderwith a prefix alongside a non-prefixedEnvSecretProviderin the sameCompositeSecretProvider, ensure the prefix does not accidentally match legitimate application environment variables. - Secrets file permissions. The file read by
FileSecretProvidershould have600permissions (readable only by the process owner). UDIOT does not enforce this; it is the operator's responsibility.
Pitfalls
secrets: 'manual'on@AIConfigtells the manager not to automatically fetch secrets for fields in that schema. You are responsible for providing values another way (e.g., viaoverrides).- The
providerfield on@AIEnvis stored in the schema but not yet used for provider dispatch by the built-in manager. It is available for custom tooling and future versions. - If
FileSecretProviderreceives a file path that does not exist at startup, it silently returnsundefined. Missing secrets only become visible asmissingRequiredviolations for fields markedrequired: true.