127 lines
4 KiB
Text
127 lines
4 KiB
Text
|
|
# Secrets Loader - Import and merge encrypted YAML secrets into Nickel configs
|
||
|
|
|
||
|
|
{
|
||
|
|
# Type for a secrets configuration
|
||
|
|
SecretsConfig = {
|
||
|
|
# Source YAML file path (can be SOPS-encrypted)
|
||
|
|
source_path | std.string | doc "Path to YAML secrets file (relative or absolute)",
|
||
|
|
# Environment (dev, staging, prod) - determines which secrets file to load
|
||
|
|
environment | std.string | doc "Environment: dev, staging, or prod" = "dev",
|
||
|
|
# Whether to merge with defaults
|
||
|
|
merge_defaults | std.bool | doc "Merge with default configuration" = true,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Load secrets from a YAML file
|
||
|
|
# If file ends with .enc, it will be decrypted via vault-service at runtime
|
||
|
|
load = fun source_path =>
|
||
|
|
let contents = std.string.trim (
|
||
|
|
# Import the YAML file as a string
|
||
|
|
# At deployment time, this will be decrypted SOPS file
|
||
|
|
std.json.stringify { path = source_path }
|
||
|
|
) in
|
||
|
|
# Parse YAML as record structure
|
||
|
|
# Assumes file is in format:
|
||
|
|
# key1:
|
||
|
|
# nested: value
|
||
|
|
# key2: value
|
||
|
|
contents,
|
||
|
|
|
||
|
|
# Load environment-specific secrets
|
||
|
|
# Tries: secrets.{env}.yaml → secrets.yaml → {}
|
||
|
|
load_env = fun base_path environment =>
|
||
|
|
let env_path = $"($base_path)/secrets.($environment).yaml" in
|
||
|
|
let default_path = $"($base_path)/secrets.yaml" in
|
||
|
|
{
|
||
|
|
env_specific = env_path,
|
||
|
|
default = default_path,
|
||
|
|
environment = environment,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Merge secrets into configuration template
|
||
|
|
# Replaces placeholder values with actual secrets
|
||
|
|
merge = fun template secrets =>
|
||
|
|
let replace_placeholders = fun obj =>
|
||
|
|
std.record.map (
|
||
|
|
fun _key value =>
|
||
|
|
if std.string.is_string value then
|
||
|
|
# Check if value is a placeholder like "${secret:database.password}"
|
||
|
|
if value |> std.string.starts_with "${secret:" then
|
||
|
|
let secret_path = (
|
||
|
|
value
|
||
|
|
|> std.string.drop_prefix "${secret:"
|
||
|
|
|> std.string.drop_suffix "}"
|
||
|
|
) in
|
||
|
|
# Navigate to secret_path in secrets record
|
||
|
|
# e.g., "database.password" → secrets.database.password
|
||
|
|
std.record.get_path (std.string.split "." secret_path) secrets
|
||
|
|
else
|
||
|
|
value
|
||
|
|
else if std.record.is_record value then
|
||
|
|
replace_placeholders value
|
||
|
|
else
|
||
|
|
value
|
||
|
|
) obj in
|
||
|
|
replace_placeholders template,
|
||
|
|
|
||
|
|
# Extract secrets by path pattern
|
||
|
|
# Useful for selecting subset of secrets
|
||
|
|
extract_by_path = fun secrets pattern =>
|
||
|
|
let matches_pattern = fun key =>
|
||
|
|
key |> std.string.contains pattern in
|
||
|
|
secrets
|
||
|
|
|> std.record.to_array
|
||
|
|
|> std.array.filter (fun {key, _value} => matches_pattern key)
|
||
|
|
|> std.array.fold_left (fun acc item =>
|
||
|
|
acc & { (item.key) = item.value }
|
||
|
|
) {},
|
||
|
|
|
||
|
|
# Validate secrets structure against schema
|
||
|
|
validate_secrets = fun secrets required_keys =>
|
||
|
|
let missing = (
|
||
|
|
required_keys
|
||
|
|
|> std.array.filter (fun key =>
|
||
|
|
not (std.record.has_field key secrets)
|
||
|
|
)
|
||
|
|
) in
|
||
|
|
if std.array.length missing > 0 then
|
||
|
|
{
|
||
|
|
valid = false,
|
||
|
|
missing_keys = missing,
|
||
|
|
error = $"Missing required secret keys: {std.string.join \", \" missing}",
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
valid = true,
|
||
|
|
missing_keys = [],
|
||
|
|
error = null,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Resolve secrets from multiple sources (environment variables, files, defaults)
|
||
|
|
resolve = fun secret_specs =>
|
||
|
|
let resolved = fun spec =>
|
||
|
|
let env_var = $"PROVISIONING_SECRET_{spec.key}" in
|
||
|
|
{
|
||
|
|
key = spec.key,
|
||
|
|
source = (
|
||
|
|
if (std.string.from_env env_var | std.string.is_empty) then
|
||
|
|
"file"
|
||
|
|
else
|
||
|
|
"env"
|
||
|
|
),
|
||
|
|
value = (
|
||
|
|
std.string.from_env env_var
|
||
|
|
| (fun env_val =>
|
||
|
|
if std.string.is_empty env_val then
|
||
|
|
spec.default_value
|
||
|
|
else
|
||
|
|
env_val
|
||
|
|
)
|
||
|
|
),
|
||
|
|
} in
|
||
|
|
secret_specs
|
||
|
|
|> std.array.map resolved
|
||
|
|
|> std.array.fold_left (fun acc item =>
|
||
|
|
acc & { (item.key) = item.value }
|
||
|
|
) {},
|
||
|
|
}
|