-
-
Notifications
You must be signed in to change notification settings - Fork 20k
Contract for files containing secrets #495303
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| { | ||
| config, | ||
| lib, | ||
| pkgs, | ||
| ... | ||
| }: | ||
| let | ||
| cfg = config.testing.hardcoded-secret; | ||
|
|
||
| inherit (lib) mapAttrs' mkOption nameValuePair; | ||
| inherit (lib.types) | ||
| attrsOf | ||
| str | ||
| submodule | ||
| ; | ||
| inherit (pkgs) writeText; | ||
| in | ||
| { | ||
| options.testing.hardcoded-secret = mkOption { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in practice, i think may be common for modules to have options not specific to a given instance like the if we were to add those later tho, i would imagine that in the case of this current module one would then want to move the current i'm not sure nixos has a great way of moving options down tho (renamed options are expected to not simultaneously exist as the new of a new option), so in that sense, i think it could be helpful to complicate this module like that already, maybe like my earlier example there. |
||
| default = { }; | ||
| description = '' | ||
| Hardcoded file secrets. These should only be used in tests. | ||
|
|
||
| They aim to replace the usage of pkgs.writeText in NixOS VM tests | ||
| as those make the file world readable | ||
| while this module set runtime permissions on the file. | ||
| This makes the tests more accurate, ensuring the permissions | ||
| set by the contract consumer are correct. | ||
| ''; | ||
| example = lib.literalExpression '' | ||
| { | ||
| mySecret = { | ||
| input = { | ||
| user = "me"; | ||
| mode = "0400"; | ||
| }; | ||
| content = "My Secret"; | ||
| }; | ||
| } | ||
| ''; | ||
| type = attrsOf ( | ||
| submodule ( | ||
| { name, ... }: | ||
| { | ||
| options = { | ||
| input = mkOption { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. adjust terminology here? (+ at stash) |
||
| description = "Input of the contract for file secrets."; | ||
| type = lib.types.submodule { | ||
| options = { | ||
| mode = mkOption { | ||
| description = '' | ||
| Mode the secret file must have. | ||
| ''; | ||
| type = str; | ||
| default = "0400"; | ||
| }; | ||
|
|
||
| owner = mkOption { | ||
| description = '' | ||
| Linux user that must own the secret file. | ||
| ''; | ||
| type = str; | ||
| default = "root"; | ||
| }; | ||
|
|
||
| group = mkOption { | ||
| description = '' | ||
| Linux group that must own the secret file. | ||
| ''; | ||
| type = str; | ||
| default = "root"; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| output = mkOption { | ||
| description = "Output of the contract for file secrets."; | ||
| default = { }; | ||
| type = lib.types.submodule { | ||
| options = { | ||
| path = mkOption { | ||
| type = str; | ||
| description = '' | ||
| Path to the file containing the secret generated out of band. | ||
|
|
||
| This path will exist after deploying to a target host, | ||
| it is not available through the nix store. | ||
| ''; | ||
| default = "/run/hardcodedsecrets/${name}"; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| content = mkOption { | ||
| type = str; | ||
| description = '' | ||
| Content of the secret as a string. | ||
|
|
||
| This will be stored in the nix store and should only be used for testing or maybe in dev. | ||
| ''; | ||
| }; | ||
| }; | ||
| } | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| config = { | ||
| system.activationScripts = mapAttrs' ( | ||
| n: cfg': | ||
| let | ||
| source = writeText "hardcodedsecret_${n}_content" cfg'.content; | ||
|
|
||
| inherit (cfg') input output; | ||
| in | ||
| nameValuePair "hardcodedsecret_${n}" '' | ||
| mkdir -p "$(dirname "${output.path}")" | ||
| touch "${output.path}" | ||
| chmod ${input.mode} "${output.path}" | ||
| chown ${input.owner}:${input.group} "${output.path}" | ||
| cp ${source} "${output.path}" | ||
| '' | ||
| ) cfg; | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { runTest }: | ||
| { | ||
| filesecrets-hardcoded-secret = runTest ./filesecrets/hardcoded-secret.nix; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| args@{ | ||
| lib, | ||
| config, | ||
| pkgs, | ||
| ... | ||
| }: | ||
| let | ||
| test = import ./test.nix args; | ||
| in | ||
| test { | ||
| name = "contracts-secrets-hardcoded-secret"; | ||
| providerRoot = [ | ||
| "testing" | ||
| "hardcoded-secret" | ||
| "mysecret" | ||
| ]; | ||
| extraModules = [ | ||
| ../../../modules/testing/hardcoded-secret.nix | ||
| ( | ||
| { config, ... }: | ||
| { | ||
| testing.hardcoded-secret.mysecret.content = config.test.content; | ||
| } | ||
| ) | ||
| ]; | ||
| } | ||
| // { | ||
| meta.maintainers = [ lib.maintainers.ibizaman ]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| { | ||
| lib, | ||
| config, | ||
| pkgs, | ||
| ... | ||
| }: | ||
| let | ||
| inherit (lib) getAttrFromPath setAttrByPath; | ||
| inherit (lib) mkOption types; | ||
| in | ||
| { | ||
| name, | ||
| providerRoot, | ||
| extraModules ? [ ], | ||
| }: | ||
| { | ||
| name = "contracts_filesecrets_${name}"; | ||
|
|
||
| nodes.machine = | ||
| { config, ... }: | ||
| { | ||
| imports = extraModules; | ||
|
|
||
| options.test = { | ||
| owner = mkOption { | ||
| type = types.str; | ||
| default = "root"; | ||
| }; | ||
|
|
||
| group = mkOption { | ||
| type = types.str; | ||
| default = "root"; | ||
| }; | ||
|
|
||
| mode = mkOption { | ||
| type = types.str; | ||
| default = "0400"; | ||
| }; | ||
|
|
||
| content = mkOption { | ||
| type = types.str; | ||
| default = "a super secret secret!"; | ||
| }; | ||
| }; | ||
|
|
||
| config = lib.mkMerge [ | ||
| (setAttrByPath providerRoot { | ||
| input = { | ||
| inherit (config.test) owner group mode; | ||
| }; | ||
| }) | ||
| (lib.mkIf (config.test.owner != "root") { | ||
| users.users.${config.test.owner}.isNormalUser = true; | ||
| }) | ||
| (lib.mkIf (config.test.group != "root") { | ||
| users.groups.${config.test.group} = { }; | ||
| }) | ||
| ]; | ||
| }; | ||
|
|
||
| testScript = | ||
| { nodes, ... }: | ||
| let | ||
| cfg = nodes.machine; | ||
| inherit (getAttrFromPath providerRoot nodes.machine) output; | ||
| in | ||
| '' | ||
| owner = machine.succeed("stat -c '%U' ${output.path}").strip() | ||
| print(f"Got owner {owner}") | ||
| if owner != "${cfg.test.owner}": | ||
| raise Exception(f"Owner should be '${cfg.test.owner}' but got '{owner}'") | ||
|
|
||
| group = machine.succeed("stat -c '%G' ${output.path}").strip() | ||
| print(f"Got group {group}") | ||
| if group != "${cfg.test.group}": | ||
| raise Exception(f"Group should be '${cfg.test.group}' but got '{group}'") | ||
|
|
||
| mode = str(int(machine.succeed("stat -c '%a' ${output.path}").strip())) | ||
| print(f"Got mode {mode}") | ||
| wantedMode = str(int("${cfg.test.mode}")) | ||
| if mode != wantedMode: | ||
| raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") | ||
|
|
||
| content = machine.succeed("cat ${output.path}").strip() | ||
| print(f"Got content {content}") | ||
| if content != "${cfg.test.content}": | ||
| raise Exception(f"Content should be '${cfg.test.content}' but got '{content}'") | ||
| ''; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from what i can tell the submodules look similar, with this one also providing
defaultvalues.would it cause recursion problems if such a module were just exposed in some mutually accessible location, be it at e.g.
contracts.secrets?options.contractscould be an option taking typeattrsOf deferredModuleor the like, so that on theconfigside we could then plug in your shared submodule forcontract.secrets, then access that from both places that got it now.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree but isn't this one more implementation competing with the other two big PRs? Not saying we shouldn't think about it but TBH I purposely kept that out of this PR to focus on the contract for secrets. Unless a good argument for, I'd rather not introduce this complexity in the PR.
My hope by doing it like this is to create a handful of contracts, use them in a handful of places and then we can think of an underlying implementation with concrete use cases.
Note also that on one side, we set default values on the consumer side of the contract and on the other on the provider side of the contract. So both sides are not exactly the same although the overall shape is.