Please report privately, through GitHub's advisory form, rather than in a public issue.
Include what you would want if you were on the other side: what an attacker can do, what they need to start with, and — if you have one — a reproduction. A first response should take a few days; if it takes longer than a week, assume the message went astray and ping the issue tracker without details.
A configuration library sits in an awkward place: it reads files a program trusts, holds values a program must not leak, and — with the companion crates — talks to services over a network. These are the properties it tries to keep, and the ones it explicitly does not.
- a symlink inside the secrets directory from resolving outside it —
refused by default since 0.8.0 with an error naming the entry
(
allow_external_symlinks(true)is the deliberate opt-out); the kubelet's..dataindirection stays inside the mount and keeps working. Pinned by the adversarial half oftests/secrets_dir.rs.
Secrets stay out of diagnostics. #[config(secret)] redacts a field in
Debug; reload diffs, check() reports, unknown-key suggestions and error
messages all report paths and types, never values, so nothing routes around the
redaction. A report of a value appearing in a log or an error message is a
vulnerability. dynamic-config/tests/security.rs asserts each of these, and CI
runs it as a job of its own.
The parsing surfaces are fuzzed. The unit parsers, Value path lookup and
the rule that decides whether a path touches a secret run under libFuzzer
(fuzz/, built on every CI run and run on a weekly schedule). A crash or a
redaction rule that answers differently for the same path is a finding: report
it through the process above, with the reproducing input attached.
Files it writes are private from the moment they exist. save and the
last-known-good cache create their file 0600 on Unix — created that way, not
chmodded afterwards — through a temporary path opened with create_new, so a
symlink planted at that path is refused rather than followed.
A remote store cannot crash the process. Everything a server sends is treated as untrusted input: lease durations that would overflow a clock, bodies that are not JSON, values that are not UTF-8. A panic reachable from a server's response is a vulnerability.
Credentials are not logged. A Debug on a source prints its address and its
key, never a token, a password or a key file's contents.
Keep secrets out of process memory. A resolved configuration holds every
value, because that is what configuration is — a program that can use a password
can read it. The age feature zeroizes the decrypted file once parsed, which
is defence in depth, not a claim about memory.
Encrypt what it writes. save and the cache write plaintext. The cache's
three modes exist so that is a choice rather than a surprise; see
the book.
Validate that a config file is trusted. If an attacker can write your config file, they can configure your program. That is the file's permissions to enforce, not this crate's.
Sandbox a Decryptor or a RemoteSource you implement. Those run your code
with your privileges.
| Version | Supported |
|---|---|
| 0.10.x | ✅ the latest patch |
| ≤ 0.9 | — end of life |
Security fixes land on the latest patch of the line above and nothing is backported before 1.0: when a release ships, every prior patch of its line is end-of-life the same day. Older toolchains resolve older published versions through cargo's MSRV-aware resolver and are explicitly unsupported. After 1.0, the current and previous minor lines. The full promise lives in the engine book's Compatibility Contract.
This crate assumes:
- The config files are trusted. Whoever can write them can configure the program, which is the point of a config file. File permissions are the control, not this crate.
- The environment is trusted. Same reasoning: anything that can set
APP_DB_HOSTis already inside the process's world. - A remote store is not trusted. It is across a network, may be compromised, and may simply be buggy. Everything it sends is parsed defensively; nothing it sends can panic the process or reach an arithmetic overflow.
- A
DecryptororRemoteSourceyou write is trusted. It runs your code. - Log output is not private. Everything this crate writes to a log or an error may end up in a system that many people can read, which is why diagnostics carry paths and types rather than values.
Prose is not a guarantee, so each claim above has something behind it that CI runs on every change:
| Claim | Enforced by |
|---|---|
| Secrets stay out of diagnostics | dynamic-config/tests/security.rs, run as its own CI job |
| Files are created private, symlinks refused | write::permissions tests, same job |
| A store cannot panic the process | checked_add on every server-supplied duration, plus a hostile-document test |
| No unsafe code | #![forbid(unsafe_code)] in every crate, and a CI job that checks the attribute is still there |
| No unmaintained or vulnerable dependency | cargo deny, on every push and weekly on a schedule |
| No surprise dependency | cargo deny sources and licences, plus dependency review on pull requests |
| Parsing survives arbitrary input | ten fuzz targets (fuzz/), run nightly |
| A ledger entry still matches something | cargo deny -D advisory-not-detected, which fails the job when an exception no longer applies |
Exceptions in deny.toml are listed one at a time with a reason. There is no
blanket "ignore dev-dependencies": something that runs on a contributor's
machine or in CI still matters. An exception that stops matching anything
fails the job rather than lingering: the reason written beside it would
otherwise become documentation of something untrue.
The OpenSSF Scorecard badge in the README is honest, and three of its checks stay low for reasons no amount of work here changes. They are written down rather than chased:
Fuzzingis a false negative. Scorecard looks for OSS-Fuzz membership; this repository fuzzes ten targets nightly out offuzz/, and one of them found the panic recorded in the 0.9.0 changelog. The check cannot see a fuzzer it does not host.Code-Reviewcannot pass with one maintainer. It counts pull requests approved by somebody other than their author, and there is nobody else. What stands in for it is stated rather than implied:mainrequires the CI and Security gates, self-approval is not possible, and every change goes through a pull request.CII-Best-Practicesis a badge application, not a property of the code. It will be filed when the project has a second maintainer to answer half of what it asks.
Maintained resolves itself with repository age. Everything else on that
tab is worth acting on.
The core crate's non-optional dependency list is deliberately short —
serde, arc-swap and the config crate, which carries the fold this
crate has none of its own — and every network client, format parser and crypto stack
is behind a feature or in a companion crate, so a build carries only what it
asked for. A dynamic-config with default features pulls in no cryptography, no
HTTP client and no runtime.
cargo deny runs on every push and weekly on a schedule, because an advisory is
published on somebody else's timetable: a dependency that was clean on Monday is
not necessarily clean on Friday, and nothing about this repository has to change
for that to happen.
GitHub's Dependabot alerts are a second feed with different coverage — GHSA
advisories that RustSec has not picked up, and vice versa — so passing
cargo deny does not close the security tab. The standing rule: every open
alert is triaged before a release ships. Triaged means one of two outcomes,
each visible afterwards: the lockfile moves to a patched version (with
--precise when the MSRV-fallback resolver refuses the jump — the same
mechanism deny.toml documents for time), or the alert is dismissed with a
written reason stating why the vulnerable path is not reachable and what would
reopen the question. An alert that is neither fixed nor argued away blocks the
release.
A fix, credit unless you would rather not have it, and a note in the changelog
under Security describing what was wrong in enough detail to tell whether you
were affected. If a fix has to be coordinated with a downstream project, that is
worth saying in the first message.