Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function to generate a config file with out of band values #328472

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

ibizaman
Copy link

@ibizaman ibizaman commented Jul 19, 2024

Description of changes

The goal is to provide one official function, that is tested, that supports the majority of use cases. Those use cases are replace-secret, genJqSecretsReplacementSnippet and any module using a _secret field.

Currently, these use cases all have different implementations. They all work of course, but having a central function that is tested and works for all cases would be better for maintainability and re-usability. Indeed, other modules that currently do not support secrets will be easily able to.

This function has one additional feature compared to existing ones, it is generic in the config generator function, allowing one to generate a JSON, YAML, INI or whatever configuration file.

Concrete example

The following service definition:

systemd.services.outOfBand = {
  wantedBy = [ "sysinit.target" ];
  after = [ "sysinit.target" ];
  unitConfig.DefaultDependencies = false;
  serviceConfig.Type = "oneshot";
  serviceConfig.RemainAfterExit = true; # Allows wait_for_unit to work.
  serviceConfig.StateDirectory = "outOfBandConfig";

  outOfBandConfig = [{
    name = "myconfig.json";
    config = {
      a.a._secret = "/run/secrets/a-secret.txt";
      b._secret = "/run/secrets/b-secret.txt";
      b.prefix = "prefix-";
      b.suffix = "-suffix";
      c = "not secret C";
      d.d = "not secret D";
      e._secret = "/run/secrets/e-secret.txt";
      e.prefixIfNotPresent = "prefix-";
      e.suffixIfNotPresent = "-suffix";
    };
    generator = utils.genConfigOutOfBandGeneratorAdapter (lib.generators.toJSON { });
  }];
  serviceConfig.DynamicUser = true;
  script = ''
    echo $STATE_DIRECTORY/myconfig.json
    cat $STATE_DIRECTORY/myconfig.json
  '';
};

With the content of the secret files being:

aSecret -> "Secret of A";
bSecret -> "Secret of B";
eSecret -> "prefix-abc";

Produces the service:

[Unit]
After=sysinit.target
DefaultDependencies=false
[Service]
DynamicUser=true
ExecStart=/nix/store/2l8qay4wfjd3f35lk5npz2jrx0r3wgl5-unit-script-outOfBand-start/bin/outOfBand-start
ExecStartPre=/nix/store/myfrvbz29xa0vz1q785ivp23k6z6r4ir-unit-script-outOfBand-outOfBandConfig/bin/outOfBand-outOfBandConfig
LoadCredential=a_a:/run/secrets/a-secret.txt
LoadCredential=b:/run/secrets/b-secret.txt
LoadCredential=e:/run/secrets/e-secret.txt
RemainAfterExit=true
StateDirectory=outOfBandConfig
Type=oneshot
[Install]
WantedBy=sysinit.target;

(I removed the Environment= to avoid cluttering the output)

And cat /nix/store/myfrvbz29xa0vz1q785ivp23k6z6r4ir-unit-script-outOfBand-outOfBandConfig/bin/outOfBand-outOfBandConfig is:

#!/nix/store/i1x9sidnvhhbbha2zhgpxkhpysw6ajmr-bash-5.2p26/bin/bash
set -e
set -euo pipefail

test -d "$(dirname "$STATE_DIRECTORY/myconfig.json")" || mkdir -p "$(dirname "$STATE_DIRECTORY/myconfig.json")"
rm -f "$STATE_DIRECTORY/myconfig.json"
install -Dm600 "/nix/store/14445vijz8cbcja8lmdsm7w208amfll3-generator-template" "$STATE_DIRECTORY/myconfig.json"
/nix/store/sprys974vsv08pi82wa5sr93s7xpfzc8-replace-secret/bin/replace-secret \
  %SECRET_A_A% "$CREDENTIALS_DIRECTORY/a_a" "$STATE_DIRECTORY/myconfig.json" \
  --prefix="" \
  --suffix="" \
  --prefix-if-not-present="" \
  --suffix-if-not-present=""

/nix/store/sprys974vsv08pi82wa5sr93s7xpfzc8-replace-secret/bin/replace-secret \
  %SECRET_B% "$CREDENTIALS_DIRECTORY/b" "$STATE_DIRECTORY/myconfig.json" \
  --prefix="prefix-" \
  --suffix="-suffix" \
  --prefix-if-not-present="" \
  --suffix-if-not-present=""

