If you're open to a PR I can submit one for this ..
Problem
It's better security practice to keep secrets out of KDL files - they shouldn't be on disk at all, and we definitely don't want to commit them to git.
To do this now, you need do a service startup replacement, like a systemd ExecStartPre that runs sed.
This has potential footguns: escaping bugs, ordering issues with secrets-fetch services, and unit restarts. Having ferron build in the substitution avoids those.
Use cases
- Bearer-token auth:
location "/api" {
if_not (is_equal "{header:authorization}" "Bearer @@FILE:/run/secrets/api-token@@") {
status 401
}
}
- Basic-auth credentials in a custom condition or header.
- Upstream auth headers:
proxy "https://backend" {
request_header "Authorization" "Bearer @@FILE:/run/secrets/upstream-token@@"
}
- Per-tenant API keys loaded from a directory of per-tenant files, where the static KDL references each by path.
The general form is: secret bytes live in a file (such as /run in tmpfs with appropriate permissions); the config references the file by path; the server expands the reference at load time and the secret never appears on disk in the merged config.
Prior art
- Caddy:
{file./path/to/file} placeholder (and {$ENV} for environment variables) — both expand at config-load time.
- nginx: variable interpolation (
$variable) plus include directives.
- Apache httpd:
Include and Define.
- HAProxy:
setenv and getenv directives.
- Traefik: file-based providers and explicit secret references.
Ferron is the outlier in not having this.
Potential implementation
A new substitution token recognized by the KDL parser at config-load time. Suggested syntax:
Semantics:
- File contents read once at config load and on
SIGHUP reload.
- Trailing newline (
\n or \r\n) trimmed by default — matches what cat of a typical password file produces and what the operator usually wants. Internal whitespace preserved verbatim.
- Read errors (file missing, permission denied, non-UTF-8 if the consumer expects UTF-8) fail config load with a clear error pointing at the offending KDL line.
- File path must be absolute. Relative paths are rejected to avoid CWD-dependent inconsistencies.
- Optional:
@@FILE_RAW:/path@@ to preserve trailing newline.
Syntax:
@@FILE... is just an example. I'm not opinionated as to the syntax. Other forms like {file:/path} could work.
Other benefits - besides the key point of better security
- Self-contained scope: the change is a preprocess pass over the loaded KDL string. It doesn't touch the auth subsystem, the proxy layer, the TLS layer, or the parser's KDL semantics. New surface is small.
- Aligns with file-based secret-management tools: sops-nix, secretspec, Kubernetes Secret volume mounts, AWS Parameter Store CSI drivers, HashiCorp Vault Agent injectors, Doppler, etc. all materialize secrets as files.
Alternatives considered
include directive for values. Less flexible — include typically pulls a whole config block, not a scalar value. Doesn't fit the use case where you want a single string substituted into a longer expression.
- Environment-variable interpolation only (e.g.
${VAR}). Works for short scalar values but is awkward for multi-line values like cert content; also requires the operator to populate env vars, which has its own fragility (secret leaks via /proc/<pid>/environ). I did see an open issue for environment substitution. This is complementary and IMO preferable, although both could be implemented.
- External preprocessor: like
sed. It's what we have to do today, but all ferron users have to implement it
- KDL custom value type: too invasive — KDL is a syntax-level language; value-substitution is more naturally a Ferron-level preprocess pass than a KDL extension.
If you're open to a PR I can submit one for this ..
Problem
It's better security practice to keep secrets out of KDL files - they shouldn't be on disk at all, and we definitely don't want to commit them to git.
To do this now, you need do a service startup replacement, like a systemd
ExecStartPrethat runs sed.This has potential footguns: escaping bugs, ordering issues with secrets-fetch services, and unit restarts. Having ferron build in the substitution avoids those.
Use cases
The general form is: secret bytes live in a file (such as /run in tmpfs with appropriate permissions); the config references the file by path; the server expands the reference at load time and the secret never appears on disk in the merged config.
Prior art
{file./path/to/file}placeholder (and{$ENV}for environment variables) — both expand at config-load time.$variable) plusincludedirectives.IncludeandDefine.setenvandgetenvdirectives.Ferron is the outlier in not having this.
Potential implementation
A new substitution token recognized by the KDL parser at config-load time. Suggested syntax:
Semantics:
SIGHUPreload.\nor\r\n) trimmed by default — matches whatcatof a typical password file produces and what the operator usually wants. Internal whitespace preserved verbatim.@@FILE_RAW:/path@@to preserve trailing newline.Syntax:
@@FILE...is just an example. I'm not opinionated as to the syntax. Other forms like{file:/path}could work.Other benefits - besides the key point of better security
Alternatives considered
includedirective for values. Less flexible —includetypically pulls a whole config block, not a scalar value. Doesn't fit the use case where you want a single string substituted into a longer expression.${VAR}). Works for short scalar values but is awkward for multi-line values like cert content; also requires the operator to populate env vars, which has its own fragility (secret leaks via/proc/<pid>/environ). I did see an open issue for environment substitution. This is complementary and IMO preferable, although both could be implemented.sed. It's what we have to do today, but all ferron users have to implement it