NAME
Punk::Config - YAML configuration with secrets kept out of the file
SYNOPSIS
# config/punk.yml - safe to commit
views:
Stencil:
template_dir: root/templates
wrapper: layout.tmpl
database:
dsn: dbi:Pg:dbname=myapp;host=db
user: myapp
password: { $env: DB_PASSWORD }
plugins:
RequestId: {}
# MyApp.pm
package MyApp;
use Punk;
config 'config/punk.yml'; # loads, applies, freezes
get '/' => 'Web::Book#home';
DESCRIPTION
Configuration is read once, at boot, from a YAML file; layered by environment; and applied to the application before to_app freezes it. See "config" in Punk for what the known blocks do.
Implemented in C (include/punk/punk_config.h): the layering, the deep merge, secret resolution, the guardrail and the redacted copy. Only YAML parsing (one YAML::XS call per file) and the $exec resolver's subprocess are Perl, both once per boot.
Secrets
A config file should say where a secret comes from, never what it is. A reference is a single-key hash whose key starts with $ - a shape nothing else in a config has, and one that needs nothing from the parser:
password: { $env: DB_PASSWORD }
password: { $file: /run/secrets/db_password }
password: { $exec: [ vault, read, -field=password, secret/db ] }
password: { $literal: not-actually-secret }
$env reads an environment variable (missing is fatal - better than starting with an empty password). $file reads a file and trims one trailing newline, which is exactly how Docker and Kubernetes deliver secrets to a container. $exec runs a command without a shell and takes its standard output, for Vault, aws secretsmanager, op read and friends; a non-zero exit is fatal. $literal is a deliberate inline value that bypasses the guardrail below.
Resolved secrets are kept apart from the public structure: $app->config has [redacted] where each one was, so it can be logged, dumped or serialised safely, and $app->secret($path) reaches the real value. Boot-time consumers are handed the real value directly, so a database password never sits in a general-purpose hash at all.
The guardrail
A plaintext value under a secret-shaped key (anything containing pass, secret, token, api_key, private_key or credential, the exact key auth, or a dsn with password= in it) is almost always a mistake. The loader notices and, by default, warns:
secrets: strict # refuse to start
secrets: warn # the default
secrets: off
auth matches only as a whole key, so an author field is left alone.
Layering
config/punk.yml is read first, then config/punk.$PUNK_ENV.yml (development unless PUNK_ENV says otherwise), then config/punk.local.yml. Later layers win. Hashes merge key by key; anything else replaces, so an array in an overlay is a replacement rather than an append.
Add punk.local.yml to .gitignore: it is the one place a developer may keep a throwaway value.
METHODS
load(file => $path, env => $name, secrets => $mode)
Read, layer, resolve and validate. Croaks if no layer exists, if a reference cannot be resolved, or - in strict mode - if the guardrail fires.
config
The public structure: secrets replaced by [redacted].
resolved
The same structure with the real values. What the boot consumers read.
get($dotted_path)
The resolved value at a path, secrets included.
secret($dotted_path)
has_secret($dotted_path)
secret_paths
The resolved secrets.
env
files
The environment name, and the layers actually read.
SEE ALSO
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)