Define and create secrets with Nix¶
Import nix-seal's NixOS, nix-darwin, or Home Manager module. No configuration framework or flake-parts module is required. Configure public trust once, then declare secrets beside their consumers.
Declare a value or template¶
After the setup below, list the values you need:
nixSeal.secrets = [ "token" "password" ];
Their sources default to <secretDirectory>/<name>.age. Ordinary declarations
need no lib.genAttrs or empty attribute sets. Add named options in the same
list when a value needs an exception:
nixSeal.secrets = [
"token"
"password"
{ registry-token.shared = true; }
{ database-password.source = "services/database/password.age"; }
];
A secret's source is a repository-relative string. shared = true selects
sharedSecretDirectory.
These settings select storage; they do not grant access to another target.
For a runtime configuration file, reference the declared secret in Nix:
{ config, ... }: {
nixSeal.secrets = [ "token" ];
nixSeal.templates.service = ''
TOKEN=${config.nixSeal.placeholder.token}
'';
}
The interpolation inserts a public marker, never the private value. A typo in
the secret name fails Nix evaluation. No JSON inventory or separate binding file
is needed. For names containing /, use the explicit alias described below.
For a separate public file, write TOKEN={{nix-seal:token}} in
templates/service.env.template and select it by name:
nixSeal.templates = [ "service.env" ];
templateDirectory changes the base directory once for the target. An explicit
file, such as nixSeal.templates.service = ./service.env.template;, overrides
that convention. The storage guide explains defaults,
shared files, and layouts for small and multi-target repositories. Include the
format extension before .template; keep conventional extensionless filenames
such as ssh_config.template. The named example above exposes its runtime file
at config.nixSeal.templates."service.env".path.
Each placeholder refers to a same-named declared secret. No inventory file or
repeated mapping is needed. Use config.nixSeal.secrets.token.path for the raw
credential and config.nixSeal.templates.service.path for the rendered file.
Only public template text enters the Nix store; substitution runs at activation.
To edit a template, edit that public text or file and rebuild. To change a
credential, edit its encrypted secret through nix-seal. Never replace a marker
with a real credential in the template.
Template lists also accept named exceptions, such as
templates = [ "service.env" { client = ./client.conf.template; } ];.
Attribute sets remain supported, including secrets.token = { }; and
templates.service = { };. A later imported module can extend a listed name
with nixSeal.secrets.token.restartUnits = [ "example.service" ];. List entries
use the ordinary Nix module merge, including priorities and conflict errors.
The full options remain available:
nixSeal.templates.service = {
source = ./service.env.template;
placeholders.token = "credentials/api-token";
mode = "0600";
restartUnits = [ "example.service" ];
};
Declare secrets."credentials/api-token" when using that alias. An encoding
override uses placeholders.token = { secret = "credentials/api-token";
encoding = "base64"; };. Supported encodings are utf8, base64, and hex.
UTF-8 inserts the original bytes. It does not escape shell, JSON, TOML, INI,
or dotenv syntax. Use values validated for the destination format, or keep an
entire private configuration in one encrypted file when its syntax or field
names must remain private. Never put a private value in Nix content or source.
Templates inherit the common activation phase of their fields. Configure a
field's phase = "services" once; its templates follow it. Fields with different
phases cannot be combined in one template. An explicit template phase must still
match its fields. Referencing a field does not change its ownership, permissions,
phase, recipients, or service actions. Configure those on the secret when needed. Unknown names, unused
overrides, malformed placeholders, and phase mismatches fail plan evaluation.
Mix public values and secrets¶
File-based templates can receive public values from your Nix configuration.
For example, an allowed_signers.template file can contain:
{{nix-seal:email}} namespaces="git" {{public:signing-key}}
Declare its public input once:
{ lib, ... }: {
nixSeal.secrets = [ "email" ];
nixSeal.templates.allowed-signers = {
source = ./allowed_signers.template;
publicValues.signing-key = lib.removeSuffix "\n" (builtins.readFile ./signing.pub);
};
}
An existing public Nix option can supply the value instead of a .pub file.
Public values are strings; use toString or a format-specific serializer for
other data. They enter the public Nix store. Keep passwords and private keys in
secrets, never in publicValues.
Nix resolves {{public:name}} once, then nix-seal resolves {{nix-seal:name}}
from decrypted fields at activation. Repeating a public marker reuses the same
value. Missing and unused public bindings fail evaluation. Values cannot contain
or introduce public or secret markers, and expanded public text is limited to
2 MiB. Substitution is literal and does not escape the destination format.
publicValues works with inline content too. Inline Nix templates can also use
ordinary Nix interpolation for public values alongside config.nixSeal.placeholder.
templates.<name>.source remains the original public file;
templates.<name>.renderedSource exposes the generated public text with secret
markers still intact. No JSON binding file is required.
Configure public trust once¶
For example, this module sets up a NixOS target. Replace the public-key placeholders with the outputs of your key-generation commands:
{ inputs, ... }: {
imports = [ inputs.nix-seal.nixosModules.default ];
nixSeal = {
repositoryRoot = ../.; # Adjust to the checkout root.
administrators.team = {
identities = {
admin = { kind = "administrator"; public = "age1..."; };
release = { kind = "signer"; public = "nix-seal-ed25519-v1:..."; };
create = { kind = "authorizer"; public = "nix-seal-ed25519-v1:..."; };
};
approvalPolicies.release = { threshold = 1; signers = [ "release" ]; };
defaultApprovalPolicy = "release";
};
publicKey = "age1...";
identityFile = "/var/lib/nix-seal/target.agekey";
secrets = [ "token" ];
templates.service = "TOKEN={{nix-seal:token}}\n";
};
}
Store private keys outside the checkout and Nix store. key generate creates
an age identity; key generate-signing creates a signing key. Generate separate
keys for release approval and first-time creation. The target identity belongs
on the target machine. A creation key cannot decrypt a canonical age file or
approve a deployment artifact. Add recovery recipients and approval thresholds
to the shared catalog as your deployment requires.
Import the same public catalog module on other targets. The optional flake
catalog and configuration-framework adapter remain supported. Direct
nixSeal.administrators declarations override the adapter's catalog default.
Declarations enable nix-seal automatically; enable = false disables integration
explicitly. A single administrator catalog is selected automatically. With
multiple catalogs, set administrator to the intended name. An explicit
administrator = null preserves the advanced unscoped identity interface.
publicKey replaces the repeated identities.target.kind/public structure.
Use the existing identities interface for custom target identity IDs. Conflicting
public-key declarations fail evaluation. A target public key is required public
trust data; nix-seal does not read local keys or discover recipients at evaluation.
The default runtime identity is the existing Ed25519 SSH host key for NixOS and
nix-darwin, or ~/.ssh/id_ed25519 for Home Manager. The dedicated age-key example
above overrides that default. Neither path is read or generated by Nix evaluation.
Activation fails if the required key is absent or cannot decrypt the artifact.
Standalone modules set repositoryRoot once. The optional configuration-framework
adapter supplies the calling flake's root automatically. Directory options remain
relative to this root and can be configured once in a shared module. nix-seal
cannot infer a repository's custom storage layout from a target's name.
NixOS and nix-darwin derive the target ID from networking.hostName; Home
Manager defaults to home/<username>. Set targetId explicitly for multiple
standalone homes with the same username. Framework-supplied target names keep
their existing behavior. Cache locations and file permissions have platform
defaults; explicit values still take precedence. Native system files default
to root ownership and mode 0400, with group root on Linux and wheel on
macOS. Home Manager uses the profile username, and group staff on macOS or
the username on Linux. Configure another group when that group does not exist.
Create the missing values¶
Declare the secret and its templates before creating ciphertext. Missing
declared sources enter a separate creation plan. Templates waiting for those
sources appear in nixSeal.pendingTemplates, with the missing local names.
They cannot enter the activation plan until every field exists. A typo that
references an undeclared secret is an error, not a pending field.
From the checkout, export the target's creation plan and enter a value privately:
bootstrap=$(nix build --no-link --print-out-paths \
.#nixosConfigurations.server.config.nixSeal.bootstrapPlanFile)
nix-seal secret bootstrap complete \
--bootstrap-plan "$bootstrap" --secret token \
--authorizer-key /absolute/private/create.key --interactive
The command accepts a local name only when it identifies one pending secret.
Use the full canonical ID if the name is ambiguous. Enter finishes a single
line without storing the newline. For a multiline value, add --multiline and
finish with Ctrl-D. Values are hidden, bounded to 64 KiB, and excluded from
arguments and ordinary output. Automation can omit --interactive and supply
stdin through a protected pipe. Existing ciphertext is never overwritten.
Add the new .age file to Git's index so a Git-based flake can see it, then
evaluate the normal plan again. The template becomes ready automatically.
bootstrapPlanFile becomes null when no declared sources are missing. Export
nixSeal.planFile, run nix-seal template check --plan <plan>, and use the
existing signed provisioning workflow before activating the target. Creating
canonical ciphertext does not deploy it.
The plan JSON is generated by nix-seal from these Nix declarations. It is a
machine interface for validation and signing, not a configuration file to write
or keep synchronized. Templates require only the Nix declaration and optional
public .template file.
For later changes, secret edit uses an explicit editor in a private workspace;
secret batch handles mapped collections; generators and delegated creation
remain available. See the command reference and
operational runbooks.