/nix/store/sprys974vsv08pi82wa5sr93s7xpfzc8-replace-secret/bin/replace-secret \
  %SECRET_E% "$CREDENTIALS_DIRECTORY/e" "$STATE_DIRECTORY/myconfig.json" \
  --prefix="" \
  --suffix="" \
  --prefix-if-not-present="prefix-" \
  --suffix-if-not-present="-suffix"

With cat /nix/store/14445vijz8cbcja8lmdsm7w208amfll3-generator-template being:

{"a":{"a":"%SECRET_A_A%"},"b":"%SECRET_B%","c":"not secret C","d":{"d":"not secret D"},"e":"%SECRET_E%"}

The final configuration file is:

$ ls -l /var/lib/outOfBandConfig/myconfig.json
-rw------- 1 outOfBand outOfBand 125 Jul 30 20:40 /var/lib/outOfBandConfig/myconfig.json

$ cat /var/lib/outOfBandConfig/myconfig.json
{"a":{"a":"Secret of A"},"b":"prefix-Secret of B-suffix","c":"not secret C","d":{"d":"not secret D"},"e":"prefix-abc-suffix"}

Update existing code

I did a grep for replace-secret, genJqSecretsReplacementSnippet and _secret and compiled a list of modules that should/could be updated to use this instead. I already made some replacements - only for modules including tests - to prove that this implementation works in those cases. Hereunder is the compiled list of modules I found:

Modules using secrets
module test-file replace-secret genJqSecretsReplacementSnippet _secret updated
./nixos/modules/services/audio/mpd.nix
./nixos/modules/services/audio/mpdscribble.nix
./nixos/modules/services/continuous-integration/jenkins/job-builder.nix
./nixos/modules/services/games/archisteamfarm.nix
./nixos/modules/services/logging/filebeat.nix ./nixos/tests/elk.nix
./nixos/modules/services/mail/mailman.nix ./nixos/tests/mailman.nix
./nixos/modules/services/matrix/mjolnir.nix ./nixos/tests/matrix/mjolnir.nix
./nixos/modules/services/misc/forgejo.nix ./nixos/tests/forgejo.nix ./nixos/tests/renovate.nix
./nixos/modules/services/misc/geoipupdate.nix
./nixos/modules/services/misc/gitea.nix ./nixos/tests/gitea.nix
./nixos/modules/services/misc/gitlab.nix ./nixos/tests/gitlab.nix
./nixos/modules/services/monitoring/parsedmarc.nix ./nixos/tests/parsedmarc/default.nix
./nixos/modules/services/monitoring/ups.nix
./nixos/modules/services/networking/coturn.nix ./nixos/tests/coturn.nix
./nixos/modules/services/networking/ddclient.nix
./nixos/modules/services/networking/gns3-server.nix ./nixos/tests/gns3-server.nix
./nixos/modules/services/networking/netbird/coturn.nix ./nixos/tests/coturn.nix
./nixos/modules/services/networking/netbird/management.nix ./nixos/tests/netbird.nix
./nixos/modules/services/networking/sing-box.nix ./nixos/tests/sing-box.nix
./nixos/modules/services/web-apps/akkoma.nix ./nixos/tests/akkoma.nix
./nixos/modules/services/web-apps/artalk.nix ./nixos/tests/artalk.nix
./nixos/modules/services/web-apps/atlassian/confluence.nix
./nixos/modules/services/web-apps/atlassian/crowd.nix
./nixos/modules/services/web-apps/atlassian/jira.nix
./nixos/modules/services/web-apps/bookstack.nix
./nixos/modules/services/web-apps/davis.nix ./nixos/tests/davis.nix
./nixos/modules/services/web-apps/dex.nix ./nixos/tests/dex-oidc.nix
./nixos/modules/services/web-apps/discourse.nix ./nixos/tests/discourse.nix
./nixos/modules/services/web-apps/discourse.nix ./nixos/tests/discourse.nix
./nixos/modules/services/web-apps/engelsystem.nix ./nixos/tests/engelsystem.nix
./nixos/modules/services/web-apps/kavita.nix ./nixos/tests/kavita.nix
./nixos/modules/services/web-apps/keycloak.nix ./nixos/tests/keycloak.nix
./nixos/modules/services/web-apps/lemmy.nix ./nixos/tests/lemmy.nix
./nixos/modules/services/web-apps/monica.nix ./nixos/tests/web-apps/monica.nix
./nixos/modules/services/web-apps/ocis.nix ./nixos/tests/ocis.nix
./nixos/modules/services/web-apps/snipe-it.nix ./nixos/tests/web-apps/snipe-it.nix

