mirror of
https://github.com/Mic92/sops-nix.git
synced 2025-12-26 14:14:58 +08:00
Merge branch 'master' of github.com:Mic92/sops-nix into nested-secrets
Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
This commit is contained in:
commit
d953133e37
43 changed files with 2962 additions and 1188 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use flake
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@V27
|
||||
- uses: cachix/install-nix-action@v30
|
||||
- name: Add keys group (needed for go tests)
|
||||
run: sudo groupadd keys
|
||||
- name: Run unit tests
|
||||
|
|
|
|||
2
.github/workflows/update-vendor-hash.yml
vendored
2
.github/workflows/update-vendor-hash.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@V27
|
||||
uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
|
|
|||
21
.github/workflows/upgrade-flakes.yml
vendored
21
.github/workflows/upgrade-flakes.yml
vendored
|
|
@ -4,19 +4,26 @@ on:
|
|||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '51 2 * * 0'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@V27
|
||||
uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Update flake.lock
|
||||
uses: DeterminateSystems/update-flake-lock@v24
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
|
||||
pr-labels: |
|
||||
merge-queue
|
||||
- name: Update flakes
|
||||
run: |
|
||||
nix flake update
|
||||
pushd dev/private
|
||||
nix flake update
|
||||
popd
|
||||
nix run .#update-dev-private-narHash
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
|
|
|
|||
18
.mergify.yml
18
.mergify.yml
|
|
@ -1,16 +1,14 @@
|
|||
queue_rules:
|
||||
- name: default
|
||||
merge_conditions:
|
||||
- check-success=buildbot/nix-eval
|
||||
defaults:
|
||||
actions:
|
||||
queue:
|
||||
merge_method: rebase
|
||||
pull_request_rules:
|
||||
- name: merge using the merge queue
|
||||
conditions:
|
||||
queue_conditions:
|
||||
- base=master
|
||||
- label~=merge-queue|dependencies
|
||||
merge_conditions:
|
||||
- check-success=buildbot/nix-build
|
||||
merge_method: rebase
|
||||
|
||||
pull_request_rules:
|
||||
- name: refactored queue action rule
|
||||
conditions: []
|
||||
actions:
|
||||
queue:
|
||||
|
||||
|
|
|
|||
66
README.md
66
README.md
|
|
@ -674,8 +674,7 @@ JSON/YAML files. Unlike the other two formats, for binary files, one file corres
|
|||
To encrypt an binary file use the following command:
|
||||
|
||||
``` console
|
||||
$ cp /etc/krb5/krb5.keytab krb5.keytab
|
||||
$ sops -e krb5.keytab
|
||||
$ sops -e /etc/krb5/krb5.keytab > krb5.keytab
|
||||
# an example of what this might result in:
|
||||
$ head krb5.keytab
|
||||
{
|
||||
|
|
@ -708,6 +707,44 @@ This is how it can be included in your `configuration.nix`:
|
|||
}
|
||||
```
|
||||
|
||||
## Emit plain file for yaml and json formats
|
||||
|
||||
By default, sops-nix extracts a single key from yaml and json files. If you
|
||||
need the plain file instead of extracting a specific key from the input document,
|
||||
you can set `key` to an empty string.
|
||||
|
||||
For example, the input document `my-config.yaml` likes this:
|
||||
|
||||
```yaml
|
||||
my-secret1: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str]
|
||||
my-secret2: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
...
|
||||
```
|
||||
|
||||
This is how it can be included in your NixOS module:
|
||||
|
||||
```nix
|
||||
{
|
||||
sops.secrets.my-config = {
|
||||
format = "yaml";
|
||||
sopsFile = ./my-config.yaml;
|
||||
key = "";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Then, it will be mounted as `/run/secrets/my-config`:
|
||||
|
||||
```yaml
|
||||
my-secret1: hello
|
||||
my-secret2: hello
|
||||
```
|
||||
|
||||
## Use with home manager
|
||||
|
||||
sops-nix also provides a home-manager module.
|
||||
|
|
@ -786,6 +823,31 @@ The secrets are decrypted in a systemd user service called `sops-nix`, so other
|
|||
}
|
||||
```
|
||||
|
||||
### Qubes Split GPG support
|
||||
|
||||
If you are using Qubes with the [Split GPG](https://www.qubes-os.org/doc/split-gpg),
|
||||
then you can configure sops to utilize the `qubes-gpg-client-wrapper` with the `sops.gnupg.qubes-split-gpg` options.
|
||||
The example above updated looks like this:
|
||||
```nix
|
||||
{
|
||||
sops = {
|
||||
gnupg.qubes-split-gpg = {
|
||||
enable = true;
|
||||
domain = "vault-gpg";
|
||||
};
|
||||
defaultSopsFile = ./secrets.yaml;
|
||||
secrets.test = {
|
||||
# sopsFile = ./secrets.yml.enc; # optionally define per-secret files
|
||||
|
||||
# %r gets replaced with a runtime directory, use %% to specify a '%'
|
||||
# sign. Runtime dir is $XDG_RUNTIME_DIR on linux and $(getconf
|
||||
# DARWIN_USER_TEMP_DIR) on darwin.
|
||||
path = "%r/test.txt";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Use with GPG instead of SSH keys
|
||||
|
||||
If you prefer having a separate GPG key, sops-nix also comes with a helper tool, `sops-init-gpg-key`:
|
||||
|
|
|
|||
11
checks/darwin.nix
Normal file
11
checks/darwin.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
{
|
||||
imports = [
|
||||
../modules/nix-darwin/default.nix
|
||||
];
|
||||
documentation.enable = false;
|
||||
sops.secrets.test_key = { };
|
||||
sops.defaultSopsFile = ../pkgs/sops-install-secrets/test-assets/secrets.yaml;
|
||||
sops.age.generateKey = true;
|
||||
system.stateVersion = 5;
|
||||
}
|
||||
15
checks/home-manager.nix
Normal file
15
checks/home-manager.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
{ config, ... }: {
|
||||
imports = [
|
||||
../modules/home-manager/sops.nix
|
||||
];
|
||||
home.stateVersion = "25.05";
|
||||
home.username = "sops-user";
|
||||
home.homeDirectory = "/home/sops-user";
|
||||
home.enableNixpkgsReleaseCheck = false;
|
||||
|
||||
sops.age.generateKey = true;
|
||||
sops.age.keyFile = "${config.home.homeDirectory}/.age-key.txt";
|
||||
sops.secrets.test_key = { };
|
||||
sops.defaultSopsFile = ../pkgs/sops-install-secrets/test-assets/secrets.yaml;
|
||||
}
|
||||
596
checks/nixos-test.nix
Normal file
596
checks/nixos-test.nix
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
{ lib, testers }:
|
||||
let
|
||||
testAssets = ../pkgs/sops-install-secrets/test-assets;
|
||||
|
||||
userPasswordTest =
|
||||
name: extraConfig:
|
||||
testers.runNixOSTest {
|
||||
inherit name;
|
||||
nodes.machine =
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
../modules/sops
|
||||
extraConfig
|
||||
];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets.test_key.neededForUsers = true;
|
||||
secrets."nested/test/file".owner = "example-user";
|
||||
};
|
||||
system.switch.enable = true;
|
||||
|
||||
users.users.example-user = lib.mkMerge [
|
||||
(lib.mkIf (!config.systemd.sysusers.enable) {
|
||||
isNormalUser = true;
|
||||
hashedPasswordFile = config.sops.secrets.test_key.path;
|
||||
})
|
||||
(lib.mkIf config.systemd.sysusers.enable {
|
||||
isSystemUser = true;
|
||||
group = "users";
|
||||
hashedPasswordFile = config.sops.secrets.test_key.path;
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
testScript =
|
||||
''
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
|
||||
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
|
||||
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
|
||||
assert user == "example-user", f"Expected 'example-user', got '{user}'"
|
||||
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
|
||||
|
||||
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
|
||||
''
|
||||
+ lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
|
||||
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
ssh-keys = testers.runNixOSTest {
|
||||
name = "sops-ssh-keys";
|
||||
nodes.server =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
services.openssh.enable = true;
|
||||
services.openssh.hostKeys = [
|
||||
{
|
||||
type = "rsa";
|
||||
bits = 4096;
|
||||
path = testAssets + "/ssh-key";
|
||||
}
|
||||
];
|
||||
sops.defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
sops.secrets.test_key = { };
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
server.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
};
|
||||
|
||||
pruning = testers.runNixOSTest {
|
||||
name = "sops-pruning";
|
||||
nodes.machine =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets.test_key = { };
|
||||
keepGenerations = lib.mkDefault 0;
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
specialisation.pruning.configuration.sops.keepGenerations = 10;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
# Force us to generation 100
|
||||
machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric")
|
||||
machine.succeed("ln -fsn /run/secrets.d/99 /run/secrets")
|
||||
machine.succeed("/run/current-system/activate")
|
||||
machine.succeed("test -d /run/secrets.d/100")
|
||||
|
||||
# Ensure nothing is pruned, these are just random numbers
|
||||
machine.succeed("test -d /run/secrets.d/1")
|
||||
machine.succeed("test -d /run/secrets.d/90")
|
||||
machine.succeed("test -d /run/secrets.d/non-numeric")
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/pruning/bin/switch-to-configuration test")
|
||||
print(machine.succeed("ls -la /run/secrets.d/"))
|
||||
|
||||
# Ensure stuff was properly pruned.
|
||||
# We are now at generation 101 so 92 must exist when we keep 10 generations
|
||||
# and 91 must not.
|
||||
machine.fail("test -d /run/secrets.d/91")
|
||||
machine.succeed("test -d /run/secrets.d/92")
|
||||
machine.succeed("test -d /run/secrets.d/non-numeric")
|
||||
'';
|
||||
};
|
||||
|
||||
age-keys = testers.runNixOSTest {
|
||||
name = "sops-age-keys";
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets = {
|
||||
test_key = { };
|
||||
|
||||
test_key_someuser_somegroup = {
|
||||
uid = config.users.users."someuser".uid;
|
||||
gid = config.users.groups."somegroup".gid;
|
||||
key = "test_key";
|
||||
};
|
||||
test_key_someuser_root = {
|
||||
uid = config.users.users."someuser".uid;
|
||||
key = "test_key";
|
||||
};
|
||||
test_key_root_root = {
|
||||
key = "test_key";
|
||||
};
|
||||
test_key_1001_1001 = {
|
||||
uid = 1001;
|
||||
gid = 1001;
|
||||
key = "test_key";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
users.users."someuser" = {
|
||||
uid = 1000;
|
||||
group = "somegroup";
|
||||
isNormalUser = true;
|
||||
};
|
||||
users.groups."somegroup" = {
|
||||
gid = 1000;
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
|
||||
with subtest("test ownership"):
|
||||
machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_somegroup) = '1000' ]")
|
||||
machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_somegroup) = '1000' ]")
|
||||
machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_somegroup) = 'someuser' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_somegroup) = 'somegroup' ]")
|
||||
|
||||
machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_root) = '1000' ]")
|
||||
machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_root) = '0' ]")
|
||||
machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_root) = 'someuser' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_root) = 'root' ]")
|
||||
|
||||
machine.succeed("[ $(stat -c%u /run/secrets/test_key_1001_1001) = '1001' ]")
|
||||
machine.succeed("[ $(stat -c%g /run/secrets/test_key_1001_1001) = '1001' ]")
|
||||
machine.succeed("[ $(stat -c%U /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]")
|
||||
'';
|
||||
};
|
||||
|
||||
age-ssh-keys = testers.runNixOSTest {
|
||||
name = "sops-age-ssh-keys";
|
||||
nodes.machine = {
|
||||
imports = [ ../modules/sops ];
|
||||
services.openssh.enable = true;
|
||||
services.openssh.hostKeys = [
|
||||
{
|
||||
type = "ed25519";
|
||||
path = testAssets + "/ssh-ed25519-key";
|
||||
}
|
||||
];
|
||||
|
||||
sops = {
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets.test_key = { };
|
||||
# Generate a key and append it to make sure it appending doesn't break anything
|
||||
age = {
|
||||
keyFile = "/tmp/testkey";
|
||||
generateKey = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
};
|
||||
|
||||
pgp-keys = testers.runNixOSTest {
|
||||
name = "sops-pgp-keys";
|
||||
nodes.server =
|
||||
{ lib, config, ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
|
||||
users.users.someuser = {
|
||||
isSystemUser = true;
|
||||
group = "nogroup";
|
||||
};
|
||||
|
||||
sops.gnupg.home = "/run/gpghome";
|
||||
sops.defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
sops.secrets.test_key.owner = config.users.users.someuser.name;
|
||||
sops.secrets."nested/test/file".owner = config.users.users.someuser.name;
|
||||
sops.secrets.existing-file = {
|
||||
key = "test_key";
|
||||
path = "/run/existing-file";
|
||||
};
|
||||
# must run before sops
|
||||
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
|
||||
cp -r ${testAssets + "/gnupghome"} /run/gpghome
|
||||
chmod -R 700 /run/gpghome
|
||||
|
||||
touch /run/existing-file
|
||||
'';
|
||||
# Useful for debugging
|
||||
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
|
||||
#environment.variables = {
|
||||
# GNUPGHOME = "/run/gpghome";
|
||||
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
|
||||
# SOPSFILE = "${testAssets + "/secrets.yaml"}";
|
||||
#};
|
||||
};
|
||||
testScript = ''
|
||||
def assertEqual(exp: str, act: str) -> None:
|
||||
if exp != act:
|
||||
raise Exception(f"{exp!r} != {act!r}")
|
||||
|
||||
|
||||
start_all()
|
||||
|
||||
value = server.succeed("cat /run/secrets/test_key")
|
||||
assertEqual("test_value", value)
|
||||
|
||||
server.succeed("runuser -u someuser -- cat /run/secrets/test_key >&2")
|
||||
value = server.succeed("cat /run/secrets/nested/test/file")
|
||||
assertEqual(value, "another value")
|
||||
|
||||
target = server.succeed("readlink -f /run/existing-file")
|
||||
assertEqual("/run/secrets.d/1/existing-file", target.strip())
|
||||
'';
|
||||
};
|
||||
|
||||
templates = testers.runNixOSTest {
|
||||
name = "sops-templates";
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets.test_key = { };
|
||||
|
||||
# Verify that things work even with `neededForUsers` secrets. See
|
||||
# <https://github.com/Mic92/sops-nix/issues/659>.
|
||||
secrets."nested/test/file".neededForUsers = true;
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
sops.templates.test_template = {
|
||||
content = ''
|
||||
This line is not modified.
|
||||
The next value will be replaced by ${config.sops.placeholder.test_key}
|
||||
This line is also not modified.
|
||||
'';
|
||||
mode = "0400";
|
||||
owner = "someuser";
|
||||
group = "somegroup";
|
||||
};
|
||||
sops.templates.test_default = {
|
||||
content = ''
|
||||
Test value: ${config.sops.placeholder.test_key}
|
||||
'';
|
||||
path = "/etc/externally/linked";
|
||||
};
|
||||
|
||||
users.groups.somegroup = { };
|
||||
users.users.someuser = {
|
||||
isSystemUser = true;
|
||||
group = "somegroup";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
def assertEqual(exp: str, act: str) -> None:
|
||||
if exp != act:
|
||||
raise Exception(f"{exp!r} != {act!r}")
|
||||
|
||||
|
||||
start_all()
|
||||
machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_template) = 'someuser' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_template) = 'somegroup' ]")
|
||||
machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_default) = 'root' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_default) = 'root' ]")
|
||||
|
||||
expected = """\
|
||||
This line is not modified.
|
||||
The next value will be replaced by test_value
|
||||
This line is also not modified.
|
||||
"""
|
||||
rendered = machine.succeed("cat /run/secrets/rendered/test_template")
|
||||
|
||||
expected_default = """\
|
||||
Test value: test_value
|
||||
"""
|
||||
rendered_default = machine.succeed("cat /run/secrets/rendered/test_default")
|
||||
|
||||
assertEqual(expected, rendered)
|
||||
assertEqual(expected_default, rendered_default)
|
||||
|
||||
# Confirm that `test_default` was symlinked to the appropriate place.
|
||||
realpath = machine.succeed("realpath /etc/externally/linked").strip()
|
||||
assertEqual(realpath, "/run/secrets.d/1/rendered/test_default")
|
||||
'';
|
||||
};
|
||||
|
||||
restart-and-reload = testers.runNixOSTest {
|
||||
name = "sops-restart-and-reload";
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ ../modules/sops ];
|
||||
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = testAssets + "/secrets.yaml";
|
||||
secrets.test_key = {
|
||||
restartUnits = [
|
||||
"restart-unit.service"
|
||||
"reload-unit.service"
|
||||
];
|
||||
reloadUnits = [ "reload-trigger.service" ];
|
||||
};
|
||||
|
||||
templates.test_template = {
|
||||
content = ''
|
||||
this is a template with
|
||||
a secret: ${config.sops.placeholder.test_key}
|
||||
'';
|
||||
restartUnits = [
|
||||
"restart-unit.service"
|
||||
"reload-unit.service"
|
||||
];
|
||||
reloadUnits = [ "reload-trigger.service" ];
|
||||
};
|
||||
};
|
||||
system.switch.enable = true;
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
systemd.services."restart-unit" = {
|
||||
description = "Restart unit";
|
||||
# not started on boot
|
||||
serviceConfig = {
|
||||
ExecStart = "/bin/sh -c 'echo ok > /restarted'";
|
||||
};
|
||||
};
|
||||
systemd.services."reload-unit" = {
|
||||
description = "Reload unit";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
reloadIfChanged = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "/bin/sh -c true";
|
||||
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
|
||||
};
|
||||
};
|
||||
systemd.services."reload-trigger" = {
|
||||
description = "Reload trigger unit";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "/bin/sh -c true";
|
||||
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
testScript = ''
|
||||
def assertOutput(output, *expected_lines):
|
||||
expected_lines = list(expected_lines)
|
||||
|
||||
# Remove unrelated fluff that shows up in the output of `switch-to-configuration`.
|
||||
prefix = "setting up /etc...\n"
|
||||
if output.startswith(prefix):
|
||||
output = output.removeprefix(prefix)
|
||||
|
||||
actual_lines = output.splitlines(keepends=False)
|
||||
|
||||
if actual_lines != expected_lines:
|
||||
raise Exception(f"{actual_lines} != {expected_lines}")
|
||||
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.fail("test -f /restarted")
|
||||
machine.fail("test -f /reloaded")
|
||||
|
||||
# Nothing is to be restarted after boot
|
||||
machine.fail("ls /run/nixos/*-list")
|
||||
|
||||
# Nothing happens when the secret is not changed
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.fail("test -f /restarted")
|
||||
machine.fail("test -f /reloaded")
|
||||
|
||||
# Ensure the secret is changed
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
|
||||
# The secret is changed, now something should happen
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
|
||||
# Ensure something happened
|
||||
machine.succeed("test -f /restarted")
|
||||
machine.succeed("test -f /reloaded")
|
||||
|
||||
# Cleanup the marker files.
|
||||
machine.succeed("rm /restarted /reloaded")
|
||||
|
||||
# Ensure the template is changed
|
||||
machine.succeed(": > /run/secrets/rendered/test_template")
|
||||
|
||||
# The template is changed, now something should happen
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
|
||||
# Ensure something happened
|
||||
machine.succeed("test -f /restarted")
|
||||
machine.succeed("test -f /reloaded")
|
||||
|
||||
# Cleanup the marker files.
|
||||
machine.succeed("rm /restarted /reloaded")
|
||||
|
||||
with subtest("change detection"):
|
||||
machine.succeed("rm /run/secrets/test_key")
|
||||
machine.succeed("rm /run/secrets/rendered/test_template")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
assertOutput(
|
||||
out,
|
||||
"adding secret: test_key",
|
||||
"adding rendered secret: test_template",
|
||||
)
|
||||
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
machine.succeed(": > /run/secrets/rendered/test_template")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
assertOutput(
|
||||
out,
|
||||
"modifying secret: test_key",
|
||||
"modifying rendered secret: test_template",
|
||||
)
|
||||
|
||||
machine.succeed(": > /run/secrets/another_key")
|
||||
machine.succeed(": > /run/secrets/rendered/another_template")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
assertOutput(
|
||||
out,
|
||||
"removing secret: another_key",
|
||||
"removing rendered secret: another_template",
|
||||
)
|
||||
|
||||
with subtest("dry activation"):
|
||||
machine.succeed("rm /run/secrets/test_key")
|
||||
machine.succeed("rm /run/secrets/rendered/test_template")
|
||||
machine.succeed(": > /run/secrets/another_key")
|
||||
machine.succeed(": > /run/secrets/rendered/another_template")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
|
||||
assertOutput(
|
||||
out,
|
||||
"would add secret: test_key",
|
||||
"would remove secret: another_key",
|
||||
"would add rendered secret: test_template",
|
||||
"would remove rendered secret: another_template",
|
||||
)
|
||||
|
||||
# Verify that we did not actually activate the new configuration.
|
||||
machine.fail("test -f /run/secrets/test_key")
|
||||
machine.fail("test -f /run/secrets/rendered/test_template")
|
||||
machine.succeed("test -f /run/secrets/another_key")
|
||||
machine.succeed("test -f /run/secrets/rendered/another_template")
|
||||
|
||||
# Now actually activate and sanity check the resulting secrets.
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.succeed("test -f /run/secrets/test_key")
|
||||
machine.succeed("test -f /run/secrets/rendered/test_template")
|
||||
machine.fail("test -f /run/secrets/another_key")
|
||||
machine.fail("test -f /run/secrets/rendered/another_template")
|
||||
|
||||
# Remove the restarted/reloaded indicators so we can confirm a
|
||||
# dry-activate doesn't trigger systemd units.
|
||||
machine.succeed("rm /restarted /reloaded")
|
||||
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
|
||||
assertOutput(
|
||||
out,
|
||||
"would modify secret: test_key",
|
||||
)
|
||||
machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]")
|
||||
|
||||
machine.fail("test -f /restarted") # not done in dry mode
|
||||
machine.fail("test -f /reloaded") # not done in dry mode
|
||||
'';
|
||||
};
|
||||
|
||||
user-passwords = userPasswordTest "sops-user-passwords" {
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") {
|
||||
user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
systemd.sysusers.enable = true;
|
||||
users.mutableUsers = true;
|
||||
system.etc.overlay.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
# must run before sops sets up keys
|
||||
systemd.services."sops-install-secrets-for-users".preStart = ''
|
||||
printf '${builtins.readFile (testAssets + "/age-keys.txt")}' > /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
}
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") {
|
||||
user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.userborn.enable = true;
|
||||
users.mutableUsers = false;
|
||||
system.etc.overlay.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
# must run before sops sets up keys
|
||||
systemd.services."sops-install-secrets-for-users".preStart = ''
|
||||
printf '${builtins.readFile (testAssets + "/age-keys.txt")}' > /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
}
|
||||
);
|
||||
}
|
||||
22
default.nix
22
default.nix
|
|
@ -1,18 +1,17 @@
|
|||
{ pkgs ? import <nixpkgs> {}
|
||||
, vendorHash ? "sha256-kFDRjAqUOcTma5qLQz9YKRfP85A1Z9AXm/jThssP5wU="
|
||||
}: let
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
vendorHash ? "sha256-7xnbw5tH3MYD/aA8yBNG327IONjUoBarTluLeqTH/8A=",
|
||||
}:
|
||||
let
|
||||
sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets {
|
||||
inherit vendorHash;
|
||||
};
|
||||
in rec {
|
||||
in
|
||||
rec {
|
||||
inherit sops-install-secrets;
|
||||
sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key {};
|
||||
sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { };
|
||||
default = sops-init-gpg-key;
|
||||
|
||||
sops-pgp-hook = pkgs.lib.warn ''
|
||||
sops-pgp-hook is deprecated, use sops-import-keys-hook instead.
|
||||
Also see https://github.com/Mic92/sops-nix/issues/98
|
||||
'' pkgs.callPackage ./pkgs/sops-pgp-hook { };
|
||||
sops-import-keys-hook = pkgs.callPackage ./pkgs/sops-import-keys-hook { };
|
||||
|
||||
# backwards compatibility
|
||||
|
|
@ -22,8 +21,9 @@ in rec {
|
|||
sops-pgp-hook-test = pkgs.callPackage ./pkgs/sops-pgp-hook-test.nix {
|
||||
inherit vendorHash;
|
||||
};
|
||||
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {};
|
||||
} // (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { };
|
||||
}
|
||||
// (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
lint = pkgs.callPackage ./pkgs/lint.nix {
|
||||
inherit sops-install-secrets;
|
||||
};
|
||||
|
|
|
|||
1
dev/private.narHash
Normal file
1
dev/private.narHash
Normal file
|
|
@ -0,0 +1 @@
|
|||
sha256-rXlTQPa9c8Ou52KO5S36sOyKUzurr5fuZcXnHr7g6YY=
|
||||
90
dev/private/flake.lock
generated
Normal file
90
dev/private/flake.lock
generated
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"nodes": {
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs-stable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731832479,
|
||||
"narHash": "sha256-icDDuYwJ0avTMZTxe1qyU/Baht5JOqw4pb5mWpR+hT0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "5056a1cf0ce7c2a08ab50713b6c4af77975f6111",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs-stable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731809072,
|
||||
"narHash": "sha256-pOsDJQR0imnFLfpvTmRpHcP0tflyxtP/QIzokrKSP8U=",
|
||||
"owner": "LnL7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "34588d57cfc41c6953c54c93b6b685cab3b548ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "LnL7",
|
||||
"repo": "nix-darwin",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1731842749,
|
||||
"narHash": "sha256-aNc8irVBH7sM5cGDvqdOueg8S+fGakf0rEMRGfGwWZw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bf6132dc791dbdff8b6894c3a85eb27ad8255682",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager",
|
||||
"nix-darwin": "nix-darwin",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs-stable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730321837,
|
||||
"narHash": "sha256-vK+a09qq19QNu2MlLcvN4qcRctJbqWkX7ahgPZ/+maI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "746901bb8dba96d154b66492a29f5db0693dbfcc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
15
dev/private/flake.nix
Normal file
15
dev/private/flake.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
description = "private inputs";
|
||||
inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05";
|
||||
|
||||
inputs.treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
inputs.treefmt-nix.inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||
|
||||
inputs.nix-darwin.url = "github:LnL7/nix-darwin";
|
||||
inputs.nix-darwin.inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||
|
||||
inputs.home-manager.url = "github:nix-community/home-manager";
|
||||
inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||
|
||||
outputs = _: { };
|
||||
}
|
||||
25
flake.lock
generated
25
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1725534445,
|
||||
"narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=",
|
||||
"lastModified": 1731763621,
|
||||
"narHash": "sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39",
|
||||
"rev": "c69a9bffbecde46b4b939465422ddc59493d3e4d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -16,26 +16,9 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1725762081,
|
||||
"narHash": "sha256-vNv+aJUW5/YurRy1ocfvs4q/48yVESwlC/yHzjkZSP8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "dc454045f5b5d814e5862a6d057e7bb5c29edc05",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
202
flake.nix
202
flake.nix
|
|
@ -1,57 +1,155 @@
|
|||
{
|
||||
description = "Integrates sops into nixos";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05";
|
||||
nixConfig.extra-substituters = ["https://cache.thalheim.io"];
|
||||
nixConfig.extra-trusted-public-keys = ["cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="];
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
nixpkgs-stable
|
||||
}: let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
|
||||
suffix-version = version: attrs: nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs;
|
||||
suffix-stable = suffix-version "-24_05";
|
||||
in {
|
||||
overlays.default = final: prev: let
|
||||
localPkgs = import ./default.nix {pkgs = final;};
|
||||
in {
|
||||
inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook sops-ssh-to-age;
|
||||
# backward compatibility
|
||||
inherit (prev) ssh-to-pgp;
|
||||
};
|
||||
nixosModules = {
|
||||
sops = import ./modules/sops;
|
||||
default = self.nixosModules.sops;
|
||||
};
|
||||
homeManagerModules.sops = import ./modules/home-manager/sops.nix;
|
||||
homeManagerModule = self.homeManagerModules.sops;
|
||||
packages = forAllSystems (system:
|
||||
import ./default.nix {
|
||||
pkgs = import nixpkgs {inherit system;};
|
||||
});
|
||||
checks = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"]
|
||||
(system: let
|
||||
tests = self.packages.${system}.sops-install-secrets.tests;
|
||||
packages-stable = import ./default.nix {
|
||||
pkgs = import nixpkgs-stable {inherit system;};
|
||||
};
|
||||
tests-stable = packages-stable.sops-install-secrets.tests;
|
||||
in tests //
|
||||
(suffix-stable tests-stable) //
|
||||
(suffix-stable packages-stable));
|
||||
|
||||
devShells = forAllSystems (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {};
|
||||
default = pkgs.callPackage ./shell.nix {};
|
||||
});
|
||||
};
|
||||
nixConfig.extra-substituters = [ "https://cache.thalheim.io" ];
|
||||
nixConfig.extra-trusted-public-keys = [
|
||||
"cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="
|
||||
];
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
}@inputs:
|
||||
let
|
||||
loadPrivateFlake =
|
||||
path:
|
||||
let
|
||||
flakeHash = builtins.readFile "${toString path}.narHash";
|
||||
flakePath = "path:${toString path}?narHash=${flakeHash}";
|
||||
in
|
||||
builtins.getFlake (builtins.unsafeDiscardStringContext flakePath);
|
||||
|
||||
privateFlake = loadPrivateFlake ./dev/private;
|
||||
|
||||
privateInputs = privateFlake.inputs;
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
];
|
||||
|
||||
eachSystem =
|
||||
f:
|
||||
builtins.listToAttrs (
|
||||
builtins.map (system: {
|
||||
name = system;
|
||||
value = f {
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
inherit system;
|
||||
};
|
||||
}) systems
|
||||
);
|
||||
|
||||
in
|
||||
# public outputs
|
||||
{
|
||||
overlays.default =
|
||||
final: prev:
|
||||
let
|
||||
localPkgs = import ./default.nix { pkgs = final; };
|
||||
in
|
||||
{
|
||||
inherit (localPkgs)
|
||||
sops-install-secrets
|
||||
sops-init-gpg-key
|
||||
sops-pgp-hook
|
||||
sops-import-keys-hook
|
||||
sops-ssh-to-age
|
||||
;
|
||||
# backward compatibility
|
||||
inherit (prev) ssh-to-pgp;
|
||||
};
|
||||
nixosModules = {
|
||||
sops = ./modules/sops;
|
||||
default = self.nixosModules.sops;
|
||||
};
|
||||
homeManagerModules.sops = ./modules/home-manager/sops.nix;
|
||||
homeManagerModule = self.homeManagerModules.sops;
|
||||
darwinModules = {
|
||||
sops = ./modules/nix-darwin;
|
||||
default = self.darwinModules.sops;
|
||||
};
|
||||
packages = eachSystem ({ pkgs, ... }: import ./default.nix { inherit pkgs; });
|
||||
}
|
||||
//
|
||||
# dev outputs
|
||||
{
|
||||
checks = eachSystem (
|
||||
{ pkgs, system, ... }:
|
||||
let
|
||||
packages-stable = import ./default.nix {
|
||||
pkgs = privateInputs.nixpkgs-stable.legacyPackages.${system};
|
||||
};
|
||||
dropOverride = attrs: nixpkgs.lib.removeAttrs attrs [ "override" ];
|
||||
tests = dropOverride (pkgs.callPackage ./checks/nixos-test.nix { });
|
||||
tests-stable = dropOverride (
|
||||
privateInputs.nixpkgs-stable.legacyPackages.${system}.callPackage ./checks/nixos-test.nix { }
|
||||
);
|
||||
suffix-version =
|
||||
version: attrs:
|
||||
nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs;
|
||||
suffix-stable = suffix-version "-24_05";
|
||||
in
|
||||
{
|
||||
home-manager = self.legacyPackages.${system}.homeConfigurations.sops.activation-script;
|
||||
}
|
||||
// (suffix-stable packages-stable)
|
||||
// nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux tests
|
||||
// nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux (suffix-stable tests-stable)
|
||||
// nixpkgs.lib.optionalAttrs pkgs.stdenv.isDarwin {
|
||||
darwin-sops =
|
||||
self.darwinConfigurations."sops-${pkgs.hostPlatform.darwinArch}".config.system.build.toplevel;
|
||||
}
|
||||
);
|
||||
|
||||
darwinConfigurations.sops-arm64 = privateInputs.nix-darwin.lib.darwinSystem {
|
||||
modules = [
|
||||
./checks/darwin.nix
|
||||
{ nixpkgs.hostPlatform = "aarch64-darwin"; }
|
||||
];
|
||||
};
|
||||
|
||||
darwinConfigurations.sops-x86_64 = privateInputs.nix-darwin.lib.darwinSystem {
|
||||
modules = [
|
||||
./checks/darwin.nix
|
||||
{ nixpkgs.hostPlatform = "x86_64-darwin"; }
|
||||
];
|
||||
};
|
||||
|
||||
legacyPackages = eachSystem (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
homeConfigurations.sops = privateInputs.home-manager.lib.homeManagerConfiguration {
|
||||
modules = [
|
||||
./checks/home-manager.nix
|
||||
];
|
||||
inherit pkgs;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
apps = eachSystem (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
update-dev-private-narHash = {
|
||||
type = "app";
|
||||
program = "${pkgs.writeShellScript "update-dev-private-narHash" ''
|
||||
nix --extra-experimental-features "nix-command flakes" flake lock ./dev/private
|
||||
nix --extra-experimental-features "nix-command flakes" hash path ./dev/private | tr -d '\n' > ./dev/private.narHash
|
||||
''}";
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
devShells = eachSystem (
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { };
|
||||
default = pkgs.callPackage ./shell.nix { };
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -4,12 +4,12 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton
|
||||
github.com/ProtonMail/go-crypto v1.1.3
|
||||
github.com/getsops/sops/v3 v3.8.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/sys v0.27.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
|
|
@ -90,9 +90,9 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.17.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/term v0.24.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/api v0.167.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
|
|
|
|||
24
go.sum
24
go.sum
|
|
@ -32,8 +32,8 @@ github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 h1:zF3WQbETL3cLvt
|
|||
github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0/go.mod h1:OUOla4dJLQ5FfdB07jnjawnMEqI0M3Q4WuD2W/DjhLo=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton h1:KVBEgU3CJpmzLChnLiSuEyCuhGhcMt3eOST+7A+ckto=
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w=
|
||||
github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M=
|
||||
|
|
@ -240,8 +240,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
|
@ -270,8 +270,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -288,15 +288,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
|
@ -304,8 +304,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
|||
|
|
@ -1,66 +1,81 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.sops;
|
||||
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets;
|
||||
secretType = lib.types.submodule ({ config, name, ... }: {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
Name of the file used in /run/user/*/secrets
|
||||
'';
|
||||
};
|
||||
sops-install-secrets = (pkgs.callPackage ../.. { }).sops-install-secrets;
|
||||
secretType = lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
Name of the file used in /run/user/*/secrets
|
||||
'';
|
||||
};
|
||||
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
Key used to lookup in the sops file.
|
||||
No tested data structures are supported right now.
|
||||
This option is ignored if format is binary.
|
||||
'';
|
||||
};
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name;
|
||||
description = ''
|
||||
Key used to lookup in the sops file.
|
||||
No tested data structures are supported right now.
|
||||
This option is ignored if format is binary.
|
||||
"" means whole file.
|
||||
'';
|
||||
};
|
||||
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "${cfg.defaultSymlinkPath}/${name}";
|
||||
description = ''
|
||||
Path where secrets are symlinked to.
|
||||
If the default is kept no other symlink is created.
|
||||
`%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf
|
||||
DARWIN_USER_TEMP_DIR` on darwin.
|
||||
'';
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "${cfg.defaultSymlinkPath}/${name}";
|
||||
description = ''
|
||||
Path where secrets are symlinked to.
|
||||
If the default is kept no other symlink is created.
|
||||
`%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf
|
||||
DARWIN_USER_TEMP_DIR` on darwin.
|
||||
'';
|
||||
};
|
||||
|
||||
format = lib.mkOption {
|
||||
type = lib.types.enum [ "yaml" "json" "binary" "ini" "dotenv" ];
|
||||
default = cfg.defaultSopsFormat;
|
||||
description = ''
|
||||
File format used to decrypt the sops secret.
|
||||
Binary files are written to the target file as is.
|
||||
'';
|
||||
};
|
||||
format = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"yaml"
|
||||
"json"
|
||||
"binary"
|
||||
"ini"
|
||||
"dotenv"
|
||||
];
|
||||
default = cfg.defaultSopsFormat;
|
||||
description = ''
|
||||
File format used to decrypt the sops secret.
|
||||
Binary files are written to the target file as is.
|
||||
'';
|
||||
};
|
||||
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the in octal.
|
||||
'';
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the in octal.
|
||||
'';
|
||||
};
|
||||
|
||||
sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = cfg.defaultSopsFile;
|
||||
defaultText = "\${config.sops.defaultSopsFile}";
|
||||
description = ''
|
||||
Sops file the secret is loaded from.
|
||||
'';
|
||||
sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = cfg.defaultSopsFile;
|
||||
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
|
||||
description = ''
|
||||
Sops file the secret is loaded from.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
pathNotInStore = lib.mkOptionType {
|
||||
name = "pathNotInStore";
|
||||
|
|
@ -70,50 +85,62 @@ let
|
|||
merge = lib.mergeEqualOption;
|
||||
};
|
||||
|
||||
manifestFor = suffix: secrets: pkgs.writeTextFile {
|
||||
name = "manifest${suffix}.json";
|
||||
text = builtins.toJSON {
|
||||
secrets = builtins.attrValues secrets;
|
||||
secretsMountPoint = cfg.defaultSecretsMountPoint;
|
||||
symlinkPath = cfg.defaultSymlinkPath;
|
||||
keepGenerations = cfg.keepGenerations;
|
||||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
userMode = true;
|
||||
logging = {
|
||||
keyImport = builtins.elem "keyImport" cfg.log;
|
||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||
manifestFor =
|
||||
suffix: secrets: templates:
|
||||
pkgs.writeTextFile {
|
||||
name = "manifest${suffix}.json";
|
||||
text = builtins.toJSON {
|
||||
secrets = builtins.attrValues secrets;
|
||||
templates = builtins.attrValues templates;
|
||||
secretsMountPoint = cfg.defaultSecretsMountPoint;
|
||||
symlinkPath = cfg.defaultSymlinkPath;
|
||||
keepGenerations = cfg.keepGenerations;
|
||||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
userMode = true;
|
||||
logging = {
|
||||
keyImport = builtins.elem "keyImport" cfg.log;
|
||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||
};
|
||||
};
|
||||
checkPhase = ''
|
||||
${sops-install-secrets}/bin/sops-install-secrets -check-mode=${
|
||||
if cfg.validateSopsFiles then "sopsfile" else "manifest"
|
||||
} "$out"
|
||||
'';
|
||||
};
|
||||
checkPhase = ''
|
||||
${sops-install-secrets}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out"
|
||||
'';
|
||||
};
|
||||
|
||||
manifest = manifestFor "" cfg.secrets;
|
||||
manifest = manifestFor "" cfg.secrets cfg.templates;
|
||||
|
||||
escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile;
|
||||
|
||||
script = toString (pkgs.writeShellScript "sops-nix-user" ((lib.optionalString (cfg.gnupg.home != null) ''
|
||||
export SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg
|
||||
'')
|
||||
+ (lib.optionalString cfg.age.generateKey ''
|
||||
if [[ ! -f ${escapedAgeKeyFile} ]]; then
|
||||
echo generating machine-specific age key...
|
||||
${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile})
|
||||
# age-keygen sets 0600 by default, no need to chmod.
|
||||
${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile}
|
||||
fi
|
||||
'' + ''
|
||||
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest}
|
||||
'')));
|
||||
in {
|
||||
script = toString (
|
||||
pkgs.writeShellScript "sops-nix-user" (
|
||||
lib.optionalString cfg.age.generateKey ''
|
||||
if [[ ! -f ${escapedAgeKeyFile} ]]; then
|
||||
echo generating machine-specific age key...
|
||||
${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile})
|
||||
# age-keygen sets 0600 by default, no need to chmod.
|
||||
${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile}
|
||||
fi
|
||||
''
|
||||
+ ''
|
||||
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest}
|
||||
''
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./templates.nix
|
||||
];
|
||||
|
||||
options.sops = {
|
||||
secrets = lib.mkOption {
|
||||
type = lib.types.attrsOf secretType;
|
||||
default = {};
|
||||
default = { };
|
||||
description = ''
|
||||
Secrets to decrypt.
|
||||
'';
|
||||
|
|
@ -134,6 +161,16 @@ in {
|
|||
'';
|
||||
};
|
||||
|
||||
defaultSopsKey = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Default key used to lookup in all secrets.
|
||||
This option is ignored if format is binary.
|
||||
"" means whole file.
|
||||
'';
|
||||
};
|
||||
|
||||
validateSopsFiles = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
|
|
@ -169,11 +206,29 @@ in {
|
|||
};
|
||||
|
||||
log = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]);
|
||||
default = [ "keyImport" "secretChanges" ];
|
||||
type = lib.types.listOf (
|
||||
lib.types.enum [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
]
|
||||
);
|
||||
default = [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
];
|
||||
description = "What to log";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables to set before calling sops-install-secrets.
|
||||
|
||||
To properly quote strings with quotes use lib.escapeShellArg.
|
||||
'';
|
||||
};
|
||||
|
||||
age = {
|
||||
keyFile = lib.mkOption {
|
||||
type = lib.types.nullOr pathNotInStore;
|
||||
|
|
@ -196,7 +251,7 @@ in {
|
|||
|
||||
sshKeyPaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = ''
|
||||
Paths to ssh keys added as age keys during sops description.
|
||||
'';
|
||||
|
|
@ -213,9 +268,22 @@ in {
|
|||
'';
|
||||
};
|
||||
|
||||
qubes-split-gpg = {
|
||||
enable = lib.mkEnableOption "Enable support for Qubes Split GPG feature: https://www.qubes-os.org/doc/split-gpg";
|
||||
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "vault-gpg";
|
||||
description = ''
|
||||
It tells Qubes OS which secure Qube holds your GPG keys for isolated cryptographic operations.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sshKeyPaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = ''
|
||||
Path to ssh keys added as GPG keys during sops description.
|
||||
This option must be explicitly unset if <literal>config.sops.gnupg.sshKeyPaths</literal> is set.
|
||||
|
|
@ -224,24 +292,73 @@ in {
|
|||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.secrets != {}) {
|
||||
assertions = [{
|
||||
assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != [];
|
||||
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
|
||||
} {
|
||||
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []);
|
||||
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
|
||||
}] ++ lib.optionals cfg.validateSopsFiles (
|
||||
lib.concatLists (lib.mapAttrsToList (name: secret: [{
|
||||
assertion = builtins.pathExists secret.sopsFile;
|
||||
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||
} {
|
||||
assertion =
|
||||
builtins.isPath secret.sopsFile ||
|
||||
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||
}]) cfg.secrets)
|
||||
);
|
||||
config = lib.mkIf (cfg.secrets != { }) {
|
||||
assertions =
|
||||
[
|
||||
{
|
||||
assertion =
|
||||
cfg.gnupg.home != null
|
||||
|| cfg.gnupg.sshKeyPaths != [ ]
|
||||
|| cfg.gnupg.qubes-split-gpg.enable == true
|
||||
|| cfg.age.keyFile != null
|
||||
|| cfg.age.sshKeyPaths != [ ];
|
||||
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
!(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ])
|
||||
&& !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true)
|
||||
&& !(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true);
|
||||
message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.gnupg.qubes-split-gpg.enable == false
|
||||
|| (
|
||||
cfg.gnupg.qubes-split-gpg.enable == true
|
||||
&& cfg.gnupg.qubes-split-gpg.domain != null
|
||||
&& cfg.gnupg.qubes-split-gpg.domain != ""
|
||||
);
|
||||
message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true";
|
||||
}
|
||||
]
|
||||
++ lib.optionals cfg.validateSopsFiles (
|
||||
lib.concatLists (
|
||||
lib.mapAttrsToList (name: secret: [
|
||||
{
|
||||
assertion = builtins.pathExists secret.sopsFile;
|
||||
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
builtins.isPath secret.sopsFile
|
||||
|| (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||
}
|
||||
]) cfg.secrets
|
||||
)
|
||||
);
|
||||
|
||||
home.sessionVariables = lib.mkIf cfg.gnupg.qubes-split-gpg.enable {
|
||||
# TODO: Add this package to nixpkgs and use it from the store
|
||||
# https://github.com/QubesOS/qubes-app-linux-split-gpg
|
||||
SOPS_GPG_EXEC = "qubes-gpg-client-wrapper";
|
||||
};
|
||||
|
||||
sops.environment = {
|
||||
SOPS_GPG_EXEC = lib.mkMerge [
|
||||
(lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
|
||||
lib.mkDefault "${pkgs.gnupg}/bin/gpg"
|
||||
))
|
||||
(lib.mkIf cfg.gnupg.qubes-split-gpg.enable (
|
||||
lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC
|
||||
))
|
||||
];
|
||||
|
||||
QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable (
|
||||
lib.mkDefault cfg.gnupg.qubes-split-gpg.domain
|
||||
);
|
||||
};
|
||||
|
||||
systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
|
||||
Unit = {
|
||||
|
|
@ -249,9 +366,13 @@ in {
|
|||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
Environment = builtins.concatStringsSep " " (
|
||||
lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment
|
||||
);
|
||||
ExecStart = script;
|
||||
};
|
||||
Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ];
|
||||
Install.WantedBy =
|
||||
if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ];
|
||||
};
|
||||
|
||||
# Darwin: load secrets once on login
|
||||
|
|
@ -259,6 +380,7 @@ in {
|
|||
enable = true;
|
||||
config = {
|
||||
Program = script;
|
||||
EnvironmentVariables = cfg.environment;
|
||||
KeepAlive = false;
|
||||
RunAtLoad = true;
|
||||
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/SopsNix/stdout";
|
||||
|
|
@ -267,28 +389,36 @@ in {
|
|||
};
|
||||
|
||||
# [re]load secrets on home-manager activation
|
||||
home.activation = let
|
||||
darwin = let
|
||||
domain-target = "gui/$(id -u ${config.home.username})";
|
||||
in ''
|
||||
/bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true
|
||||
/bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist
|
||||
'';
|
||||
home.activation =
|
||||
let
|
||||
darwin =
|
||||
let
|
||||
domain-target = "gui/$(id -u ${config.home.username})";
|
||||
in
|
||||
''
|
||||
/bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true
|
||||
/bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist
|
||||
'';
|
||||
|
||||
linux = let systemctl = config.systemd.user.systemctlPath; in ''
|
||||
systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true)
|
||||
linux =
|
||||
let
|
||||
systemctl = config.systemd.user.systemctlPath;
|
||||
in
|
||||
''
|
||||
systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true)
|
||||
|
||||
if [[ $systemdStatus == 'running' ]]; then
|
||||
${systemctl} restart --user sops-nix
|
||||
else
|
||||
echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed."
|
||||
fi
|
||||
if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then
|
||||
${systemctl} restart --user sops-nix
|
||||
else
|
||||
echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed."
|
||||
fi
|
||||
|
||||
unset systemdStatus
|
||||
'';
|
||||
|
||||
in {
|
||||
sops-nix = if pkgs.stdenv.isLinux then linux else darwin;
|
||||
};
|
||||
unset systemdStatus
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
sops-nix = if pkgs.stdenv.isLinux then linux else darwin;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
106
modules/home-manager/templates.nix
Normal file
106
modules/home-manager/templates.nix
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkOption
|
||||
mkDefault
|
||||
mapAttrs
|
||||
types
|
||||
;
|
||||
in
|
||||
{
|
||||
options.sops = {
|
||||
templates = mkOption {
|
||||
description = "Templates for secret files";
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets/rendered
|
||||
'';
|
||||
};
|
||||
path = mkOption {
|
||||
description = "Path where the rendered file will be placed";
|
||||
type = types.singleLineStr;
|
||||
# Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go`
|
||||
default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}";
|
||||
};
|
||||
content = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Content of the file
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the rendered secret file in octal.
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
default = pkgs.writeText config.name config.content;
|
||||
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
|
||||
example = "./configuration-template.conf";
|
||||
description = ''
|
||||
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
|
||||
'';
|
||||
};
|
||||
restartUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be restarted when the rendered template changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
reloadUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be reloaded when the rendered template changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
placeholder = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.mkOptionType {
|
||||
name = "coercibleToString";
|
||||
description = "value that can be coerced to string";
|
||||
check = lib.strings.isConvertibleWithToString;
|
||||
merge = lib.mergeEqualOption;
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
visible = false;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.optionalAttrs (options ? sops.secrets) (
|
||||
lib.mkIf (config.sops.templates != { }) {
|
||||
sops.placeholder = mapAttrs (
|
||||
name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
|
||||
) config.sops.secrets;
|
||||
}
|
||||
);
|
||||
}
|
||||
401
modules/nix-darwin/default.nix
Normal file
401
modules/nix-darwin/default.nix
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.sops;
|
||||
sops-install-secrets = cfg.package;
|
||||
manifestFor = pkgs.callPackage ./manifest-for.nix {
|
||||
inherit cfg;
|
||||
inherit (pkgs) writeTextFile;
|
||||
};
|
||||
manifest = manifestFor "" regularSecrets regularTemplates { };
|
||||
|
||||
# Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.)
|
||||
regularTemplates = cfg.templates;
|
||||
|
||||
pathNotInStore = lib.mkOptionType {
|
||||
name = "pathNotInStore";
|
||||
description = "path not in the Nix store";
|
||||
descriptionClass = "noun";
|
||||
check = x: !lib.path.hasStorePathPrefix (/. + x);
|
||||
merge = lib.mergeEqualOption;
|
||||
};
|
||||
|
||||
regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets;
|
||||
|
||||
withEnvironment = import ./with-environment.nix {
|
||||
inherit cfg lib;
|
||||
};
|
||||
secretType = lib.types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
config = {
|
||||
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
|
||||
sopsFileHash = lib.mkOptionDefault (
|
||||
lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"
|
||||
);
|
||||
};
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets
|
||||
'';
|
||||
};
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Key used to lookup in the sops file.
|
||||
No tested data structures are supported right now.
|
||||
This option is ignored if format is binary.
|
||||
'';
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default =
|
||||
if config.neededForUsers then
|
||||
"/run/secrets-for-users/${config.name}"
|
||||
else
|
||||
"/run/secrets/${config.name}";
|
||||
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
|
||||
description = ''
|
||||
Path where secrets are symlinked to.
|
||||
If the default is kept no symlink is created.
|
||||
'';
|
||||
};
|
||||
format = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"yaml"
|
||||
"json"
|
||||
"binary"
|
||||
"dotenv"
|
||||
"ini"
|
||||
];
|
||||
default = cfg.defaultSopsFormat;
|
||||
description = ''
|
||||
File format used to decrypt the sops secret.
|
||||
Binary files are written to the target file as is.
|
||||
'';
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the in octal.
|
||||
'';
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = "root";
|
||||
description = ''
|
||||
User of the file. Can only be set if uid is 0.
|
||||
'';
|
||||
};
|
||||
uid = lib.mkOption {
|
||||
type = with lib.types; nullOr int;
|
||||
default = 0;
|
||||
description = ''
|
||||
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
|
||||
'';
|
||||
};
|
||||
group = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = "staff";
|
||||
defaultText = "staff";
|
||||
description = ''
|
||||
Group of the file. Can only be set if gid is 0.
|
||||
'';
|
||||
};
|
||||
gid = lib.mkOption {
|
||||
type = with lib.types; nullOr int;
|
||||
default = 0;
|
||||
description = ''
|
||||
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
|
||||
'';
|
||||
};
|
||||
sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
|
||||
description = ''
|
||||
Sops file the secret is loaded from.
|
||||
'';
|
||||
};
|
||||
sopsFileHash = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Hash of the sops file.
|
||||
'';
|
||||
};
|
||||
neededForUsers = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
**Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS.
|
||||
This can be used to retrieve user's passwords from sops-nix.
|
||||
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
darwinSSHKeys = [
|
||||
{
|
||||
type = "rsa";
|
||||
path = "/etc/ssh/ssh_host_rsa_key";
|
||||
}
|
||||
{
|
||||
type = "ed25519";
|
||||
path = "/etc/ssh/ssh_host_ed25519_key";
|
||||
}
|
||||
];
|
||||
|
||||
escapedKeyFile = lib.escapeShellArg cfg.age.keyFile;
|
||||
# Skip ssh keys deployed with sops to avoid a catch 22
|
||||
defaultImportKeys =
|
||||
algo:
|
||||
map (e: e.path) (
|
||||
lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys
|
||||
);
|
||||
|
||||
installScript = ''
|
||||
${
|
||||
if cfg.age.generateKey then
|
||||
''
|
||||
if [[ ! -f ${escapedKeyFile} ]]; then
|
||||
echo generating machine-specific age key...
|
||||
mkdir -p "$(dirname ${escapedKeyFile})"
|
||||
# age-keygen sets 0600 by default, no need to chmod.
|
||||
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile}
|
||||
fi
|
||||
''
|
||||
else
|
||||
""
|
||||
}
|
||||
echo "Setting up secrets..."
|
||||
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"}
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.sops = {
|
||||
secrets = lib.mkOption {
|
||||
type = lib.types.attrsOf secretType;
|
||||
default = { };
|
||||
description = ''
|
||||
Path where the latest secrets are mounted to.
|
||||
'';
|
||||
};
|
||||
|
||||
defaultSopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
Default sops file used for all secrets.
|
||||
'';
|
||||
};
|
||||
|
||||
defaultSopsFormat = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "yaml";
|
||||
description = ''
|
||||
Default sops format used for all secrets.
|
||||
'';
|
||||
};
|
||||
|
||||
validateSopsFiles = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Check all sops files at evaluation time.
|
||||
This requires sops files to be added to the nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
keepGenerations = lib.mkOption {
|
||||
type = lib.types.ints.unsigned;
|
||||
default = 1;
|
||||
description = ''
|
||||
Number of secrets generations to keep. Setting this to 0 disables pruning.
|
||||
'';
|
||||
};
|
||||
|
||||
log = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.enum [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
]
|
||||
);
|
||||
default = [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
];
|
||||
description = "What to log";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables to set before calling sops-install-secrets.
|
||||
|
||||
The values are placed in single quotes and not escaped any further to
|
||||
allow usage of command substitutions for more flexibility. To properly quote
|
||||
strings with quotes use lib.escapeShellArg.
|
||||
|
||||
This will be evaluated twice when using secrets that use neededForUsers but
|
||||
in a subshell each time so the environment variables don't collide.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = (pkgs.callPackage ../.. { }).sops-install-secrets;
|
||||
defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets";
|
||||
description = ''
|
||||
sops-install-secrets package to use.
|
||||
'';
|
||||
};
|
||||
|
||||
validationPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default =
|
||||
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then
|
||||
sops-install-secrets
|
||||
else
|
||||
(pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets;
|
||||
defaultText = lib.literalExpression "config.sops.package";
|
||||
|
||||
description = ''
|
||||
sops-install-secrets package to use when validating configuration.
|
||||
|
||||
Defaults to sops.package if building natively, and a native version of sops-install-secrets if cross compiling.
|
||||
'';
|
||||
};
|
||||
|
||||
age = {
|
||||
keyFile = lib.mkOption {
|
||||
type = lib.types.nullOr pathNotInStore;
|
||||
default = null;
|
||||
example = "/var/lib/sops-nix/key.txt";
|
||||
description = ''
|
||||
Path to age key file used for sops decryption.
|
||||
'';
|
||||
};
|
||||
|
||||
generateKey = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether or not to generate the age key. If this
|
||||
option is set to false, the key must already be
|
||||
present at the specified location.
|
||||
'';
|
||||
};
|
||||
|
||||
sshKeyPaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = defaultImportKeys "ed25519";
|
||||
defaultText = lib.literalMD "The ed25519 keys from {option}`config.services.openssh.hostKeys`";
|
||||
description = ''
|
||||
Paths to ssh keys added as age keys during sops description.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
gnupg = {
|
||||
home = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/root/.gnupg";
|
||||
description = ''
|
||||
Path to gnupg database directory containing the key for decrypting the sops file.
|
||||
'';
|
||||
};
|
||||
|
||||
sshKeyPaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = defaultImportKeys "rsa";
|
||||
defaultText = lib.literalMD "The rsa keys from {option}`config.services.openssh.hostKeys`";
|
||||
description = ''
|
||||
Path to ssh keys added as GPG keys during sops description.
|
||||
This option must be explicitly unset if <literal>config.sops.gnupg.home</literal> is set.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
imports = [
|
||||
./templates
|
||||
./secrets-for-users
|
||||
];
|
||||
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf (cfg.secrets != { }) {
|
||||
assertions =
|
||||
[
|
||||
{
|
||||
assertion =
|
||||
cfg.gnupg.home != null
|
||||
|| cfg.gnupg.sshKeyPaths != [ ]
|
||||
|| cfg.age.keyFile != null
|
||||
|| cfg.age.sshKeyPaths != [ ];
|
||||
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
|
||||
}
|
||||
{
|
||||
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]);
|
||||
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
|
||||
}
|
||||
]
|
||||
++ lib.optionals cfg.validateSopsFiles (
|
||||
lib.concatLists (
|
||||
lib.mapAttrsToList (name: secret: [
|
||||
{
|
||||
assertion = builtins.pathExists secret.sopsFile;
|
||||
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
builtins.isPath secret.sopsFile
|
||||
|| (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||
}
|
||||
{
|
||||
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null;
|
||||
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set";
|
||||
}
|
||||
{
|
||||
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null;
|
||||
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set";
|
||||
}
|
||||
]) cfg.secrets
|
||||
)
|
||||
);
|
||||
|
||||
system.build.sops-nix-manifest = manifest;
|
||||
system.activationScripts = {
|
||||
postActivation.text = lib.mkAfter installScript;
|
||||
};
|
||||
|
||||
launchd.daemons.sops-install-secrets = {
|
||||
command = installScript;
|
||||
serviceConfig = {
|
||||
RunAtLoad = true;
|
||||
KeepAlive = false;
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
{
|
||||
sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
|
||||
lib.mkDefault "${pkgs.gnupg}/bin/gpg"
|
||||
);
|
||||
}
|
||||
];
|
||||
}
|
||||
34
modules/nix-darwin/manifest-for.nix
Normal file
34
modules/nix-darwin/manifest-for.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{ writeTextFile, cfg }:
|
||||
|
||||
suffix: secrets: templates: extraJson:
|
||||
|
||||
writeTextFile {
|
||||
name = "manifest${suffix}.json";
|
||||
text = builtins.toJSON (
|
||||
{
|
||||
secrets = builtins.attrValues secrets;
|
||||
templates = builtins.attrValues templates;
|
||||
# Does this need to be configurable?
|
||||
secretsMountPoint = "/run/secrets.d";
|
||||
symlinkPath = "/run/secrets";
|
||||
keepGenerations = cfg.keepGenerations;
|
||||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
useTmpfs = false;
|
||||
placeholderBySecretName = cfg.placeholder;
|
||||
userMode = false;
|
||||
logging = {
|
||||
keyImport = builtins.elem "keyImport" cfg.log;
|
||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||
};
|
||||
}
|
||||
// extraJson
|
||||
);
|
||||
checkPhase = ''
|
||||
${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${
|
||||
if cfg.validateSopsFiles then "sopsfile" else "manifest"
|
||||
} "$out"
|
||||
'';
|
||||
}
|
||||
53
modules/nix-darwin/secrets-for-users/default.nix
Normal file
53
modules/nix-darwin/secrets-for-users/default.nix
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.sops;
|
||||
secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets;
|
||||
templatesForUsers = { }; # We do not currently support `neededForUsers` for templates.
|
||||
manifestFor = pkgs.callPackage ../manifest-for.nix {
|
||||
inherit cfg;
|
||||
inherit (pkgs) writeTextFile;
|
||||
};
|
||||
withEnvironment = import ../with-environment.nix {
|
||||
inherit cfg lib;
|
||||
};
|
||||
manifestForUsers = manifestFor "-for-users" secretsForUsers templatesForUsers {
|
||||
secretsMountPoint = "/run/secrets-for-users.d";
|
||||
symlinkPath = "/run/secrets-for-users";
|
||||
};
|
||||
|
||||
installScript = ''
|
||||
echo "Setting up secrets for users"
|
||||
${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"}
|
||||
'';
|
||||
in
|
||||
{
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
(lib.filterAttrs (
|
||||
_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")
|
||||
) secretsForUsers) == { };
|
||||
message = "neededForUsers cannot be used for secrets that are not root-owned";
|
||||
}
|
||||
];
|
||||
|
||||
system.activationScripts = lib.mkIf (secretsForUsers != [ ]) {
|
||||
postActivation.text = lib.mkAfter installScript;
|
||||
};
|
||||
|
||||
launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != [ ]) {
|
||||
command = installScript;
|
||||
serviceConfig = {
|
||||
RunAtLoad = true;
|
||||
KeepAlive = false;
|
||||
};
|
||||
};
|
||||
|
||||
system.build.sops-nix-users-manifest = manifestForUsers;
|
||||
}
|
||||
102
modules/nix-darwin/templates/default.nix
Normal file
102
modules/nix-darwin/templates/default.nix
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkOption
|
||||
mkDefault
|
||||
mapAttrs
|
||||
types
|
||||
;
|
||||
in
|
||||
{
|
||||
options.sops = {
|
||||
templates = mkOption {
|
||||
description = "Templates for secret files";
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets/rendered
|
||||
'';
|
||||
};
|
||||
path = mkOption {
|
||||
description = "Path where the rendered file will be placed";
|
||||
type = types.singleLineStr;
|
||||
default = "/run/secrets/rendered/${config.name}";
|
||||
};
|
||||
content = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Content of the file
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the rendered secret file in octal.
|
||||
'';
|
||||
};
|
||||
owner = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "root";
|
||||
description = ''
|
||||
User of the file.
|
||||
'';
|
||||
};
|
||||
group = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "staff";
|
||||
defaultText = "staff";
|
||||
description = ''
|
||||
Group of the file. Default on darwin in staff.
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
default = pkgs.writeText config.name config.content;
|
||||
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
|
||||
example = "./configuration-template.conf";
|
||||
description = ''
|
||||
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
placeholder = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.mkOptionType {
|
||||
name = "coercibleToString";
|
||||
description = "value that can be coerced to string";
|
||||
check = lib.strings.isConvertibleWithToString;
|
||||
merge = lib.mergeEqualOption;
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
visible = false;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.optionalAttrs (options ? sops.secrets) (
|
||||
lib.mkIf (config.sops.templates != { }) {
|
||||
sops.placeholder = mapAttrs (
|
||||
name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
|
||||
) config.sops.secrets;
|
||||
}
|
||||
);
|
||||
}
|
||||
14
modules/nix-darwin/with-environment.nix
Normal file
14
modules/nix-darwin/with-environment.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{ cfg, lib }:
|
||||
|
||||
sopsCall:
|
||||
|
||||
if cfg.environment == { } then
|
||||
sopsCall
|
||||
else
|
||||
''
|
||||
(
|
||||
# shellcheck disable=SC2030,SC2031
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)}
|
||||
${sopsCall}
|
||||
)
|
||||
''
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
{ config, options, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.sops;
|
||||
|
|
@ -8,7 +14,7 @@ let
|
|||
inherit cfg;
|
||||
inherit (pkgs) writeTextFile;
|
||||
};
|
||||
manifest = manifestFor "" regularSecrets {};
|
||||
manifest = manifestFor "" regularSecrets regularTemplates { };
|
||||
|
||||
pathNotInStore = lib.mkOptionType {
|
||||
name = "pathNotInStore";
|
||||
|
|
@ -20,128 +26,168 @@ let
|
|||
|
||||
regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets;
|
||||
|
||||
useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) ||
|
||||
(options.services ? userborn && config.services.userborn.enable);
|
||||
# Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.)
|
||||
regularTemplates = cfg.templates;
|
||||
|
||||
useSystemdActivation =
|
||||
(options.systemd ? sysusers && config.systemd.sysusers.enable)
|
||||
|| (options.services ? userborn && config.services.userborn.enable);
|
||||
|
||||
withEnvironment = import ./with-environment.nix {
|
||||
inherit cfg lib;
|
||||
};
|
||||
secretType = lib.types.submodule ({ config, ... }: {
|
||||
config = {
|
||||
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
|
||||
sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}");
|
||||
};
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets
|
||||
'';
|
||||
secretType = lib.types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
config = {
|
||||
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
|
||||
sopsFileHash = lib.mkOptionDefault (
|
||||
lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"
|
||||
);
|
||||
};
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Key used to lookup in the sops file.
|
||||
No tested data structures are supported right now.
|
||||
This option is ignored if format is binary.
|
||||
'';
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets
|
||||
'';
|
||||
};
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.name;
|
||||
description = ''
|
||||
Key used to lookup in the sops file.
|
||||
No tested data structures are supported right now.
|
||||
This option is ignored if format is binary.
|
||||
"" means whole file.
|
||||
'';
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default =
|
||||
if config.neededForUsers then
|
||||
"/run/secrets-for-users/${config.name}"
|
||||
else
|
||||
"/run/secrets/${config.name}";
|
||||
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
|
||||
description = ''
|
||||
Path where secrets are symlinked to.
|
||||
If the default is kept no symlink is created.
|
||||
'';
|
||||
};
|
||||
format = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"yaml"
|
||||
"json"
|
||||
"binary"
|
||||
"dotenv"
|
||||
"ini"
|
||||
];
|
||||
default = cfg.defaultSopsFormat;
|
||||
description = ''
|
||||
File format used to decrypt the sops secret.
|
||||
Binary files are written to the target file as is.
|
||||
'';
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the in octal.
|
||||
'';
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
User of the file. Can only be set if uid is 0.
|
||||
'';
|
||||
};
|
||||
uid = lib.mkOption {
|
||||
type = with lib.types; nullOr int;
|
||||
default = 0;
|
||||
description = ''
|
||||
UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist.
|
||||
'';
|
||||
};
|
||||
group = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = if config.owner != null then users.${config.owner}.group else null;
|
||||
defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`";
|
||||
description = ''
|
||||
Group of the file. Can only be set if gid is 0.
|
||||
'';
|
||||
};
|
||||
gid = lib.mkOption {
|
||||
type = with lib.types; nullOr int;
|
||||
default = 0;
|
||||
description = ''
|
||||
GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist.
|
||||
'';
|
||||
};
|
||||
sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}";
|
||||
description = ''
|
||||
Sops file the secret is loaded from.
|
||||
'';
|
||||
};
|
||||
sopsFileHash = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
restartUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be restarted when this secret changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
reloadUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be reloaded when this secret changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
|
||||
'';
|
||||
};
|
||||
neededForUsers = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enabling this option causes the secret to be decrypted before users and groups are created.
|
||||
This can be used to retrieve user's passwords from sops-nix.
|
||||
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
|
||||
'';
|
||||
};
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}";
|
||||
defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise.";
|
||||
description = ''
|
||||
Path where secrets are symlinked to.
|
||||
If the default is kept no symlink is created.
|
||||
'';
|
||||
};
|
||||
format = lib.mkOption {
|
||||
type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"];
|
||||
default = cfg.defaultSopsFormat;
|
||||
description = ''
|
||||
File format used to decrypt the sops secret.
|
||||
Binary files are written to the target file as is.
|
||||
'';
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the in octal.
|
||||
'';
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "root";
|
||||
description = ''
|
||||
User of the file.
|
||||
'';
|
||||
};
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = users.${config.owner}.group;
|
||||
defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`";
|
||||
description = ''
|
||||
Group of the file.
|
||||
'';
|
||||
};
|
||||
sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
defaultText = "\${config.sops.defaultSopsFile}";
|
||||
description = ''
|
||||
Sops file the secret is loaded from.
|
||||
'';
|
||||
};
|
||||
sopsFileHash = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Hash of the sops file, useful in <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
restartUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be restarted when this secret changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
reloadUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be reloaded when this secret changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
|
||||
'';
|
||||
};
|
||||
neededForUsers = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enabling this option causes the secret to be decrypted before users and groups are created.
|
||||
This can be used to retrieve user's passwords from sops-nix.
|
||||
Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
# Skip ssh keys deployed with sops to avoid a catch 22
|
||||
defaultImportKeys = algo:
|
||||
defaultImportKeys =
|
||||
algo:
|
||||
if config.services.openssh.enable then
|
||||
map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) config.services.openssh.hostKeys)
|
||||
map (e: e.path) (
|
||||
lib.filter (
|
||||
e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)
|
||||
) config.services.openssh.hostKeys
|
||||
)
|
||||
else
|
||||
[];
|
||||
in {
|
||||
[ ];
|
||||
in
|
||||
{
|
||||
options.sops = {
|
||||
secrets = lib.mkOption {
|
||||
type = lib.types.attrsOf secretType;
|
||||
default = {};
|
||||
default = { };
|
||||
description = ''
|
||||
Path where the latest secrets are mounted to.
|
||||
'';
|
||||
|
|
@ -162,6 +208,16 @@ in {
|
|||
'';
|
||||
};
|
||||
|
||||
defaultSopsKey = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Default key used to lookup in all secrets.
|
||||
This option is ignored if format is binary.
|
||||
"" means whole file.
|
||||
'';
|
||||
};
|
||||
|
||||
validateSopsFiles = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
|
|
@ -180,14 +236,22 @@ in {
|
|||
};
|
||||
|
||||
log = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]);
|
||||
default = [ "keyImport" "secretChanges" ];
|
||||
type = lib.types.listOf (
|
||||
lib.types.enum [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
]
|
||||
);
|
||||
default = [
|
||||
"keyImport"
|
||||
"secretChanges"
|
||||
];
|
||||
description = "What to log";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path);
|
||||
default = {};
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables to set before calling sops-install-secrets.
|
||||
|
||||
|
|
@ -202,7 +266,7 @@ in {
|
|||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = (pkgs.callPackage ../.. {}).sops-install-secrets;
|
||||
default = (pkgs.callPackage ../.. { }).sops-install-secrets;
|
||||
defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets";
|
||||
description = ''
|
||||
sops-install-secrets package to use.
|
||||
|
|
@ -212,9 +276,10 @@ in {
|
|||
validationPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default =
|
||||
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
|
||||
then sops-install-secrets
|
||||
else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets;
|
||||
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then
|
||||
sops-install-secrets
|
||||
else
|
||||
(pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets;
|
||||
defaultText = lib.literalExpression "config.sops.package";
|
||||
|
||||
description = ''
|
||||
|
|
@ -298,34 +363,78 @@ in {
|
|||
imports = [
|
||||
./templates
|
||||
./secrets-for-users
|
||||
(lib.mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ])
|
||||
(lib.mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ])
|
||||
(lib.mkRenamedOptionModule
|
||||
[
|
||||
"sops"
|
||||
"gnupgHome"
|
||||
]
|
||||
[
|
||||
"sops"
|
||||
"gnupg"
|
||||
"home"
|
||||
]
|
||||
)
|
||||
(lib.mkRenamedOptionModule
|
||||
[
|
||||
"sops"
|
||||
"sshKeyPaths"
|
||||
]
|
||||
[
|
||||
"sops"
|
||||
"gnupg"
|
||||
"sshKeyPaths"
|
||||
]
|
||||
)
|
||||
];
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf (cfg.secrets != {}) {
|
||||
assertions = [{
|
||||
assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != [];
|
||||
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
|
||||
} {
|
||||
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []);
|
||||
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
|
||||
}] ++ lib.optionals cfg.validateSopsFiles (
|
||||
lib.concatLists (lib.mapAttrsToList (name: secret: [{
|
||||
assertion = builtins.pathExists secret.sopsFile;
|
||||
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||
} {
|
||||
assertion =
|
||||
builtins.isPath secret.sopsFile ||
|
||||
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||
}]) cfg.secrets)
|
||||
(lib.mkIf (cfg.secrets != { }) {
|
||||
assertions =
|
||||
[
|
||||
{
|
||||
assertion =
|
||||
cfg.gnupg.home != null
|
||||
|| cfg.gnupg.sshKeyPaths != [ ]
|
||||
|| cfg.age.keyFile != null
|
||||
|| cfg.age.sshKeyPaths != [ ];
|
||||
message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home";
|
||||
}
|
||||
{
|
||||
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]);
|
||||
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
|
||||
}
|
||||
]
|
||||
++ lib.optionals cfg.validateSopsFiles (
|
||||
lib.concatLists (
|
||||
lib.mapAttrsToList (name: secret: [
|
||||
{
|
||||
assertion = builtins.pathExists secret.sopsFile;
|
||||
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
builtins.isPath secret.sopsFile
|
||||
|| (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||
}
|
||||
{
|
||||
assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null;
|
||||
message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set";
|
||||
}
|
||||
{
|
||||
assertion = secret.gid != null && secret.gid != 0 -> secret.group == null;
|
||||
message = "In ${secret.name} exactly one of sops.group and sops.gid must be set";
|
||||
}
|
||||
]) cfg.secrets
|
||||
)
|
||||
);
|
||||
|
||||
sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) (
|
||||
lib.mkDefault "${pkgs.gnupg}/bin/gpg"
|
||||
);
|
||||
|
||||
sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg");
|
||||
|
||||
# When using sysusers we no longer be started as an activation script because those are started in initrd while sysusers is started later.
|
||||
# When using sysusers we no longer are started as an activation script because those are started in initrd while sysusers is started later.
|
||||
systemd.services.sops-install-secrets = lib.mkIf (regularSecrets != { } && useSystemdActivation) {
|
||||
wantedBy = [ "sysinit.target" ];
|
||||
wantedBy = [ "sysinit.target" ];
|
||||
after = [ "systemd-sysusers.service" ];
|
||||
environment = cfg.environment;
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
|
|
@ -338,27 +447,43 @@ in {
|
|||
};
|
||||
|
||||
system.activationScripts = {
|
||||
setupSecrets = lib.mkIf (regularSecrets != {} && !useSystemdActivation) (lib.stringAfter ([ "specialfs" "users" "groups" ] ++ lib.optional cfg.age.generateKey "generate-age-key") ''
|
||||
[ -e /run/current-system ] || echo setting up secrets...
|
||||
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"}
|
||||
'' // lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
});
|
||||
setupSecrets = lib.mkIf (regularSecrets != { } && !useSystemdActivation) (
|
||||
lib.stringAfter
|
||||
(
|
||||
[
|
||||
"specialfs"
|
||||
"users"
|
||||
"groups"
|
||||
]
|
||||
++ lib.optional cfg.age.generateKey "generate-age-key"
|
||||
)
|
||||
''
|
||||
[ -e /run/current-system ] || echo setting up secrets...
|
||||
${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"}
|
||||
''
|
||||
// lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
}
|
||||
);
|
||||
|
||||
generate-age-key = let
|
||||
escapedKeyFile = lib.escapeShellArg cfg.age.keyFile;
|
||||
in lib.mkIf cfg.age.generateKey (lib.stringAfter [] ''
|
||||
if [[ ! -f ${escapedKeyFile} ]]; then
|
||||
echo generating machine-specific age key...
|
||||
mkdir -p $(dirname ${escapedKeyFile})
|
||||
# age-keygen sets 0600 by default, no need to chmod.
|
||||
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile}
|
||||
fi
|
||||
'');
|
||||
generate-age-key =
|
||||
let
|
||||
escapedKeyFile = lib.escapeShellArg cfg.age.keyFile;
|
||||
in
|
||||
lib.mkIf cfg.age.generateKey (
|
||||
lib.stringAfter [ ] ''
|
||||
if [[ ! -f ${escapedKeyFile} ]]; then
|
||||
echo generating machine-specific age key...
|
||||
mkdir -p $(dirname ${escapedKeyFile})
|
||||
# age-keygen sets 0600 by default, no need to chmod.
|
||||
${pkgs.age}/bin/age-keygen -o ${escapedKeyFile}
|
||||
fi
|
||||
''
|
||||
);
|
||||
};
|
||||
})
|
||||
{
|
||||
system.build.sops-nix-manifest = manifest;
|
||||
system.build.sops-nix-manifest = manifest;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,34 @@
|
|||
{ writeTextFile, cfg }:
|
||||
|
||||
suffix: secrets: extraJson:
|
||||
suffix: secrets: templates: extraJson:
|
||||
|
||||
writeTextFile {
|
||||
name = "manifest${suffix}.json";
|
||||
text = builtins.toJSON ({
|
||||
secrets = builtins.attrValues secrets;
|
||||
# Does this need to be configurable?
|
||||
secretsMountPoint = "/run/secrets.d";
|
||||
symlinkPath = "/run/secrets";
|
||||
keepGenerations = cfg.keepGenerations;
|
||||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
useTmpfs = cfg.useTmpfs;
|
||||
userMode = false;
|
||||
logging = {
|
||||
keyImport = builtins.elem "keyImport" cfg.log;
|
||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||
};
|
||||
} // extraJson);
|
||||
text = builtins.toJSON (
|
||||
{
|
||||
secrets = builtins.attrValues secrets;
|
||||
templates = builtins.attrValues templates;
|
||||
# Does this need to be configurable?
|
||||
secretsMountPoint = "/run/secrets.d";
|
||||
symlinkPath = "/run/secrets";
|
||||
keepGenerations = cfg.keepGenerations;
|
||||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
useTmpfs = cfg.useTmpfs;
|
||||
placeholderBySecretName = cfg.placeholder;
|
||||
userMode = false;
|
||||
logging = {
|
||||
keyImport = builtins.elem "keyImport" cfg.log;
|
||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||
};
|
||||
}
|
||||
// extraJson
|
||||
);
|
||||
checkPhase = ''
|
||||
${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out"
|
||||
${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${
|
||||
if cfg.validateSopsFiles then "sopsfile" else "manifest"
|
||||
} "$out"
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
{ lib, options, config, pkgs, ... }:
|
||||
{
|
||||
lib,
|
||||
options,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.sops;
|
||||
secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets;
|
||||
templatesForUsers = { }; # We do not currently support `neededForUsers` for templates.
|
||||
manifestFor = pkgs.callPackage ../manifest-for.nix {
|
||||
inherit cfg;
|
||||
inherit (pkgs) writeTextFile;
|
||||
|
|
@ -9,49 +16,59 @@ let
|
|||
withEnvironment = import ../with-environment.nix {
|
||||
inherit cfg lib;
|
||||
};
|
||||
manifestForUsers = manifestFor "-for-users" secretsForUsers {
|
||||
manifestForUsers = manifestFor "-for-users" secretsForUsers templatesForUsers {
|
||||
secretsMountPoint = "/run/secrets-for-users.d";
|
||||
symlinkPath = "/run/secrets-for-users";
|
||||
};
|
||||
sysusersEnabled = options.systemd ? sysusers && config.systemd.sysusers.enable;
|
||||
useSystemdActivation = sysusersEnabled ||
|
||||
(options.services ? userborn && config.services.userborn.enable);
|
||||
useSystemdActivation =
|
||||
sysusersEnabled || (options.services ? userborn && config.services.userborn.enable);
|
||||
in
|
||||
{
|
||||
systemd.services.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != { } && useSystemdActivation) {
|
||||
wantedBy = [ "systemd-sysusers.service" ];
|
||||
before = [ "systemd-sysusers.service" ];
|
||||
environment = cfg.environment;
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
systemd.services.sops-install-secrets-for-users =
|
||||
lib.mkIf (secretsForUsers != { } && useSystemdActivation)
|
||||
{
|
||||
wantedBy = [ "systemd-sysusers.service" ];
|
||||
before = [ "systemd-sysusers.service" ];
|
||||
environment = cfg.environment;
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ];
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ];
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
};
|
||||
|
||||
system.activationScripts = lib.mkIf (secretsForUsers != { } && !useSystemdActivation) {
|
||||
setupSecretsForUsers = lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") ''
|
||||
[ -e /run/current-system ] || echo setting up secrets for users...
|
||||
${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"}
|
||||
'' // lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
};
|
||||
setupSecretsForUsers =
|
||||
lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") ''
|
||||
[ -e /run/current-system ] || echo setting up secrets for users...
|
||||
${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"}
|
||||
''
|
||||
// lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
};
|
||||
|
||||
users.deps = [ "setupSecretsForUsers" ];
|
||||
};
|
||||
|
||||
assertions = [{
|
||||
assertion = (lib.filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == { };
|
||||
message = "neededForUsers cannot be used for secrets that are not root-owned";
|
||||
} {
|
||||
assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers;
|
||||
message = ''
|
||||
systemd.sysusers.enable in combination with sops.secrets.<name>.neededForUsers can only work with config.users.mutableUsers enabled.
|
||||
See https://github.com/Mic92/sops-nix/issues/475
|
||||
'';
|
||||
}];
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
(lib.filterAttrs (
|
||||
_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")
|
||||
) secretsForUsers) == { };
|
||||
message = "neededForUsers cannot be used for secrets that are not root-owned";
|
||||
}
|
||||
{
|
||||
assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers;
|
||||
message = ''
|
||||
systemd.sysusers.enable in combination with sops.secrets.<name>.neededForUsers can only work with config.users.mutableUsers enabled.
|
||||
See https://github.com/Mic92/sops-nix/issues/475
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
system.build.sops-nix-users-manifest = manifestForUsers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,123 @@
|
|||
{ config, pkgs, lib, options, ... }:
|
||||
with lib;
|
||||
with lib.types;
|
||||
with builtins;
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.sops;
|
||||
secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets;
|
||||
inherit (lib)
|
||||
mkOption
|
||||
mkDefault
|
||||
mapAttrs
|
||||
types
|
||||
;
|
||||
|
||||
users = config.users.users;
|
||||
in {
|
||||
in
|
||||
{
|
||||
options.sops = {
|
||||
templates = mkOption {
|
||||
description = "Templates for secret files";
|
||||
type = attrsOf (submodule ({ config, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = singleLineStr;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets-rendered
|
||||
'';
|
||||
};
|
||||
path = mkOption {
|
||||
description = "Path where the rendered file will be placed";
|
||||
type = singleLineStr;
|
||||
default = "/run/secrets-rendered/${config.name}";
|
||||
};
|
||||
content = mkOption {
|
||||
type = lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Content of the file
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = singleLineStr;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the rendered secret file in octal.
|
||||
'';
|
||||
};
|
||||
owner = mkOption {
|
||||
type = singleLineStr;
|
||||
default = "root";
|
||||
description = ''
|
||||
User of the file.
|
||||
'';
|
||||
};
|
||||
group = mkOption {
|
||||
type = singleLineStr;
|
||||
default = users.${config.owner}.group;
|
||||
defaultText = ''config.users.users.''${cfg.owner}.group'';
|
||||
description = ''
|
||||
Group of the file.
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
default = pkgs.writeText config.name config.content;
|
||||
defaultText = ''pkgs.writeText config.name config.content'';
|
||||
example = "./configuration-template.conf";
|
||||
description = ''
|
||||
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}));
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = config._module.args.name;
|
||||
description = ''
|
||||
Name of the file used in /run/secrets/rendered
|
||||
'';
|
||||
};
|
||||
path = mkOption {
|
||||
description = "Path where the rendered file will be placed";
|
||||
type = types.singleLineStr;
|
||||
# Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go`
|
||||
default = "/run/secrets/rendered/${config.name}";
|
||||
};
|
||||
content = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Content of the file
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "0400";
|
||||
description = ''
|
||||
Permissions mode of the rendered secret file in octal.
|
||||
'';
|
||||
};
|
||||
owner = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = "root";
|
||||
description = ''
|
||||
User of the file.
|
||||
'';
|
||||
};
|
||||
group = mkOption {
|
||||
type = types.singleLineStr;
|
||||
default = users.${config.owner}.group;
|
||||
defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group'';
|
||||
description = ''
|
||||
Group of the file.
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
default = pkgs.writeText config.name config.content;
|
||||
defaultText = lib.literalExpression ''pkgs.writeText config.name config.content'';
|
||||
example = "./configuration-template.conf";
|
||||
description = ''
|
||||
File used as the template. When this value is specified, `sops.templates.<name>.content` is ignored.
|
||||
'';
|
||||
};
|
||||
restartUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be restarted when the rendered template changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
reloadUnits = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "sshd.service" ];
|
||||
description = ''
|
||||
Names of units that should be reloaded when the rendered template changes.
|
||||
This works the same way as <xref linkend="opt-systemd.services._name_.reloadTriggers" />.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
placeholder = mkOption {
|
||||
type = attrsOf (mkOptionType {
|
||||
name = "coercibleToString";
|
||||
description = "value that can be coerced to string";
|
||||
check = strings.isConvertibleWithToString;
|
||||
merge = mergeEqualOption;
|
||||
});
|
||||
type = types.attrsOf (
|
||||
types.mkOptionType {
|
||||
name = "coercibleToString";
|
||||
description = "value that can be coerced to string";
|
||||
check = lib.strings.isConvertibleWithToString;
|
||||
merge = lib.mergeEqualOption;
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
visible = false;
|
||||
};
|
||||
};
|
||||
|
||||
config = optionalAttrs (options ? sops.secrets)
|
||||
(mkIf (config.sops.templates != { }) {
|
||||
sops.placeholder = mapAttrs
|
||||
(name: _: mkDefault "<SOPS:${hashString "sha256" name}:PLACEHOLDER>")
|
||||
config.sops.secrets;
|
||||
|
||||
system.activationScripts.renderSecrets = mkIf (cfg.templates != { })
|
||||
(stringAfter ([ "setupSecrets" ]
|
||||
++ optional (secretsForUsers != { }) "setupSecretsForUsers") ''
|
||||
echo Setting up sops templates...
|
||||
${concatMapStringsSep "\n" (name:
|
||||
let
|
||||
tpl = config.sops.templates.${name};
|
||||
substitute = pkgs.writers.writePython3 "substitute" { }
|
||||
(readFile ./subs.py);
|
||||
subst-pairs = pkgs.writeText "pairs" (concatMapStringsSep "\n"
|
||||
(name:
|
||||
"${toString config.sops.placeholder.${name}} ${
|
||||
config.sops.secrets.${name}.path
|
||||
}") (attrNames config.sops.secrets));
|
||||
in ''
|
||||
mkdir -p "${dirOf tpl.path}"
|
||||
(umask 077; ${substitute} ${tpl.file} ${subst-pairs} > ${tpl.path})
|
||||
chmod "${tpl.mode}" "${tpl.path}"
|
||||
chown "${tpl.owner}:${tpl.group}" "${tpl.path}"
|
||||
'') (attrNames config.sops.templates)}
|
||||
'');
|
||||
});
|
||||
config = lib.optionalAttrs (options ? sops.secrets) (
|
||||
lib.mkIf (config.sops.templates != { }) {
|
||||
sops.placeholder = mapAttrs (
|
||||
name: _: mkDefault "<SOPS:${builtins.hashString "sha256" name}:PLACEHOLDER>"
|
||||
) config.sops.secrets;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
from sys import argv
|
||||
|
||||
|
||||
def substitute(target: str, subst: str) -> str:
|
||||
with open(target) as f:
|
||||
content = f.read()
|
||||
|
||||
with open(subst) as f:
|
||||
subst_pairs = f.read().splitlines()
|
||||
|
||||
for pair in subst_pairs:
|
||||
placeholder, path = pair.split()
|
||||
if placeholder in content:
|
||||
with open(path) as f:
|
||||
content = content.replace(placeholder, f.read())
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def main() -> None:
|
||||
target = argv[1]
|
||||
subst = argv[2]
|
||||
print(substitute(target, subst))
|
||||
|
||||
|
||||
main()
|
||||
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
sopsCall:
|
||||
|
||||
if cfg.environment == {} then
|
||||
if cfg.environment == { } then
|
||||
sopsCall
|
||||
else ''
|
||||
(
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)}
|
||||
${sopsCall}
|
||||
)
|
||||
''
|
||||
else
|
||||
''
|
||||
(
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)}
|
||||
${sopsCall}
|
||||
)
|
||||
''
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
{ makeSetupHook, gnupg, sops, lib }:
|
||||
{
|
||||
makeSetupHook,
|
||||
gnupg,
|
||||
sops,
|
||||
lib,
|
||||
}:
|
||||
|
||||
let
|
||||
# FIXME: drop after 23.05
|
||||
propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs";
|
||||
propagatedBuildInputs =
|
||||
if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then
|
||||
"deps"
|
||||
else
|
||||
"propagatedBuildInputs";
|
||||
in
|
||||
(makeSetupHook {
|
||||
name = "sops-import-keys-hook";
|
||||
substitutions = {
|
||||
gpg = "${gnupg}/bin/gpg";
|
||||
};
|
||||
${propagatedBuildInputs} = [ sops gnupg ];
|
||||
${propagatedBuildInputs} = [
|
||||
sops
|
||||
gnupg
|
||||
];
|
||||
} ./sops-import-keys-hook.bash)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# shell.nix
|
||||
with import <nixpkgs> {};
|
||||
with import <nixpkgs> { };
|
||||
mkShell {
|
||||
sopsPGPKeyDirs = [
|
||||
"./keys"
|
||||
|
|
@ -10,6 +10,6 @@ mkShell {
|
|||
];
|
||||
sopsCreateGPGHome = "1";
|
||||
nativeBuildInputs = [
|
||||
(pkgs.callPackage ../../.. {}).sops-import-keys-hook
|
||||
(pkgs.callPackage ../../.. { }).sops-import-keys-hook
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
{ stdenv, lib, makeWrapper, gnupg, coreutils, util-linux, unixtools }:
|
||||
{
|
||||
stdenv,
|
||||
lib,
|
||||
makeWrapper,
|
||||
gnupg,
|
||||
coreutils,
|
||||
util-linux,
|
||||
unixtools,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
name = "sops-init-gpg-key";
|
||||
|
|
@ -11,9 +19,14 @@ stdenv.mkDerivation {
|
|||
installPhase = ''
|
||||
install -m755 -D $src $out/bin/sops-init-gpg-key
|
||||
wrapProgram $out/bin/sops-init-gpg-key \
|
||||
--prefix PATH : ${lib.makeBinPath [
|
||||
coreutils util-linux gnupg unixtools.hostname
|
||||
]}
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
coreutils
|
||||
util-linux
|
||||
gnupg
|
||||
unixtools.hostname
|
||||
]
|
||||
}
|
||||
'';
|
||||
|
||||
doInstallCheck = true;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
|
@ -71,21 +70,16 @@ func MountSecretFs(mountpoint string, keysGID int, _useTmpfs bool, userMode bool
|
|||
size := mb * 1024 * 1024 / 512 // size in sectors a 512 bytes
|
||||
cmd := exec.Command("hdiutil", "attach", "-nomount", fmt.Sprintf("ram://%d", int(size)))
|
||||
out, err := cmd.Output() // /dev/diskN
|
||||
log.Printf("%q\n", string(out))
|
||||
diskpath := strings.TrimRight(string(out[:]), " \t\n")
|
||||
log.Printf("%q\n", diskpath)
|
||||
log.Printf("hdiutil attach ret %v. out: %s", err, diskpath)
|
||||
|
||||
// format as hfs
|
||||
out, err = exec.Command("newfs_hfs", "-s", diskpath).Output()
|
||||
log.Printf("newfs_hfs ret %v. out: %s", err, out)
|
||||
|
||||
// "posix" mount takes `struct hfs_mount_args` which we dont have bindings for at hand.
|
||||
// See https://stackoverflow.com/a/49048846/4108673
|
||||
// err = unix.Mount("hfs", mountpoint, unix.MNT_NOEXEC|unix.MNT_NODEV, mount_args)
|
||||
// Instead we call:
|
||||
out, err = exec.Command("mount", "-t", "hfs", "-o", "nobrowse,nodev,nosuid,-m=0751", diskpath, mountpoint).Output()
|
||||
log.Printf("mount ret %v. out: %s", err, out)
|
||||
|
||||
// There is no documented way to check for memfs mountpoint. Thus we place a file.
|
||||
path := mountpoint + "/sops-nix-secretfs"
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
{ lib, buildGoModule, stdenv, vendorHash, go, callPackages }:
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
stdenv,
|
||||
vendorHash,
|
||||
go,
|
||||
}:
|
||||
buildGoModule {
|
||||
pname = "sops-install-secrets";
|
||||
version = "0.0.1";
|
||||
|
||||
src = lib.sourceByRegex ../.. [ "go\.(mod|sum)" "pkgs" "pkgs/sops-install-secrets.*" ];
|
||||
src = lib.sourceByRegex ../.. [
|
||||
"go\.(mod|sum)"
|
||||
"pkgs"
|
||||
"pkgs/sops-install-secrets.*"
|
||||
];
|
||||
|
||||
subPackages = [ "pkgs/sops-install-secrets" ];
|
||||
|
||||
# requires root privileges for tests
|
||||
doCheck = false;
|
||||
|
||||
passthru.tests = callPackages ./nixos-test.nix { };
|
||||
outputs = [ "out" ] ++ lib.lists.optionals (stdenv.isLinux) [ "unittest" ];
|
||||
|
||||
outputs = [ "out" ] ++
|
||||
lib.lists.optionals (stdenv.isLinux) [ "unittest" ];
|
||||
|
||||
postInstall = ''
|
||||
go test -c ./pkgs/sops-install-secrets
|
||||
'' + lib.optionalString (stdenv.isLinux) ''
|
||||
# *.test is only tested on linux. $unittest does not exist on darwin.
|
||||
install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test
|
||||
# newer versions of nixpkgs no longer require this step
|
||||
if command -v remove-references-to; then
|
||||
remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test
|
||||
fi
|
||||
'';
|
||||
postInstall =
|
||||
''
|
||||
go test -c ./pkgs/sops-install-secrets
|
||||
''
|
||||
+ lib.optionalString (stdenv.isLinux) ''
|
||||
# *.test is only tested on linux. $unittest does not exist on darwin.
|
||||
install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test
|
||||
# newer versions of nixpkgs no longer require this step
|
||||
if command -v remove-references-to; then
|
||||
remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test
|
||||
fi
|
||||
'';
|
||||
|
||||
inherit vendorHash;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
|
@ -29,8 +30,10 @@ type secret struct {
|
|||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Path string `json:"path"`
|
||||
Owner string `json:"owner"`
|
||||
Group string `json:"group"`
|
||||
Owner *string `json:"owner,omitempty"`
|
||||
UID int `json:"uid"`
|
||||
Group *string `json:"group,omitempty"`
|
||||
GID int `json:"gid"`
|
||||
SopsFile string `json:"sopsFile"`
|
||||
Format FormatType `json:"format"`
|
||||
Mode string `json:"mode"`
|
||||
|
|
@ -47,18 +50,39 @@ type loggingConfig struct {
|
|||
SecretChanges bool `json:"secretChanges"`
|
||||
}
|
||||
|
||||
type template struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Owner *string `json:"owner,omitempty"`
|
||||
UID int `json:"uid"`
|
||||
Group *string `json:"group,omitempty"`
|
||||
GID int `json:"gid"`
|
||||
File string `json:"file"`
|
||||
RestartUnits []string `json:"restartUnits"`
|
||||
ReloadUnits []string `json:"reloadUnits"`
|
||||
value []byte
|
||||
mode os.FileMode
|
||||
content string
|
||||
owner int
|
||||
group int
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Secrets []secret `json:"secrets"`
|
||||
SecretsMountPoint string `json:"secretsMountPoint"`
|
||||
SymlinkPath string `json:"symlinkPath"`
|
||||
KeepGenerations int `json:"keepGenerations"`
|
||||
SSHKeyPaths []string `json:"sshKeyPaths"`
|
||||
GnupgHome string `json:"gnupgHome"`
|
||||
AgeKeyFile string `json:"ageKeyFile"`
|
||||
AgeSSHKeyPaths []string `json:"ageSshKeyPaths"`
|
||||
UseTmpfs bool `json:"useTmpfs"`
|
||||
UserMode bool `json:"userMode"`
|
||||
Logging loggingConfig `json:"logging"`
|
||||
Secrets []secret `json:"secrets"`
|
||||
Templates []template `json:"templates"`
|
||||
PlaceholderBySecretName map[string]string `json:"placeholderBySecretName"`
|
||||
SecretsMountPoint string `json:"secretsMountPoint"`
|
||||
SymlinkPath string `json:"symlinkPath"`
|
||||
KeepGenerations int `json:"keepGenerations"`
|
||||
SSHKeyPaths []string `json:"sshKeyPaths"`
|
||||
GnupgHome string `json:"gnupgHome"`
|
||||
AgeKeyFile string `json:"ageKeyFile"`
|
||||
AgeSSHKeyPaths []string `json:"ageSshKeyPaths"`
|
||||
UseTmpfs bool `json:"useTmpfs"`
|
||||
UserMode bool `json:"userMode"`
|
||||
Logging loggingConfig `json:"logging"`
|
||||
}
|
||||
|
||||
type secretFile struct {
|
||||
|
|
@ -126,12 +150,16 @@ type options struct {
|
|||
}
|
||||
|
||||
type appContext struct {
|
||||
manifest manifest
|
||||
secretFiles map[string]secretFile
|
||||
checkMode CheckMode
|
||||
ignorePasswd bool
|
||||
manifest manifest
|
||||
secretFiles map[string]secretFile
|
||||
secretByPlaceholder map[string]*secret
|
||||
checkMode CheckMode
|
||||
ignorePasswd bool
|
||||
}
|
||||
|
||||
// Keep this in sync with `modules/sops/templates/default.nix`
|
||||
const RenderedSubdir string = "rendered"
|
||||
|
||||
func readManifest(path string) (*manifest, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
|
@ -146,51 +174,51 @@ func readManifest(path string) (*manifest, error) {
|
|||
return &m, nil
|
||||
}
|
||||
|
||||
func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, secret *secret) bool {
|
||||
func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, group int) bool {
|
||||
validUG := true
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
validUG = validUG && int(stat.Uid) == secret.owner
|
||||
validUG = validUG && int(stat.Gid) == secret.group
|
||||
validUG = validUG && int(stat.Uid) == owner
|
||||
validUG = validUG && int(stat.Gid) == group
|
||||
} else {
|
||||
panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.")
|
||||
}
|
||||
return linkTarget == targetFile && validUG
|
||||
}
|
||||
|
||||
func symlinkSecret(targetFile string, secret *secret, userMode bool) error {
|
||||
func createSymlink(targetFile string, path string, owner int, group int, userMode bool) error {
|
||||
for {
|
||||
stat, err := os.Lstat(secret.Path)
|
||||
stat, err := os.Lstat(path)
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.Symlink(targetFile, secret.Path); err != nil {
|
||||
return fmt.Errorf("cannot create symlink '%s': %w", secret.Path, err)
|
||||
if err = os.Symlink(targetFile, path); err != nil {
|
||||
return fmt.Errorf("cannot create symlink '%s': %w", path, err)
|
||||
}
|
||||
if !userMode {
|
||||
if err = SecureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil {
|
||||
return fmt.Errorf("cannot chown symlink '%s': %w", secret.Path, err)
|
||||
if err = SecureSymlinkChown(path, targetFile, owner, group); err != nil {
|
||||
return fmt.Errorf("cannot chown symlink '%s': %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("cannot stat '%s': %w", secret.Path, err)
|
||||
return fmt.Errorf("cannot stat '%s': %w", path, err)
|
||||
}
|
||||
if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
linkTarget, err := os.Readlink(secret.Path)
|
||||
linkTarget, err := os.Readlink(path)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("cannot read symlink '%s': %w", secret.Path, err)
|
||||
} else if linksAreEqual(linkTarget, targetFile, stat, secret) {
|
||||
return fmt.Errorf("cannot read symlink '%s': %w", path, err)
|
||||
} else if linksAreEqual(linkTarget, targetFile, stat, owner, group) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := os.Remove(secret.Path); err != nil {
|
||||
return fmt.Errorf("cannot override %s: %w", secret.Path, err)
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("cannot override %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error {
|
||||
for i, secret := range secrets {
|
||||
func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates []template, userMode bool) error {
|
||||
for _, secret := range secrets {
|
||||
targetFile := filepath.Join(targetDir, secret.Name)
|
||||
if targetFile == secret.Path {
|
||||
continue
|
||||
|
|
@ -199,10 +227,25 @@ func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error {
|
|||
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err)
|
||||
}
|
||||
if err := symlinkSecret(targetFile, &secrets[i], userMode); err != nil {
|
||||
if err := createSymlink(targetFile, secret.Path, secret.owner, secret.group, userMode); err != nil {
|
||||
return fmt.Errorf("failed to symlink secret '%s': %w", secret.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, template := range templates {
|
||||
targetFile := filepath.Join(targetDir, RenderedSubdir, template.Name)
|
||||
if targetFile == template.Path {
|
||||
continue
|
||||
}
|
||||
parent := filepath.Dir(template.Path)
|
||||
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("cannot create parent directory of '%s': %w", template.Path, err)
|
||||
}
|
||||
if err := createSymlink(targetFile, template.Path, template.owner, template.group, userMode); err != nil {
|
||||
return fmt.Errorf("failed to symlink template '%s': %w", template.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -294,12 +337,20 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
|
|||
case Binary, Dotenv, Ini:
|
||||
sourceFile.binary = plain
|
||||
case Yaml:
|
||||
if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
|
||||
return fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err)
|
||||
if s.Key == "" {
|
||||
sourceFile.binary = plain
|
||||
} else {
|
||||
if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
|
||||
return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
|
||||
}
|
||||
}
|
||||
case JSON:
|
||||
if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
|
||||
return fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err)
|
||||
if s.Key == "" {
|
||||
sourceFile.binary = plain
|
||||
} else {
|
||||
if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
|
||||
return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("secret of type %s in %s is not supported", s.Format, s.SopsFile)
|
||||
|
|
@ -309,11 +360,15 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
|
|||
case Binary, Dotenv, Ini:
|
||||
s.value = sourceFile.binary
|
||||
case Yaml, JSON:
|
||||
strVal, err := recurseSecretKey(s.Format, sourceFile.data, s.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
|
||||
if s.Key == "" {
|
||||
s.value = sourceFile.binary
|
||||
} else {
|
||||
strVal, err := recurseSecretKey(s.Format, sourceFile.data, s.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
|
||||
}
|
||||
s.value = []byte(strVal)
|
||||
}
|
||||
s.value = []byte(strVal)
|
||||
}
|
||||
sourceFiles[s.SopsFile] = sourceFile
|
||||
return nil
|
||||
|
|
@ -366,24 +421,30 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us
|
|||
return &dir, nil
|
||||
}
|
||||
|
||||
func createParentDirs(parent string, target string, keysGID int, userMode bool) error {
|
||||
dirs := strings.Split(filepath.Dir(target), "/")
|
||||
pathSoFar := parent
|
||||
for _, dir := range dirs {
|
||||
pathSoFar = filepath.Join(pathSoFar, dir)
|
||||
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
|
||||
return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
|
||||
}
|
||||
if !userMode {
|
||||
if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil {
|
||||
return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error {
|
||||
for _, secret := range secrets {
|
||||
fp := filepath.Join(secretDir, secret.Name)
|
||||
|
||||
dirs := strings.Split(filepath.Dir(secret.Name), "/")
|
||||
pathSoFar := secretDir
|
||||
for _, dir := range dirs {
|
||||
pathSoFar = filepath.Join(pathSoFar, dir)
|
||||
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
|
||||
return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, fp, err)
|
||||
}
|
||||
if !userMode {
|
||||
if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil {
|
||||
return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, fp, err)
|
||||
}
|
||||
}
|
||||
if err := createParentDirs(secretDir, secret.Name, keysGID, userMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fp, []byte(secret.value), secret.mode); err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", fp, err)
|
||||
}
|
||||
|
|
@ -460,7 +521,7 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse ini of '%s': %w", s.SopsFile, err)
|
||||
}
|
||||
// TODO: we do not acctually check the contents of the ini here...
|
||||
// TODO: we do not actually check the contents of the ini here...
|
||||
}
|
||||
|
||||
return &secretFile{
|
||||
|
|
@ -476,7 +537,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
|
|||
s.Name, s.SopsFile, s.Format,
|
||||
file.firstSecret.Format, file.firstSecret.Name)
|
||||
}
|
||||
if app.checkMode != Manifest && (s.Format != Binary && s.Format != Dotenv && s.Format != Ini) {
|
||||
if app.checkMode != Manifest && !(s.Format == Binary || s.Format == Dotenv || s.Format == Ini) && s.Key != "" {
|
||||
_, err := recurseSecretKey(s.Format, file.keys, s.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
|
||||
|
|
@ -485,37 +546,68 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) validateSecret(secret *secret) error {
|
||||
mode, err := strconv.ParseUint(secret.Mode, 8, 16)
|
||||
func validateMode(mode string) (os.FileMode, error) {
|
||||
parsed, err := strconv.ParseUint(mode, 8, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid number in mode: %d: %w", mode, err)
|
||||
return 0, fmt.Errorf("invalid number in mode: %s: %w", mode, err)
|
||||
}
|
||||
secret.mode = os.FileMode(mode)
|
||||
return os.FileMode(parsed), nil
|
||||
}
|
||||
|
||||
func validateOwner(owner string) (int, error) {
|
||||
lookedUp, err := user.Lookup(owner)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to lookup user '%s': %w", owner, err)
|
||||
}
|
||||
ownerNr, err := strconv.ParseUint(lookedUp.Uid, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse uid %s: %w", lookedUp.Uid, err)
|
||||
}
|
||||
return int(ownerNr), nil
|
||||
}
|
||||
|
||||
func validateGroup(group string) (int, error) {
|
||||
lookedUp, err := user.LookupGroup(group)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to lookup group '%s': %w", group, err)
|
||||
}
|
||||
groupNr, err := strconv.ParseUint(lookedUp.Gid, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse gid %s: %w", lookedUp.Gid, err)
|
||||
}
|
||||
return int(groupNr), nil
|
||||
}
|
||||
|
||||
func (app *appContext) validateSecret(secret *secret) error {
|
||||
mode, err := validateMode(secret.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.mode = mode
|
||||
|
||||
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
||||
secret.owner = 0
|
||||
secret.group = 0
|
||||
} else if app.checkMode == Off || app.ignorePasswd {
|
||||
// we only access to the user/group during deployment
|
||||
owner, err := user.Lookup(secret.Owner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup user '%s': %w", secret.Owner, err)
|
||||
if secret.Owner == nil {
|
||||
secret.owner = secret.UID
|
||||
} else {
|
||||
owner, err := validateOwner(*secret.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.owner = owner
|
||||
}
|
||||
ownerNr, err := strconv.ParseUint(owner.Uid, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err)
|
||||
}
|
||||
secret.owner = int(ownerNr)
|
||||
|
||||
group, err := user.LookupGroup(secret.Group)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup group '%s': %w", secret.Group, err)
|
||||
if secret.Group == nil {
|
||||
secret.group = secret.GID
|
||||
} else {
|
||||
group, err := validateGroup(*secret.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.group = group
|
||||
}
|
||||
groupNr, err := strconv.ParseUint(group.Gid, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err)
|
||||
}
|
||||
secret.group = int(groupNr)
|
||||
}
|
||||
|
||||
if secret.Format == "" {
|
||||
|
|
@ -533,12 +625,79 @@ func (app *appContext) validateSecret(secret *secret) error {
|
|||
return err
|
||||
}
|
||||
app.secretFiles[secret.SopsFile] = *maybeFile
|
||||
|
||||
file = *maybeFile
|
||||
}
|
||||
|
||||
return app.validateSopsFile(secret, &file)
|
||||
}
|
||||
|
||||
func renderTemplates(templates []template, secretByPlaceholder map[string]*secret) {
|
||||
for i := range templates {
|
||||
template := &templates[i]
|
||||
rendered := renderTemplate(&template.content, secretByPlaceholder)
|
||||
template.value = []byte(rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplate(content *string, secretByPlaceholder map[string]*secret) string {
|
||||
rendered := *content
|
||||
for placeholder, secret := range secretByPlaceholder {
|
||||
rendered = strings.ReplaceAll(rendered, placeholder, string(secret.value))
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
func (app *appContext) validateTemplate(template *template) error {
|
||||
mode, err := validateMode(template.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
template.mode = mode
|
||||
|
||||
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
||||
template.owner = 0
|
||||
template.group = 0
|
||||
} else if app.checkMode == Off || app.ignorePasswd {
|
||||
if template.Owner == nil {
|
||||
template.owner = template.UID
|
||||
} else {
|
||||
owner, err := validateOwner(*template.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
template.owner = owner
|
||||
}
|
||||
|
||||
if template.Group == nil {
|
||||
template.group = template.GID
|
||||
} else {
|
||||
group, err := validateGroup(*template.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
template.group = group
|
||||
}
|
||||
}
|
||||
|
||||
var templateText string
|
||||
if template.Content != "" {
|
||||
templateText = template.Content
|
||||
} else if template.File != "" {
|
||||
templateBytes, err := os.ReadFile(template.File)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read %s: %w", template.File, err)
|
||||
}
|
||||
templateText = string(templateBytes)
|
||||
} else {
|
||||
return fmt.Errorf("neither content nor file was specified for template %s", template.Name)
|
||||
}
|
||||
|
||||
template.content = templateText
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) validateManifest() error {
|
||||
m := &app.manifest
|
||||
if m.GnupgHome != "" {
|
||||
|
|
@ -553,7 +712,22 @@ func (app *appContext) validateManifest() error {
|
|||
}
|
||||
|
||||
for i := range m.Secrets {
|
||||
if err := app.validateSecret(&m.Secrets[i]); err != nil {
|
||||
secret := &m.Secrets[i]
|
||||
if err := app.validateSecret(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The Nix module only defines placeholders for secrets if there are
|
||||
// templates.
|
||||
if len(m.Templates) > 0 {
|
||||
placeholder := m.PlaceholderBySecretName[secret.Name]
|
||||
app.secretByPlaceholder[placeholder] = secret
|
||||
}
|
||||
}
|
||||
|
||||
for i := range m.Templates {
|
||||
template := &m.Templates[i]
|
||||
if err := app.validateTemplate(template); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -743,7 +917,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc)
|
|||
return filepath.Walk(filename, symWalkFunc)
|
||||
}
|
||||
|
||||
func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret) error {
|
||||
func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error {
|
||||
var restart []string
|
||||
var reload []string
|
||||
|
||||
|
|
@ -751,6 +925,10 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s
|
|||
modifiedSecrets := make(map[string]bool)
|
||||
removedSecrets := make(map[string]bool)
|
||||
|
||||
newTemplates := make(map[string]bool)
|
||||
modifiedTemplates := make(map[string]bool)
|
||||
removedTemplates := make(map[string]bool)
|
||||
|
||||
// When the symlink path does not exist yet, we are being run in stage-2-init.sh
|
||||
// where switch-to-configuration is not run so the services would only be restarted
|
||||
// the next time switch-to-configuration is run.
|
||||
|
|
@ -789,8 +967,46 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s
|
|||
}
|
||||
}
|
||||
|
||||
// Find modified/new templates
|
||||
for _, template := range templates {
|
||||
oldPath := filepath.Join(symlinkPath, RenderedSubdir, template.Name)
|
||||
newPath := filepath.Join(secretDir, RenderedSubdir, template.Name)
|
||||
|
||||
// Read the old file
|
||||
oldData, err := os.ReadFile(oldPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// File did not exist before
|
||||
restart = append(restart, template.RestartUnits...)
|
||||
reload = append(reload, template.ReloadUnits...)
|
||||
newTemplates[template.Name] = true
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the new file
|
||||
newData, err := os.ReadFile(newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(oldData, newData) {
|
||||
restart = append(restart, template.RestartUnits...)
|
||||
reload = append(reload, template.ReloadUnits...)
|
||||
modifiedTemplates[template.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
writeLines := func(list []string, file string) error {
|
||||
if len(list) != 0 {
|
||||
if _, err := os.Stat(filepath.Dir(file)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -822,7 +1038,8 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s
|
|||
return nil
|
||||
}
|
||||
|
||||
// Find removed secrets
|
||||
// Find removed secrets/templates.
|
||||
symlinkRenderedPath := filepath.Join(symlinkPath, RenderedSubdir)
|
||||
err := symlinkWalk(symlinkPath, symlinkPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -830,42 +1047,67 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s
|
|||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
path = strings.TrimPrefix(path, symlinkPath+string(os.PathSeparator))
|
||||
for _, secret := range secrets {
|
||||
if secret.Name == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the path we're looking at isn't in `symlinkRenderedPath`, then
|
||||
// it's a secret.
|
||||
rel, err := filepath.Rel(symlinkRenderedPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isSecret := strings.HasPrefix(rel, "..")
|
||||
|
||||
if isSecret {
|
||||
path = strings.TrimPrefix(path, symlinkPath+string(os.PathSeparator))
|
||||
for _, secret := range secrets {
|
||||
if secret.Name == path {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
removedSecrets[path] = true
|
||||
} else {
|
||||
path = strings.TrimPrefix(path, symlinkRenderedPath+string(os.PathSeparator))
|
||||
for _, template := range templates {
|
||||
if template.Name == path {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
removedTemplates[path] = true
|
||||
}
|
||||
removedSecrets[path] = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output new/modified/removed secrets
|
||||
outputChanged := func(changed map[string]bool, regularPrefix, dryPrefix string) {
|
||||
// Output new/modified/removed secrets/templates
|
||||
outputChanged := func(noun string, changed map[string]bool, regularPrefix, dryPrefix string) {
|
||||
if len(changed) > 0 {
|
||||
s := ""
|
||||
if len(changed) != 1 {
|
||||
s = "s"
|
||||
}
|
||||
if isDry {
|
||||
fmt.Printf("%s secret%s: ", dryPrefix, s)
|
||||
fmt.Printf("%s %s%s: ", dryPrefix, noun, s)
|
||||
} else {
|
||||
fmt.Printf("%s secret%s: ", regularPrefix, s)
|
||||
fmt.Printf("%s %s%s: ", regularPrefix, noun, s)
|
||||
}
|
||||
comma := ""
|
||||
for name := range changed {
|
||||
fmt.Printf("%s%s", comma, name)
|
||||
comma = ", "
|
||||
|
||||
// Sort the output for deterministic behavior.
|
||||
keys := make([]string, 0, len(changed))
|
||||
for key := range changed {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
fmt.Println()
|
||||
sort.Strings(keys)
|
||||
|
||||
fmt.Println(strings.Join(keys, ", "))
|
||||
}
|
||||
}
|
||||
outputChanged(newSecrets, "adding", "would add")
|
||||
outputChanged(modifiedSecrets, "modifying", "would modify")
|
||||
outputChanged(removedSecrets, "removing", "would remove")
|
||||
outputChanged("secret", newSecrets, "adding", "would add")
|
||||
outputChanged("secret", modifiedSecrets, "modifying", "would modify")
|
||||
outputChanged("secret", removedSecrets, "removing", "would remove")
|
||||
outputChanged("rendered secret", newTemplates, "adding", "would add")
|
||||
outputChanged("rendered secret", modifiedTemplates, "modifying", "would modify")
|
||||
outputChanged("rendered secret", removedTemplates, "removing", "would remove")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -937,6 +1179,26 @@ func replaceRuntimeDir(path, rundir string) (ret string) {
|
|||
return
|
||||
}
|
||||
|
||||
func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error {
|
||||
for _, template := range templates {
|
||||
fp := filepath.Join(targetDir, template.Name)
|
||||
|
||||
if err := createParentDirs(targetDir, template.Name, keysGID, userMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fp, []byte(template.value), template.mode); err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", fp, err)
|
||||
}
|
||||
if !userMode {
|
||||
if err := os.Chown(fp, template.owner, template.group); err != nil {
|
||||
return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, template.owner, template.group, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installSecrets(args []string) error {
|
||||
opts, err := parseFlags(args)
|
||||
if err != nil {
|
||||
|
|
@ -965,10 +1227,11 @@ func installSecrets(args []string) error {
|
|||
}
|
||||
|
||||
app := appContext{
|
||||
manifest: *manifest,
|
||||
checkMode: opts.checkMode,
|
||||
ignorePasswd: opts.ignorePasswd,
|
||||
secretFiles: make(map[string]secretFile),
|
||||
manifest: *manifest,
|
||||
checkMode: opts.checkMode,
|
||||
ignorePasswd: opts.ignorePasswd,
|
||||
secretFiles: make(map[string]secretFile),
|
||||
secretByPlaceholder: make(map[string]*secret),
|
||||
}
|
||||
|
||||
if err = app.validateManifest(); err != nil {
|
||||
|
|
@ -1042,10 +1305,13 @@ func installSecrets(args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err = decryptSecrets(manifest.Secrets); err != nil {
|
||||
if err := decryptSecrets(manifest.Secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that the secrets are decrypted, we can render the templates.
|
||||
renderTemplates(manifest.Templates, app.secretByPlaceholder)
|
||||
|
||||
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGID, manifest.UserMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare new secrets directory: %w", err)
|
||||
|
|
@ -1053,8 +1319,13 @@ func installSecrets(args []string) error {
|
|||
if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil {
|
||||
return fmt.Errorf("cannot write secrets: %w", err)
|
||||
}
|
||||
|
||||
if err := writeTemplates(path.Join(*secretDir, RenderedSubdir), manifest.Templates, keysGID, manifest.UserMode); err != nil {
|
||||
return fmt.Errorf("cannot render templates: %w", err)
|
||||
}
|
||||
|
||||
if !manifest.UserMode {
|
||||
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil {
|
||||
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets, manifest.Templates); err != nil {
|
||||
return fmt.Errorf("cannot request units to restart: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1062,7 +1333,7 @@ func installSecrets(args []string) error {
|
|||
if isDry {
|
||||
return nil
|
||||
}
|
||||
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.UserMode); err != nil {
|
||||
if err := symlinkSecretsAndTemplates(manifest.SymlinkPath, manifest.Secrets, manifest.Templates, manifest.UserMode); err != nil {
|
||||
return fmt.Errorf("failed to prepare symlinks to secret store: %w", err)
|
||||
}
|
||||
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
|
||||
|
|
|
|||
|
|
@ -98,12 +98,14 @@ func testGPG(t *testing.T) {
|
|||
}
|
||||
}()
|
||||
|
||||
nobody := "nobody"
|
||||
nogroup := "nogroup"
|
||||
// should create a symlink
|
||||
yamlSecret := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
Owner: &nobody,
|
||||
Group: &nogroup,
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: path.Join(testdir.path, "test-target"),
|
||||
Mode: "0400",
|
||||
|
|
@ -112,12 +114,13 @@ func testGPG(t *testing.T) {
|
|||
}
|
||||
|
||||
var jsonSecret, binarySecret, dotenvSecret, iniSecret secret
|
||||
root := "root"
|
||||
// should not create a symlink
|
||||
jsonSecret = yamlSecret
|
||||
jsonSecret.Name = "test2"
|
||||
jsonSecret.Owner = "root"
|
||||
jsonSecret.Owner = &root
|
||||
jsonSecret.Format = "json"
|
||||
jsonSecret.Group = "root"
|
||||
jsonSecret.Group = &root
|
||||
jsonSecret.SopsFile = path.Join(assets, "secrets.json")
|
||||
jsonSecret.Path = path.Join(testdir.secretsPath, "test2")
|
||||
jsonSecret.Mode = "0700"
|
||||
|
|
@ -130,16 +133,16 @@ func testGPG(t *testing.T) {
|
|||
|
||||
dotenvSecret = yamlSecret
|
||||
dotenvSecret.Name = "test4"
|
||||
dotenvSecret.Owner = "root"
|
||||
dotenvSecret.Group = "root"
|
||||
dotenvSecret.Owner = &root
|
||||
dotenvSecret.Group = &root
|
||||
dotenvSecret.Format = "dotenv"
|
||||
dotenvSecret.SopsFile = path.Join(assets, "secrets.env")
|
||||
dotenvSecret.Path = path.Join(testdir.secretsPath, "test4")
|
||||
|
||||
iniSecret = yamlSecret
|
||||
iniSecret.Name = "test5"
|
||||
iniSecret.Owner = "root"
|
||||
iniSecret.Group = "root"
|
||||
iniSecret.Owner = &root
|
||||
iniSecret.Group = &root
|
||||
iniSecret.Format = "ini"
|
||||
iniSecret.SopsFile = path.Join(assets, "secrets.ini")
|
||||
iniSecret.Path = path.Join(testdir.secretsPath, "test5")
|
||||
|
|
@ -214,11 +217,13 @@ func testSSHKey(t *testing.T) {
|
|||
ok(t, err)
|
||||
file.Close()
|
||||
|
||||
nobody := "nobody"
|
||||
nogroup := "nogroup"
|
||||
s := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
Owner: &nobody,
|
||||
Group: &nogroup,
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: target,
|
||||
Mode: "0400",
|
||||
|
|
@ -247,11 +252,13 @@ func TestAge(t *testing.T) {
|
|||
ok(t, err)
|
||||
file.Close()
|
||||
|
||||
nobody := "nobody"
|
||||
nogroup := "nogroup"
|
||||
s := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
Owner: &nobody,
|
||||
Group: &nogroup,
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: target,
|
||||
Mode: "0400",
|
||||
|
|
@ -280,11 +287,13 @@ func TestAgeWithSSH(t *testing.T) {
|
|||
ok(t, err)
|
||||
file.Close()
|
||||
|
||||
nobody := "nobody"
|
||||
nogroup := "nogroup"
|
||||
s := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
Owner: &nobody,
|
||||
Group: &nogroup,
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: target,
|
||||
Mode: "0400",
|
||||
|
|
@ -314,11 +323,13 @@ func TestValidateManifest(t *testing.T) {
|
|||
testdir := newTestDir(t)
|
||||
defer testdir.Remove()
|
||||
|
||||
nobody := "nobody"
|
||||
nogroup := "nogroup"
|
||||
s := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
Owner: &nobody,
|
||||
Group: &nogroup,
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: path.Join(testdir.path, "test-target"),
|
||||
Mode: "0400",
|
||||
|
|
|
|||
|
|
@ -1,424 +0,0 @@
|
|||
{ lib, testers }:
|
||||
let
|
||||
userPasswordTest = name: extraConfig: testers.runNixOSTest {
|
||||
inherit name;
|
||||
nodes.machine = { config, lib, ... }: {
|
||||
imports = [
|
||||
../../modules/sops
|
||||
extraConfig
|
||||
];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key.neededForUsers = true;
|
||||
secrets."nested/test/file".owner = "example-user";
|
||||
};
|
||||
|
||||
users.users.example-user = lib.mkMerge [
|
||||
(lib.mkIf (! config.systemd.sysusers.enable) {
|
||||
isNormalUser = true;
|
||||
hashedPasswordFile = config.sops.secrets.test_key.path;
|
||||
})
|
||||
(lib.mkIf config.systemd.sysusers.enable {
|
||||
isSystemUser = true;
|
||||
group = "users";
|
||||
hashedPasswordFile = config.sops.secrets.test_key.path;
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
|
||||
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
|
||||
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
|
||||
assert user == "example-user", f"Expected 'example-user', got '{user}'"
|
||||
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
|
||||
|
||||
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
|
||||
'' + lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
|
||||
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
|
||||
'';
|
||||
};
|
||||
in {
|
||||
ssh-keys = testers.runNixOSTest {
|
||||
name = "sops-ssh-keys";
|
||||
nodes.server = { ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
services.openssh.enable = true;
|
||||
services.openssh.hostKeys = [{
|
||||
type = "rsa";
|
||||
bits = 4096;
|
||||
path = ./test-assets/ssh-key;
|
||||
}];
|
||||
sops.defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
sops.secrets.test_key = { };
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
server.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
};
|
||||
|
||||
pruning = testers.runNixOSTest {
|
||||
name = "sops-pruning";
|
||||
nodes.machine = { lib, ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = { };
|
||||
keepGenerations = lib.mkDefault 0;
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
specialisation.pruning.configuration.sops.keepGenerations = 10;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
# Force us to generation 100
|
||||
machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric")
|
||||
machine.succeed("ln -fsn /run/secrets.d/99 /run/secrets")
|
||||
machine.succeed("/run/current-system/activate")
|
||||
machine.succeed("test -d /run/secrets.d/100")
|
||||
|
||||
# Ensure nothing is pruned, these are just random numbers
|
||||
machine.succeed("test -d /run/secrets.d/1")
|
||||
machine.succeed("test -d /run/secrets.d/90")
|
||||
machine.succeed("test -d /run/secrets.d/non-numeric")
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/pruning/bin/switch-to-configuration test")
|
||||
print(machine.succeed("ls -la /run/secrets.d/"))
|
||||
|
||||
# Ensure stuff was properly pruned.
|
||||
# We are now at generation 101 so 92 must exist when we keep 10 generations
|
||||
# and 91 must not.
|
||||
machine.fail("test -d /run/secrets.d/91")
|
||||
machine.succeed("test -d /run/secrets.d/92")
|
||||
machine.succeed("test -d /run/secrets.d/non-numeric")
|
||||
'';
|
||||
};
|
||||
|
||||
age-keys = testers.runNixOSTest {
|
||||
name = "sops-age-keys";
|
||||
nodes.machine = { lib, ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = { };
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
};
|
||||
|
||||
age-ssh-keys = testers.runNixOSTest {
|
||||
name = "sops-age-ssh-keys";
|
||||
nodes.machine = {
|
||||
imports = [ ../../modules/sops ];
|
||||
services.openssh.enable = true;
|
||||
services.openssh.hostKeys = [{
|
||||
type = "ed25519";
|
||||
path = ./test-assets/ssh-ed25519-key;
|
||||
}];
|
||||
sops = {
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = { };
|
||||
# Generate a key and append it to make sure it appending doesn't break anything
|
||||
age = {
|
||||
keyFile = "/tmp/testkey";
|
||||
generateKey = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
};
|
||||
|
||||
pgp-keys = testers.runNixOSTest {
|
||||
name = "sops-pgp-keys";
|
||||
nodes.server = { pkgs, lib, config, ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
|
||||
users.users.someuser = {
|
||||
isSystemUser = true;
|
||||
group = "nogroup";
|
||||
};
|
||||
|
||||
sops.gnupg.home = "/run/gpghome";
|
||||
sops.defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
sops.secrets.test_key.owner = config.users.users.someuser.name;
|
||||
sops.secrets."nested/test/file".owner = config.users.users.someuser.name;
|
||||
sops.secrets.existing-file = {
|
||||
key = "test_key";
|
||||
path = "/run/existing-file";
|
||||
};
|
||||
# must run before sops
|
||||
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
|
||||
cp -r ${./test-assets/gnupghome} /run/gpghome
|
||||
chmod -R 700 /run/gpghome
|
||||
|
||||
touch /run/existing-file
|
||||
'';
|
||||
# Useful for debugging
|
||||
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
|
||||
#environment.variables = {
|
||||
# GNUPGHOME = "/run/gpghome";
|
||||
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
|
||||
# SOPSFILE = "${./test-assets/secrets.yaml}";
|
||||
#};
|
||||
};
|
||||
testScript = ''
|
||||
def assertEqual(exp: str, act: str) -> None:
|
||||
if exp != act:
|
||||
raise Exception(f"'{exp}' != '{act}'")
|
||||
|
||||
|
||||
start_all()
|
||||
|
||||
value = server.succeed("cat /run/secrets/test_key")
|
||||
assertEqual("test_value", value)
|
||||
|
||||
server.succeed("runuser -u someuser -- cat /run/secrets/test_key >&2")
|
||||
value = server.succeed("cat /run/secrets/nested/test/file")
|
||||
assertEqual(value, "another value")
|
||||
|
||||
target = server.succeed("readlink -f /run/existing-file")
|
||||
assertEqual("/run/secrets.d/1/existing-file", target.strip())
|
||||
'';
|
||||
};
|
||||
|
||||
templates = testers.runNixOSTest {
|
||||
name = "sops-templates";
|
||||
nodes.machine = { config, lib, ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = { };
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
sops.templates.test_template = {
|
||||
content = ''
|
||||
This line is not modified.
|
||||
The next value will be replaced by ${config.sops.placeholder.test_key}
|
||||
This line is also not modified.
|
||||
'';
|
||||
mode = "0400";
|
||||
owner = "someuser";
|
||||
group = "somegroup";
|
||||
};
|
||||
sops.templates.test_default.content = ''
|
||||
Test value: ${config.sops.placeholder.test_key}
|
||||
'';
|
||||
|
||||
users.groups.somegroup = {};
|
||||
users.users.someuser = {
|
||||
isSystemUser = true;
|
||||
group = "somegroup";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("[ $(stat -c%U /run/secrets-rendered/test_template) = 'someuser' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets-rendered/test_template) = 'somegroup' ]")
|
||||
machine.succeed("[ $(stat -c%U /run/secrets-rendered/test_default) = 'root' ]")
|
||||
machine.succeed("[ $(stat -c%G /run/secrets-rendered/test_default) = 'root' ]")
|
||||
|
||||
expected = """
|
||||
This line is not modified.
|
||||
The next value will be replaced by test_value
|
||||
This line is also not modified.
|
||||
"""
|
||||
rendered = machine.succeed("cat /run/secrets-rendered/test_template")
|
||||
|
||||
expected_default = """
|
||||
Test value: test_value
|
||||
"""
|
||||
rendered_default = machine.succeed("cat /run/secrets-rendered/test_default")
|
||||
|
||||
if rendered.strip() != expected.strip() or rendered_default.strip() != expected_default.strip():
|
||||
raise Exception("Template is not rendered correctly")
|
||||
'';
|
||||
};
|
||||
|
||||
restart-and-reload = testers.runNixOSTest {
|
||||
name = "sops-restart-and-reload";
|
||||
nodes.machine = { pkgs, lib, config, ... }: {
|
||||
imports = [ ../../modules/sops ];
|
||||
|
||||
sops = {
|
||||
age.keyFile = "/run/age-keys.txt";
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = {
|
||||
restartUnits = [ "restart-unit.service" "reload-unit.service" ];
|
||||
reloadUnits = [ "reload-trigger.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
|
||||
systemd.services."restart-unit" = {
|
||||
description = "Restart unit";
|
||||
# not started on boot
|
||||
serviceConfig = { ExecStart = "/bin/sh -c 'echo ok > /restarted'"; };
|
||||
};
|
||||
systemd.services."reload-unit" = {
|
||||
description = "Reload unit";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
reloadIfChanged = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "/bin/sh -c true";
|
||||
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
|
||||
};
|
||||
};
|
||||
systemd.services."reload-trigger" = {
|
||||
description = "Reload trigger unit";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "/bin/sh -c true";
|
||||
ExecReload = "/bin/sh -c 'echo ok > /reloaded'";
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.fail("test -f /restarted")
|
||||
machine.fail("test -f /reloaded")
|
||||
|
||||
# Nothing is to be restarted after boot
|
||||
machine.fail("ls /run/nixos/*-list")
|
||||
|
||||
# Nothing happens when the secret is not changed
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.fail("test -f /restarted")
|
||||
machine.fail("test -f /reloaded")
|
||||
|
||||
# Ensure the secret is changed
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
|
||||
# The secret is changed, now something should happen
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
|
||||
# Ensure something happened
|
||||
machine.succeed("test -f /restarted")
|
||||
machine.succeed("test -f /reloaded")
|
||||
|
||||
with subtest("change detection"):
|
||||
machine.succeed("rm /run/secrets/test_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
if "adding secret" not in out:
|
||||
raise Exception("Addition detection does not work")
|
||||
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
if "modifying secret" not in out:
|
||||
raise Exception("Modification detection does not work")
|
||||
|
||||
machine.succeed(": > /run/secrets/another_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
if "removing secret" not in out:
|
||||
raise Exception("Removal detection does not work")
|
||||
|
||||
with subtest("dry activation"):
|
||||
machine.succeed("rm /run/secrets/test_key")
|
||||
machine.succeed(": > /run/secrets/another_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
|
||||
if "would add secret" not in out:
|
||||
raise Exception("Dry addition detection does not work")
|
||||
if "would remove secret" not in out:
|
||||
raise Exception("Dry removal detection does not work")
|
||||
|
||||
machine.fail("test -f /run/secrets/test_key")
|
||||
machine.succeed("test -f /run/secrets/another_key")
|
||||
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||
machine.succeed("test -f /run/secrets/test_key")
|
||||
machine.succeed("rm /restarted /reloaded")
|
||||
machine.fail("test -f /run/secrets/another_key")
|
||||
|
||||
machine.succeed(": > /run/secrets/test_key")
|
||||
out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
|
||||
if "would modify secret" not in out:
|
||||
raise Exception("Dry modification detection does not work")
|
||||
machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]")
|
||||
|
||||
machine.fail("test -f /restarted") # not done in dry mode
|
||||
machine.fail("test -f /reloaded") # not done in dry mode
|
||||
'';
|
||||
};
|
||||
|
||||
user-passwords = userPasswordTest "sops-user-passwords" {
|
||||
# must run before sops sets up keys
|
||||
boot.initrd.postDeviceCommands = ''
|
||||
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
};
|
||||
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") {
|
||||
user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" ({ pkgs, ... }: {
|
||||
systemd.sysusers.enable = true;
|
||||
users.mutableUsers = true;
|
||||
system.etc.overlay.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
# must run before sops sets up keys
|
||||
systemd.services."sops-install-secrets-for-users".preStart = ''
|
||||
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
});
|
||||
} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") {
|
||||
user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" ({ pkgs, ... }: {
|
||||
services.userborn.enable = true;
|
||||
users.mutableUsers = false;
|
||||
system.etc.overlay.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
# must run before sops sets up keys
|
||||
systemd.services."sops-install-secrets-for-users".preStart = ''
|
||||
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
|
||||
chmod -R 700 /run/age-keys.txt
|
||||
'';
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [ go delve util-linux gnupg ];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
go
|
||||
delve
|
||||
util-linux
|
||||
gnupg
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
{ makeSetupHook, gnupg, sops, lib }:
|
||||
{
|
||||
makeSetupHook,
|
||||
gnupg,
|
||||
sops,
|
||||
lib,
|
||||
}:
|
||||
|
||||
let
|
||||
# FIXME: drop after 23.05
|
||||
propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs";
|
||||
propagatedBuildInputs =
|
||||
if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then
|
||||
"deps"
|
||||
else
|
||||
"propagatedBuildInputs";
|
||||
in
|
||||
(makeSetupHook {
|
||||
name = "sops-pgp-hook";
|
||||
substitutions = {
|
||||
gpg = "${gnupg}/bin/gpg";
|
||||
};
|
||||
${propagatedBuildInputs} = [ sops gnupg ];
|
||||
${propagatedBuildInputs} = [
|
||||
sops
|
||||
gnupg
|
||||
];
|
||||
} ./sops-pgp-hook.bash)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# shell.nix
|
||||
with import <nixpkgs> {};
|
||||
with import <nixpkgs> { };
|
||||
mkShell {
|
||||
sopsPGPKeyDirs = [
|
||||
"./keys"
|
||||
|
|
@ -9,6 +9,6 @@ mkShell {
|
|||
"./non-existing-key.gpg"
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
(pkgs.callPackage ../../.. {}).sops-pgp-hook
|
||||
(pkgs.callPackage ../../.. { }).sops-pgp-hook
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
{ pkgs ? import <nixpkgs> {}
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
let
|
||||
sopsPkgs = import ../. { inherit pkgs; };
|
||||
in pkgs.stdenv.mkDerivation {
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "env";
|
||||
nativeBuildInputs = with pkgs; [
|
||||
bashInteractive
|
||||
gnupg
|
||||
util-linux
|
||||
nix
|
||||
sopsPkgs.sops-pgp-hook-test
|
||||
] ++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest;
|
||||
nativeBuildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
bashInteractive
|
||||
gnupg
|
||||
util-linux
|
||||
nix
|
||||
sopsPkgs.sops-pgp-hook-test
|
||||
]
|
||||
++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest;
|
||||
# allow to prefetch shell dependencies in build phase
|
||||
dontUnpack = true;
|
||||
installPhase = ''
|
||||
|
|
|
|||
15
shell.nix
15
shell.nix
|
|
@ -1,6 +1,15 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
{
|
||||
mkShell,
|
||||
bashInteractive,
|
||||
go,
|
||||
delve,
|
||||
gnupg,
|
||||
util-linux,
|
||||
nix,
|
||||
golangci-lint,
|
||||
}:
|
||||
mkShell {
|
||||
nativeBuildInputs = [
|
||||
bashInteractive
|
||||
go
|
||||
delve
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue