Security Model
What dotsec protects, what it assumes, and what it explicitly does not defend against. For the mechanics (ciphertext format, MAC scope, key wrapping) see How Encryption Works.
What dotsec solves — and what it doesn't
dotsec reduces exposure of secrets at rest — on disk, in repos, in CI caches — especially against the dragnet
postinstall/ filesystem-scrape supply-chain attack class.dotsec does not eliminate secret exfiltration by code running in the same process or the same CI job after dotsec has decrypted. A malicious dependency running in your application after
dotsec runhas injected env vars can still read those env vars. A compromised CI runner that holdskms:Decryptcan still call it.
That's the honest one-paragraph framing. It's a real reduction in attack surface — .env files are the single most-harvested artifact in the 2025 npm worm wave, and a .sec file is worthless ciphertext to a dragnet. But "no .env files" is not the same as "no secret leakage." Anything that claims the second needs a runtime sandbox; dotsec doesn't.
For production use, prefer the KMS provider with per-environment KMS keys (dev / staging / prod separate) and least-privileged IAM pinned to the dotsec:format=v3 encryption context. Everything else — workflow approval gates, OIDC federation, lockfile hygiene, runtime egress restrictions — is your standard cloud-security responsibility, not dotsec's.
The core claim
A .sec file is safe to publish. The design assumes the file is world-readable — committed to a public repo, attached to a PR, cached by a CI runner. Everything sensitive in it is AES-256-GCM encrypted with a 256-bit data encryption key (DEK), and the DEK is wrapped by your age keypair or AWS KMS.
What's secret is the key, never the file:
The KMS path is the strongest version of this. With KMS, dotsec is a thin client and the trust root is the HSM that backs your existing AWS account — the same one your compliance team has already approved for every other workload. dotsec is not a vendor you trust; AWS is, and you trust them already.
How the cryptography is built
The properties below are visible in the source — they're not marketing claims.
For the full wire-format details and the explicit list of what the MAC does not cover (and why), read on.
What an attacker with the .sec file can do
Nothing useful, by design — but precisely:
The last row is the documented gap: plaintext values are not MAC-covered, so hand-editing them stays friction-free. But "not a secret" is not the same as "safe to leave mutable." A spectrum to think about:
Rule of thumb: if mutating the value alone would shift a security boundary, mark it @encrypt even if the value itself isn't a credential. Confidentiality isn't the only reason to encrypt — integrity is.
For everything else, put validation rules (@type, @pattern, @min/@max, enum(...)) in dotsec.schema. The schema is integrity-bound, and dotsec validate runs on every load — a tampered plaintext value that breaks the schema is rejected before your app sees it.
On-disk surface area
dotsec is designed so the only sensitive artifact on a developer's machine is the private key — and even that can move off-disk:
The KMS row matters for the supply-chain attack class — when a compromised dependency's postinstall script runs as your user and grep-walks the filesystem for .env, .sec.key, AWS credentials, etc., there's simply nothing to find. The wrapped DEK in .sec is useless without an IAM-authenticated, CloudTrail-logged call to KMS, which the malicious script cannot make undetected.
DOTSEC_PRIVATE_KEY is checked before any key file (discovery order), so any tool that can inject an env var into a child process can take over key delivery — letting you delete .sec.key from disk. Examples worth exploring: the 1Password CLI (op run resolves op:// secret references), direnv bound to a keychain command, or a shell function that reads from gpg --decrypt.
Where dotsec stops
dotsec hands plaintext secrets to your process via env vars. From that point on, runtime exposure — application logs, crash reporters, container introspection, frontend bundles, whatever — is your application's and runtime's responsibility. dotsec doesn't claim to solve any of that, and you shouldn't expect it to.
Entry names are visible
Even with every value encrypted, the names of your secrets are plaintext in the .sec file:
Anyone reading the file learns that you use Stripe, OpenAI, and JWTs, and they learn the count and rough shape of your secret architecture. For most teams this is fine — the names are derivable from your code anyway, your package.json lists Stripe and OpenAI SDKs. For teams where the fact of using a particular vendor is itself sensitive, give entries opaque names (API_KEY_1, THIRD_PARTY_AUTH_42) and remap inside your app. This hurts readability for everyone including you; only do it when you have to.
Assumptions
- dotsec defends data at rest, not at runtime. See above.
- dotsec can't stop same-user code from clobbering
.sec.key. Use the KMS provider if that's in your threat model — no key file. - Key compromise ends the story. An attacker holding both the file and the private key (or
kms:Decryptrights) reads everything. There is no defense-in-depth below the key. panic = "abort"skips destructor-based memory wipe. Two layers defend against the resulting coredump exposure: the fuzz harness keeps the input-driven panic surface closed, anddotseccallssetrlimit(RLIMIT_CORE, 0)at startup so a panic can't drop a dump containing in-flight secrets in the first place.dotsec migrateexecutes the v4 config. Adotsec.config.{ts,js}is code; migrating runs it. Only migrate configs you trust — see the migrate command.
Engineering posture
- Fuzzing. The parser surface that consumes untrusted
.seccontent (grammar,@dotsec(...)header, schema files, parse→render round-trip) is covered by fourcargo-fuzztargets with curated seed corpora — seefuzz/in the repo. - Dependency audit. CI runs
cargo auditon every push; ignores live in.cargo/audit.toml, each with a written rationale. - Memory hygiene. DEKs, decrypted values, and key-file contents are wrapped in
Zeroizingat the moment secret material enters them, so every exit path (including errors) wipes them. - Constant-time comparisons for the file MAC and the key commitment (via
subtle). - No secrets over FFI. The
@dotsec/coreNode bindings expose parsing, validation, and formatting only — no decrypt, no key material crosses the boundary.
Audit and maturity
Honest disclosure, because security-conscious adopters ask:
What we have. The age and AES-256-GCM primitives themselves are well-audited (age had a Cure53 review in 2021). The dotsec wire format on top of them — the canonical serialization, MAC scope, schema-hash binding — is specified in source-as-spec form: dotsec-core/src/header_v3.rs documents the directive shape, crypto/src/mac.rs documents the canonical bytes the MAC covers. The parser surface that consumes untrusted .sec content is fuzzed by four cargo-fuzz targets running nightly in CI. Memory hygiene (zeroize on every error path), constant-time MAC comparison (via subtle), and panic-time coredump suppression (setrlimit(RLIMIT_CORE, 0)) are documented above.
What we don't have. No independent cryptographic audit of the dotsec wire format yet. No published test vectors as a stand-alone artifact (they exist as unit tests in crypto/src/mac.rs but aren't packaged for cross-implementation verification). The project is a single-maintainer Rust rewrite that hit v7 in 2026; it does not have the "deployed at scale for five years across thousands of teams" maturity that some adopters require.
Context for the comparison everyone makes. Mozilla SOPS is older, more widely deployed, and addresses the same broad category (encrypted secrets in git, with KMS/age wrapping). The honest positioning is:
- SOPS is the choice when you need multi-cloud (sops wraps the same DEK to AWS KMS and GCP KMS and age and PGP in one file), polyglot file formats (YAML, JSON, ENV, INI, binary), and a mature ecosystem with helm/terraform/kustomize plugins.
- dotsec is the choice when you're
.env-shaped, want schema-driven validation with zero-runtime TypeScript codegen, preferdotsec run -- <cmd>runtime injection over SDKs, and want the AWS-native pattern (KMS encryption context, CloudTrail per-decrypt audit) as a first-class story rather than a plugin.
Different shapes for adjacent problems. For most npm-shaped Node/TS shops on AWS, dotsec fits. For polyglot DevOps shops managing YAML/JSON config across clouds, SOPS fits better. Pick the tool that matches your shape.
If "battle-tested" is a hard requirement — financial services, healthcare, anything compliance-bound on a vendor-maturity matrix — wait, or pick SOPS, or run dotsec in development environments first and revisit for production once your own internal review converges. We'd rather you make an informed choice than a misled one.
Wire format history
The on-disk envelope is versioned by the format= field in the @dotsec(...) directive, independent of the package version.
Readers reject unknown format= tags rather than guessing. The format version only bumps when the envelope changes incompatibly — package majors come and go without touching it.
Reporting
Found something? Open a GitHub security advisory — please don't file public issues for suspected vulnerabilities.