Btw I have no idea how to best structure such a PR. Should I split it? Should I squash all commits? Something else?

Things done

  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandboxing enabled in nix.conf? (See Nix manual)
    • sandbox = relaxed
    • sandbox = true
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 24.11 Release Notes (or backporting 23.11 and 24.05 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

Add a 👍 reaction to pull requests you find important.

@github-actions github-actions bot added 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 6.topic: module system About "NixOS" module system internals 6.topic: lib The Nixpkgs function library labels Jul 19, 2024
@ibizaman
Copy link
Author

@ofborg test misc genConfigOutOfBand-test jackett snipe-it monica keycloak gitlab

@ibizaman ibizaman force-pushed the official_replace_secrets branch from f42c5f6 to 41815b9 Compare July 19, 2024 16:15
@ibizaman
Copy link
Author

@ofborg test misc genConfigOutOfBand-test jackett snipe-it monica keycloak gitlab

@ibizaman
Copy link
Author

Mmmh I don't know why I got this from ofborg:

The following builds were skipped because they don't evaluate on x86_64-linux: nixosTests.jackett, nixosTests.snipe-it

I don't see where in the code it says that platform is not supported for those tests. They succeed locally for me at least on that platform.

@ibizaman ibizaman force-pushed the official_replace_secrets branch from 41815b9 to 09a5b98 Compare July 19, 2024 21:10
@ambroisie ambroisie mentioned this pull request Jul 20, 2024
5 tasks
@ibizaman ibizaman force-pushed the official_replace_secrets branch from 09a5b98 to b9ee1ed Compare July 21, 2024 23:36
@ibizaman
Copy link
Author

@ofborg test misc
@ofborg test genConfigOutOfBand-test
@ofborg test jackett
@ofborg test snipe-it
@ofborg test monica
@ofborg test keycloak
@ofborg test gitlab

@ibizaman
Copy link
Author

@ofborg test filebeat

@ibizaman
Copy link
Author

@ofborg test kavita

${utils.genConfigOutOfBand {
config = cfg.settings;
configLocation = "/var/lib/filebeat/filebeat.yml";
generator = utils.genConfigOutOfBandFormatAdapter json;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be yaml? I assume the module used json due to genJqSecretsReplacementSnippet.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what, I tried YAML at first but it failed. It's because of the private key, it includes newlines. So with YAML, the newlines was creating a syntax error because the indentation was not respected. With JSON, the syntax is valid even with newlines because it includes opening and closing quotes. And anyway, JSON is a subset of YAML afterall.

That being said, another solution would be to enhance the replaceSecret.py script to have it replace "real" newlines with \n. That would be fully backwards compatible. I didn't do it because I don't want the replaceSecret.py script to have one argument per use case but could if you'd think that's better.

@ibizaman ibizaman marked this pull request as ready for review July 24, 2024 06:29
@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/thoughts-about-uniform-secrets-integration-in-nixos-modules/49623/3

@ibizaman ibizaman requested a review from tfc as a code owner July 29, 2024 21:54
@github-actions github-actions bot added the 6.topic: testing Tooling for automated testing of packages and modules label Jul 29, 2024
@ibizaman
Copy link
Author

@ofborg test genConfigOutOfBand-test

The goal is to provide one official function, that is tested, that supports the majority of use
cases. Those use cases are `replace-secret`, `genJqSecretsReplacementSnippet` and any module
using a `_secret` field.

Currently, these use cases have all different implementations. They all work of course, but having a
central function that works for all cases would be better for maintainability and re-usability.
Indeed, other modules that currently do not support secrets will be easily able to.

This function has one additional feature compared to existing ones, it is generic in the config
generator function, allowing one to generate a JSON, YAML, INI or whatever configuration file.
@ibizaman ibizaman force-pushed the official_replace_secrets branch from 30f989d to 6f9cbe8 Compare July 29, 2024 22:15
@ofborg ofborg bot removed 2.status: merge conflict This PR has merge conflicts with the target branch 10.rebuild-darwin: 1 labels Jul 29, 2024
@ibizaman ibizaman requested a review from a team as a code owner July 31, 2024 10:21
@ibizaman
Copy link
Author

@ofborg test genConfigOutOfBand-test
@ofborg test misc

@ibizaman ibizaman mentioned this pull request Jul 31, 2024
12 tasks
@ibizaman
Copy link
Author

ibizaman commented Jul 31, 2024

Copy link
Member

@SuperSandro2000 SuperSandro2000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely must be split up. Please create one PR for the lib changes with good commits where only one function is added. When that is merged we can add the core functionality and maybe convert a service. Then we can do a PR per service so that the respective maintainers can properly review them.

Also please follow the contributing guide when creating commits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit must be split into the respective commits before to have a clean history.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the lib functions are making good progress, but feel free to put a version of the functions you need in local let bindings with a reference to the PR URLs so that they don't need the level of scrutiny that we apply to lib changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instant facepalm. What a great idea! I should’ve done that from the start. 🎉

@SuperSandro2000 SuperSandro2000 marked this pull request as draft July 31, 2024 12:06
@ibizaman
Copy link
Author

ibizaman commented Jul 31, 2024

@SuperSandro2000 for sure. I wanted to see the final result first. To make sure all the pieces fall into place and gather feedback. Not sure how we do that here usually.

Just to make sure, when you say one PR for the lib, do you mean a single one for misc.nix, utils.nix and all other “internal” files?

@ibizaman ibizaman mentioned this pull request Jul 31, 2024
13 tasks
@ibizaman
Copy link
Author

ibizaman commented Jul 31, 2024

^ I started splitting this one into multiple PRs.

@ibizaman ibizaman mentioned this pull request Jul 31, 2024
13 tasks
ibizaman pushed a commit to ibizaman/nixpkgs that referenced this pull request Aug 15, 2024
This new 'recursiveCollect' function is like 'collect' while additionally descending into lists.

This function will be used with user-provided freeform variables - arbitrary combinations of
attrset, lists and values - in the upcoming secret management feature. The PR NixOS#328472 shows how it
will be used.
ibizaman pushed a commit to ibizaman/nixpkgs that referenced this pull request Aug 15, 2024
This new 'mapDataRecursiveCond' function is like 'mapAttrsRecursiveCond' and additionally recurses on lists.

This function will be used with user-provided freeform variables - arbitrary combinations of
attrset, lists and values - in the upcoming secret management feature. The PR NixOS#328472 shows how it
will be used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this support structured secrets? genJqSecretsReplacementSnippet supports that in #325217. See that PR for more info about what is structured secrets.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL this exists, what a great feature.

Copy link
Author

@ibizaman ibizaman Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work indeed! "Just" need to pass in the correct generator function.

I must say though that the reason I started hacking on this feature is actually because I did not want to let the secret files have any structure. Because that means the final user must get the structure right and there's no easy way to give a meaningful error message. The experience for getting the structure right can be frustrating as I experienced first hand.
So I started on this because I wanted the structure of the config file to be determined in Nix directly, and any secret files would then only contain the secret value itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should work indeed! "Just" need to pass in the correct generator function.

Hmm, I do not know any "correct" JSON generator function.


I do not mean this PR has to support structured secrets. I just want to add that supporting structured secrets does have its value. For example, you may consider an attr name (in Nix lang) a secret. For another example, the secret value may be a list of strings.

Copy link
Contributor

@ElvishJerricco ElvishJerricco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this difficult to review, and not just because of the parts that ought to be separate PRs. I don't have a clear understanding of:

  • Why we need this
  • Why it's so complex
  • Why it has to add options to systemd services
  • How this is better than what we already do

I think this needs a design document, bordering on the scale of an RFC, to define its motivation, design, and consideration of alternatives.

@ibizaman
Copy link
Author

@ElvishJerricco TBH I though about doing that before starting the PR but I didn’t think the PR would grow that big. It makes sense to have one now.

But also I wanted to use the PR as a testing ground. Personally I like to write code to get a feel for a feature. But that’s why the PR is still draft also.

I’ll explain in the RFC what’s the rationale between everything in the PR.

@wegank wegank added the 2.status: merge conflict This PR has merge conflicts with the target branch label Sep 10, 2024
@ibizaman
Copy link
Author

Just wanted to post an update. I couldn’t progress on this this last month but should be able to pick this up again in two weeks. I’m still fully invested in getting this across the finish line!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.status: merge conflict This PR has merge conflicts with the target branch 6.topic: lib The Nixpkgs function library 6.topic: module system About "NixOS" module system internals 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 6.topic: testing Tooling for automated testing of packages and modules 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 1-10 10.rebuild-linux: 1-10
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants