Add secretspec support - #16300
Conversation
Nix credentials currently have to be stored as literal configuration values. This makes long-lived forge tokens, netrc contents, and build secrets visible to configuration introspection and harder to manage through external secret providers. Allow these credentials to be referenced by SecretSpec name and resolved only when used. Support GitHub, GitLab, and SourceHut access tokens with the existing host and path-prefix matching, complete netrc files, and impure environment variables requested by fixed-output derivations. Preserve compatibility by preferring literal values for equally specific access-token mappings and equally named impure environment variables. SecretSpec netrc files take precedence over netrc-file. Prevent flakes from configuring credential-bearing settings, even when flake configuration is accepted. Propagate the requesting client trust level through local and remote builds so untrusted daemon clients cannot consume daemon-configured SecretSpec impure environment mappings. Make SecretSpec support optional at build time so libstore remains available where secretspec-ffi cannot be built. Closes: NixOS#6536 Related: NixOS#8635 Related: NixOS#6942 Assisted-by: Claude Code (claude-opus-5) Assisted-by: Codex (GPT-5)
|
We could even go further to provide helpers for migration from existing files, but that would need a bit more work. [project]
name = "nix"
revision = "1.0"
# `system` is intentionally supplied by the machine's global SecretSpec config.
# This alias exposes the existing /etc/nix/netrc as a migration source.
[providers]
legacy_netrc = "file:///etc/nix"
[profiles.default]
NIX_NETRC = {
description = "Complete netrc file used by Nix",
required = false,
as_path = true,
providers = ["system", "legacy_netrc"],
refs = {
legacy_netrc = { item = "netrc" },
},
}and then migrate: $ secretspec \
--file /etc/nix/secretspec.toml \
--profile default \
import legacy_netrc \
--delete-sourceI'll add |
xokdvium
left a comment
There was a problem hiding this comment.
Lots of very questionable things here.
| /* Keep every resolved context alive so an in-flight user never observes an | ||
| `as_path` file being deleted after a configuration change. */ | ||
| mutable Sync<std::map<SecretSpecRequest, std::shared_ptr<SecretSpecCache>>> _caches; | ||
|
|
||
| /* Serializes resolution so that a request is resolved only once, without | ||
| blocking cache lookups for the duration of the resolution. */ | ||
| mutable std::mutex _resolveMutex; |
There was a problem hiding this comment.
Eh, I'm feeling like a parrot at this point. No, please. No more global mutable state until we figure out how to not rely on globals for things.
Having global credentials (especially stuffed into the settings systems) seems like a disaster waiting to happen.
| if (globalConfig.getFlakeConfigSetting(baseName) == FlakeConfigSetting::Forbidden) { | ||
| warn("ignoring flake configuration setting '%s' because it is not allowed to be set by flakes", name); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Alternatively, can we remove nixConfig ? :)
I don't think this makes much sense considering how nixConfig now can RCE you pretty trivially.
| { | ||
| std::string value; | ||
| std::string description; | ||
| FlakeConfigSetting flakeConfigSetting; |
There was a problem hiding this comment.
Seems like a huge layer violation to pollute libutil with flakes.
| static const ResolvedSecret & getSecret(const SecretSpecCache & cache, const std::string & name) | ||
| { | ||
| if (auto error = cache.errors.find(name); error != cache.errors.end()) | ||
| std::rethrow_exception(error->second); |
There was a problem hiding this comment.
This will lead to data races and mutation of the refcounted exception. This is not how to rethrow shared errors. The trace system mutates them.
| Worker(Store & store, Store & evalStore); | ||
| Worker(Store & store, Store & evalStore, TrustedFlag requestTrusted = Trusted); |
There was a problem hiding this comment.
Seeing how we'd love to make the worker less ephemeral and properly reusable this seems like the wrong abstraction layer.
Also the default argument seems very footgunny if the idea is that it's security relevant?
| { | ||
| copyDrvsFromEvalStore(drvPaths); | ||
| auto conn(store->getConnection()); | ||
| setOptions(*conn); |
| std::string SecretSpecSettings::defaultFile() | ||
| { | ||
| return NIX_SECRETSPEC_FILE; | ||
| } | ||
|
|
There was a problem hiding this comment.
Hm, usually nix (in static builds) does not rely on non-embedded data.
Or at least we try to. Seems like that would break that?
|
Tbh I think we should have a better way to abstract around credential providers, so that it's much more hidden away and sits behind a consistent interface. That should hopefully allow us to pay off the existing tech debt around credentials too. |
|
Linking nix against 27 cloud SDKs transitively sounds like a terrible idea. We just made a lot of effort getting rid of the aws SDK dependency. Keeping nix’s dependency footprint small is useful for static linking, and trust bootstrapping. Why not have an interface like varlink over Unix domain sockets for credential brokerage? So that nix can talk to some secretspec credential daemon? that way we don’t explode the build time dependency footprint of nix. Credentials are a runtime concern; move it to a runtime daemon. This is also what https://spiffe.io does (albeit gRPC; not varlink) I started a similar discussion here systemd/systemd#40469 but iirc there are also similar discussions happening in gnome another option would be exploring introducing this as a nix plugin? though personally I’m very much in IPC > shared library camp for plugin-shaped things. |
|
iiuc, the fix to store leakage here could be a simple as having the interface be a file (or what @arianvp said) rather than the secret itself? |
Ditto to this, I have a weird personal secrets thing (nothing pre-existing worked well for my LUKS setup after scripted stage1 got deprecated) and I'd rather just plug it in to some extension point rather than be forced to migrate to something else that may or may not support my use-case. It's also probably good to keep options open until the ecosystem at large converges on one option, instead of blessing an arbitrary one. |
|
Thanks everyone for feedback, I'll try to incorporate it in next iteration. Draft for Resolver and Provider IPC for secretspec is ready. Feedback welcome! Provider allows talking to secretspec cli without bringing in huge dependency chain, like for Nix. Provider IPC allows you to build your own provider! |
Could you share what's missing bit? I'm happy to integrate that into our proposal for IPC.
I'm going to rework the PR so it integrates secretstpec via IPC but leaves options open for other protocols to co-exist. That way we can improve our protocol given the feedback while allowing anyone else to add their own. |
|
I've split out secrets abstraction inside Nix into #16339 where I explain why it's better to have a generic interface inside instead of #9857 / #16087 Once I'm happy with IPC (reviews welcome) inside secretspec, I'll redo this PR as requested by many. That should also get rid of the other review raised, all globals being removed (which #957 btw has two). |
|
This pull request has been mentioned on NixOS Discourse. There might be relevant details there: https://discourse.nixos.org/t/secrets-don-t-belong-in-config/79078/40 |
|
This pull request has been mentioned on NixOS Discourse. There might be relevant details there: |
Nix credentials currently have to be stored as literal configuration values. This makes long-lived forge tokens, netrc contents, and build secrets visible to configuration introspection and harder to manage through external secret providers.
I've written about this in secrets don't belong in config.
This PR introduces first-class support for https://secretspec.dev (akin to terraform) that comes currently with support for 30 secrets providers via
pkgs.secretspec-ffi.Nix ships with its own
secretspec.tomlthat can be overiden for customization viasecretspec-file.It also comes with an extensive test suite.
How it works
SecretSpec separates secret declarations from secret values:
secretspec.tomldeclares the available secret names and whether eachsecret is an inline value or a materialized file.
secretspec-provider,secretspec-profile, andsecretspec-scopeselect the resolution context.
Closes: #6536
Related: #8635
Related: #6942