From 5d6bbabd2302be2754bbaf41aaf362f8eeb3ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:24:20 +0100 Subject: [PATCH 01/32] add treefmt --- flake.nix | 11 +++++++++++ formatter.nix | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 formatter.nix diff --git a/flake.nix b/flake.nix index b389c09f..57a5056d 100644 --- a/flake.nix +++ b/flake.nix @@ -95,6 +95,10 @@ in { home-manager = self.legacyPackages.${system}.homeConfigurations.sops.activation-script; + treefmt = + (pkgs.callPackage ./formatter.nix { + inputs = privateInputs; + }).config.build.check; } // (suffix-stable packages-stable) // nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux tests @@ -131,6 +135,13 @@ } ); + formatter = eachSystem ( + { pkgs, ... }: + (pkgs.callPackage ./formatter.nix { + inputs = privateInputs; + }).config.build.wrapper + ); + apps = eachSystem ( { pkgs, ... }: { diff --git a/formatter.nix b/formatter.nix new file mode 100644 index 00000000..ae1bd3a2 --- /dev/null +++ b/formatter.nix @@ -0,0 +1,27 @@ +{ pkgs, inputs, ... }: +inputs.treefmt-nix.lib.evalModule pkgs { + projectRootFile = ".git/config"; + + programs = { + nixfmt.enable = true; + + deadnix.enable = true; + deno.enable = true; + }; + + settings = { + global.excludes = [ + "./pkgs/sops-install-secrets/test-assets/*" + "*.narHash" + # unsupported extensions + "*.{asc,pub,gpg}" + "*/secrets.{bin,json,ini,yaml}" + ]; + + formatter = { + deadnix = { + priority = 1; + }; + }; + }; +} From 76aa7844271f2c2e87484aef83cad77334b31294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:32:35 +0100 Subject: [PATCH 02/32] delete duplicate shell.nix --- pkgs/sops-install-secrets/.envrc | 1 - pkgs/sops-install-secrets/shell.nix | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 pkgs/sops-install-secrets/.envrc delete mode 100644 pkgs/sops-install-secrets/shell.nix diff --git a/pkgs/sops-install-secrets/.envrc b/pkgs/sops-install-secrets/.envrc deleted file mode 100644 index 1d953f4b..00000000 --- a/pkgs/sops-install-secrets/.envrc +++ /dev/null @@ -1 +0,0 @@ -use nix diff --git a/pkgs/sops-install-secrets/shell.nix b/pkgs/sops-install-secrets/shell.nix deleted file mode 100644 index 10e4e813..00000000 --- a/pkgs/sops-install-secrets/shell.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ - pkgs ? import { }, -}: -pkgs.mkShell { - nativeBuildInputs = with pkgs; [ - go - delve - util-linux - gnupg - ]; -} From 7b60015dd53811e5bd936637614ee8dc0ad87ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:34:00 +0100 Subject: [PATCH 03/32] reformat with treefmt --- .github/workflows/test.yml | 14 +- .github/workflows/upgrade-flakes.yml | 12 +- README.md | 378 ++++++++++++++++----------- checks/darwin.nix | 1 - checks/home-manager.nix | 4 +- 5 files changed, 246 insertions(+), 163 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b3e026df..6db6026e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,14 +5,14 @@ on: branches: - master schedule: - - cron: '51 2 * * *' + - cron: "51 2 * * *" jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v30 - - name: Add keys group (needed for go tests) - run: sudo groupadd keys - - name: Run unit tests - run: nix develop .#unit-tests --command "true" + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v30 + - name: Add keys group (needed for go tests) + run: sudo groupadd keys + - name: Run unit tests + run: nix develop .#unit-tests --command "true" diff --git a/.github/workflows/upgrade-flakes.yml b/.github/workflows/upgrade-flakes.yml index ed3d8918..a49f48a3 100644 --- a/.github/workflows/upgrade-flakes.yml +++ b/.github/workflows/upgrade-flakes.yml @@ -3,7 +3,7 @@ on: repository_dispatch: workflow_dispatch: schedule: - - cron: '51 2 * * 0' + - cron: "51 2 * * 0" permissions: pull-requests: write @@ -20,10 +20,10 @@ jobs: access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - name: Update flakes run: | - nix flake update - pushd dev/private - nix flake update - popd - nix run .#update-dev-private-narHash + 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 diff --git a/README.md b/README.md index 5d5769f9..1ada466b 100644 --- a/README.md +++ b/README.md @@ -2,63 +2,87 @@ ![sops-nix logo](https://github.com/Mic92/sops-nix/releases/download/assets/logo.gif "Logo of sops-nix") -Atomic, declarative, and reproducible secret provisioning for NixOS based on [sops](https://github.com/mozilla/sops). +Atomic, declarative, and reproducible secret provisioning for NixOS based on +[sops](https://github.com/mozilla/sops). ## How it works -Secrets are decrypted from [`sops` files](https://github.com/mozilla/sops#2usage) during -activation time. The secrets are stored as one secret per file and access-controlled by full declarative configuration of their users, permissions, and groups. -GPG keys or `age` keys can be used for decryption, and compatibility shims are supported to enable the use of SSH RSA or SSH Ed25519 keys. -Sops also supports cloud key management APIs such as AWS -KMS, GCP KMS, Azure Key Vault and Hashicorp Vault. While not -officially supported by sops-nix yet, these can be controlled using +Secrets are decrypted from +[`sops` files](https://github.com/mozilla/sops#2usage) during activation time. +The secrets are stored as one secret per file and access-controlled by full +declarative configuration of their users, permissions, and groups. GPG keys or +`age` keys can be used for decryption, and compatibility shims are supported to +enable the use of SSH RSA or SSH Ed25519 keys. Sops also supports cloud key +management APIs such as AWS KMS, GCP KMS, Azure Key Vault and Hashicorp Vault. +While not officially supported by sops-nix yet, these can be controlled using environment variables that can be passed to sops. ## Features -- Compatible with all NixOS deployment frameworks: [NixOps](https://github.com/NixOS/nixops), nixos-rebuild, [krops](https://github.com/krebs/krops/), [morph](https://github.com/DBCDK/morph), [nixus](https://github.com/Infinisil/nixus), etc. -- Version-control friendly: Since all files are encrypted they can be directly committed to version control without worry. Diffs of the secrets are readable, and [can be shown in cleartext](https://github.com/mozilla/sops#showing-diffs-in-cleartext-in-git). -- CI friendly: Since sops files can be added to the Nix store without leaking secrets, a machine definition can be built as a whole from a repository, without needing to rely on external secrets or services. +- Compatible with all NixOS deployment frameworks: + [NixOps](https://github.com/NixOS/nixops), nixos-rebuild, + [krops](https://github.com/krebs/krops/), + [morph](https://github.com/DBCDK/morph), + [nixus](https://github.com/Infinisil/nixus), etc. +- Version-control friendly: Since all files are encrypted they can be directly + committed to version control without worry. Diffs of the secrets are readable, + and + [can be shown in cleartext](https://github.com/mozilla/sops#showing-diffs-in-cleartext-in-git). +- CI friendly: Since sops files can be added to the Nix store without leaking + secrets, a machine definition can be built as a whole from a repository, + without needing to rely on external secrets or services. - Home-manager friendly: Provides a home-manager module -- Works well in teams: sops-nix comes with `nix-shell` hooks that allows multiple people to quickly import all GPG keys. - The cryptography used in sops is designed to be scalable: Secrets are only encrypted once with a master key +- Works well in teams: sops-nix comes with `nix-shell` hooks that allows + multiple people to quickly import all GPG keys. The cryptography used in sops + is designed to be scalable: Secrets are only encrypted once with a master key instead of encrypted per machine/developer key. -- Atomic upgrades: New secrets are written to a new directory which replaces the old directory atomically. -- Rollback support: If sops files are added to the Nix store, old secrets can be rolled back. This is optional. -- Fast time-to-deploy: Unlike solutions implemented by NixOps, krops and morph, no extra steps are required to upload secrets. -- A variety of storage formats: Secrets can be stored in YAML, dotenv, INI, JSON or binary. -- Minimizes configuration errors: sops files are checked against the configuration at evaluation time. +- Atomic upgrades: New secrets are written to a new directory which replaces the + old directory atomically. +- Rollback support: If sops files are added to the Nix store, old secrets can be + rolled back. This is optional. +- Fast time-to-deploy: Unlike solutions implemented by NixOps, krops and morph, + no extra steps are required to upload secrets. +- A variety of storage formats: Secrets can be stored in YAML, dotenv, INI, JSON + or binary. +- Minimizes configuration errors: sops files are checked against the + configuration at evaluation time. ## Demo -There is a `configuration.nix` example in the [deployment step](#deploy-example) of our usage example. +There is a `configuration.nix` example in the [deployment step](#deploy-example) +of our usage example. ## Supported encryption methods sops-nix supports two basic ways of encryption, GPG and `age`. -GPG is based on [GnuPG](https://gnupg.org/) and encrypts against GPG public keys. Private GPG keys may -be used to decrypt the secrets on the target machine. The tool [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) can -be used to derive a GPG key from a SSH (host) key in RSA format. +GPG is based on [GnuPG](https://gnupg.org/) and encrypts against GPG public +keys. Private GPG keys may be used to decrypt the secrets on the target machine. +The tool [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) can be used to +derive a GPG key from a SSH (host) key in RSA format. -The other method is `age` which is based on [`age`](https://github.com/FiloSottile/age). -The tool ([`ssh-to-age`](https://github.com/Mic92/ssh-to-age)) can convert SSH host or user keys in Ed25519 -format to `age` keys. +The other method is `age` which is based on +[`age`](https://github.com/FiloSottile/age). The tool +([`ssh-to-age`](https://github.com/Mic92/ssh-to-age)) can convert SSH host or +user keys in Ed25519 format to `age` keys. ## Usage example -If you prefer video over the textual description below, you can also checkout this [6min tutorial](https://www.youtube.com/watch?v=G5f6GC7SnhU) by [@vimjoyer](https://github.com/vimjoyer). +If you prefer video over the textual description below, you can also checkout +this [6min tutorial](https://www.youtube.com/watch?v=G5f6GC7SnhU) by +[@vimjoyer](https://github.com/vimjoyer).
1. Install sops-nix -Choose one of the following methods. When using it non-globally with home-manager, refer to [Use with home-manager](#use-with-home-manager). +Choose one of the following methods. When using it non-globally with +home-manager, refer to [Use with home-manager](#use-with-home-manager). #### Flakes (current recommendation) If you use experimental nix flakes support: -``` nix +```nix { inputs.sops-nix.url = "github:Mic92/sops-nix"; # optional, not necessary for the module @@ -79,23 +103,24 @@ If you use experimental nix flakes support: ``` #### [`niv`](https://github.com/nmattia/niv) (recommended if not using flakes) - First add it to niv: - + +First add it to niv: + ```console $ niv add Mic92/sops-nix ``` - Then add the following to your `configuration.nix` in the `imports` list: - +Then add the following to your `configuration.nix` in the `imports` list: + ```nix { imports = [ "${(import ./nix/sources.nix).sops-nix}/modules/sops" ]; } ``` - + #### `fetchTarball` - Add the following to your `configuration.nix`: +Add the following to your `configuration.nix`: ```nix { @@ -111,7 +136,7 @@ $ niv add Mic92/sops-nix ]; } ``` - +
@@ -134,7 +159,8 @@ $ gpg --full-generate-key $ gpg --default-new-key-algo rsa4096 --gen-key ``` -Or you can use the `ssh-to-pgp` tool to get a GPG key from an SSH key: +Or you can use the `ssh-to-pgp` tool to get a GPG key from an SSH key: + ```console $ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i $HOME/.ssh/id_rsa | gpg --import --quiet" 2504791468b153b8a3963cc97ba53d1919c5dfd4 @@ -142,12 +168,18 @@ $ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i $HOME/.ssh/ $ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i $HOME/.ssh/id_rsa -o $USER.asc" 2504791468b153b8a3963cc97ba53d1919c5dfd4 ``` -(Note that `ssh-to-pgp` only supports RSA keys; to use Ed25519 keys, use `age`.) + +(Note that `ssh-to-pgp` only supports RSA keys; to use Ed25519 keys, use +`age`.)\ If you get the following, + ```console ssh-to-pgp: failed to parse private ssh key: ssh: this private key is passphrase protected ``` -then your SSH key is encrypted with your password and you will need to create an unencrypted copy temporarily. + +then your SSH key is encrypted with your password and you will need to create an +unencrypted copy temporarily. + ```console $ cp $HOME/.ssh/id_rsa /tmp/id_rsa $ ssh-keygen -p -N "" -f /tmp/id_rsa @@ -158,13 +190,16 @@ $ rm /tmp/id_rsa
How to find the public key of an `age` key -If you generated an `age` key, the `age` public key can be found via `age-keygen -y $PATH_TO_KEY`: +If you generated an `age` key, the `age` public key can be found via +`age-keygen -y $PATH_TO_KEY`: + ```console $ age-keygen -y ~/.config/sops/age/keys.txt age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl ``` Otherwise, you can convert an existing SSH key into an `age` public key: + ```console $ nix-shell -p ssh-to-age --run "ssh-to-age < ~/.ssh/id_ed25519.pub" # or @@ -177,6 +212,7 @@ $ nix-shell -p ssh-to-age --run "ssh-add -L | ssh-to-age" How to find the GPG fingerprint of a key Invoke this command and look for your key: + ```console $ gpg --list-secret-keys /tmp/tmp.JA07D1aVRD/pubring.kbx @@ -187,11 +223,15 @@ uid [ unknown] root ``` The fingerprint here is `9F89C5F69A10281A835014B09C3DC61F752087EF`. +
-Your `age` public key or GPG fingerprint can be written to your [`.sops.yaml`](https://github.com/getsops/sops#using-sops-yaml-conf-to-select-kms-pgp-and-age-for-new-files) in the root of your configuration directory or repository: +Your `age` public key or GPG fingerprint can be written to your +[`.sops.yaml`](https://github.com/getsops/sops#using-sops-yaml-conf-to-select-kms-pgp-and-age-for-new-files) +in the root of your configuration directory or repository: + ```yaml -# This example uses YAML anchors which allows reuse of multiple keys +# This example uses YAML anchors which allows reuse of multiple keys # without having to repeat yourself. # Also see https://github.com/Mic92/dotfiles/blob/master/nixos/.sops.yaml # for a more complex example. @@ -201,15 +241,14 @@ keys: creation_rules: - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ key_groups: - - pgp: - - *admin_alice - age: - - *admin_bob + - pgp: + - *admin_alice + age: + - *admin_bob ``` -**Note:** -Be sure to not include a `-` before subsequent key types under `key_groups` -(i.e. `age` in the above example should not have a `-` in front). +**Note:** Be sure to not include a `-` before subsequent key types under +`key_groups` (i.e. `age` in the above example should not have a `-` in front). This will otherwise cause sops to require multiple keys (shamir secret sharing) to decrypt a secret, which breaks normal sops-nix usage. @@ -218,9 +257,12 @@ to decrypt a secret, which breaks normal sops-nix usage.
3. Get a public key for your target machine -The easiest way to add new machines is by using SSH host keys (this requires OpenSSH to be enabled). +The easiest way to add new machines is by using SSH host keys (this requires +OpenSSH to be enabled). + +If you are using `age`, the `ssh-to-age` tool can be used to convert any SSH +Ed25519 public key to the `age` format: -If you are using `age`, the `ssh-to-age` tool can be used to convert any SSH Ed25519 public key to the `age` format: ```console $ nix-shell -p ssh-to-age --run 'ssh-keyscan example.com | ssh-to-age' age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 @@ -228,7 +270,8 @@ $ nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to- age1rgffpespcyjn0d8jglk7km9kfrfhdyev6camd3rck6pn8y47ze4sug23v3 ``` -For GPG, since sops does not natively support SSH keys yet, sops-nix supports a conversion tool (`ssh-to-pgp`) to store them as GPG keys: +For GPG, since sops does not natively support SSH keys yet, sops-nix supports a +conversion tool (`ssh-to-pgp`) to store them as GPG keys: ```console $ ssh root@server01 "cat /etc/ssh/ssh_host_rsa_key" | nix-shell -p ssh-to-pgp --run "ssh-to-pgp -o server01.asc" @@ -240,7 +283,8 @@ $ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i /etc/ssh/ssh_host_rsa_key -o serv 0fd60c8c3b664aceb1796ce02b318df330331003 ``` -The output of these commands is the identifier for the server's key, which can be added to your `.sops.yaml`: +The output of these commands is the identifier for the server's key, which can +be added to your `.sops.yaml`: ```yaml keys: @@ -251,22 +295,23 @@ keys: creation_rules: - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ key_groups: - - pgp: - - *admin_alice - - *server_azmidi - age: - - *admin_bob - - *server_nosaxa + - pgp: + - *admin_alice + - *server_azmidi + age: + - *admin_bob + - *server_nosaxa - path_regex: secrets/azmidi/[^/]+\.(yaml|json|env|ini)$ key_groups: - - pgp: - - *admin_alice - - *server_azmidi - age: - - *admin_bob + - pgp: + - *admin_alice + - *server_azmidi + age: + - *admin_bob ``` -If you prefer having a separate GPG key, see [Use with GPG instead of SSH keys](#use-with-GPG-instead-of-SSH-keys). +If you prefer having a separate GPG key, see +[Use with GPG instead of SSH keys](#use-with-GPG-instead-of-SSH-keys).
@@ -275,8 +320,8 @@ If you prefer having a separate GPG key, see [Use with GPG instead of SSH keys]( To create a sops file you need write a `.sops.yaml` as described above. -When using GnuPG you also need to import your personal GPG key -(and your colleagues) and your servers into your GPG key chain. +When using GnuPG you also need to import your personal GPG key (and your +colleagues) and your servers into your GPG key chain.
sops-nix can automate the import of GPG keys with a hook for nix-shell, allowing public @@ -342,8 +387,10 @@ After configuring `.sops.yaml`, you can open a new file with sops: $ nix-shell -p sops --run "sops secrets/example.yaml" ``` -This will start your configured editor located at the `$EDITOR` environment variable. +This will start your configured editor located at the `$EDITOR` environment +variable.\ An example secret file might be: + ```yaml # Files must always have a string value example-key: example-value @@ -394,7 +441,9 @@ sops: version: 3.7.1 ``` -If you add a new host to your `.sops.yaml` file, you will need to update the keys for all secrets that are used by the new host. This can be done like so: +If you add a new host to your `.sops.yaml` file, you will need to update the +keys for all secrets that are used by the new host. This can be done like so: + ``` $ nix-shell -p sops --run "sops updatekeys secrets/example.yaml" ``` @@ -404,7 +453,8 @@ $ nix-shell -p sops --run "sops updatekeys secrets/example.yaml"
5. Deploy -If you derived your server public key from SSH, all you need in your `configuration.nix` is: +If you derived your server public key from SSH, all you need in your +`configuration.nix` is: ```nix { @@ -425,8 +475,8 @@ If you derived your server public key from SSH, all you need in your `configurat } ``` -On `nixos-rebuild switch` this will make the keys accessible -via `/run/secrets/example-key` and `/run/secrets/myservice/my_subdir/my_secret`: +On `nixos-rebuild switch` this will make the keys accessible via +`/run/secrets/example-key` and `/run/secrets/myservice/my_subdir/my_secret`: ```console $ cat /run/secrets/example-key @@ -446,11 +496,11 @@ lrwxrwxrwx 16 root 12 Jul 6:23  /run/secrets -> /run/secrets.d/1 ## Set secret permission/owner and allow services to access it -By default secrets are owned by `root:root`. Furthermore -the parent directory `/run/secrets.d` is only owned by -`root` and the `keys` group has read access to it: +By default secrets are owned by `root:root`. Furthermore the parent directory +`/run/secrets.d` is only owned by `root` and the `keys` group has read access to +it: -``` console +```console $ ls -la /run/secrets.d/1 total 24 drwxr-x--- 2 root keys 0 Jul 12 6:23 . @@ -458,8 +508,8 @@ drwxr-x--- 3 root keys 0 Jul 12 6:23 .. -r-------- 1 root root 20 Jul 12 6:23 example-secret ``` -The secrets option has further parameter to change secret permission. -Consider the following nixos configuration example: +The secrets option has further parameter to change secret permission. Consider +the following nixos configuration example: ```nix { @@ -514,9 +564,11 @@ the service needs a token and a SSH private key to function.
## Restarting/reloading systemd units on secret change -It is possible to restart or reload units when a secret changes or is newly initialized. +It is possible to restart or reload units when a secret changes or is newly +initialized. This behavior can be configured per-secret: + ```nix { sops.secrets."home-assistant-secrets.yaml" = { @@ -528,9 +580,8 @@ This behavior can be configured per-secret: ## Symlinks to other directories -Some services might expect files in certain locations. -Using the `path` option a symlink to this directory can -be created: +Some services might expect files in certain locations. Using the `path` option a +symlink to this directory can be created: ```nix { @@ -548,13 +599,17 @@ lrwxrwxrwx 1 root root 40 Jul 19 22:36 /var/lib/hass/secrets.yaml -> /run/secret ## Setting a user's password -sops-nix has to run after NixOS creates users (in order to specify what users own a secret.) -This means that it's not possible to set `users.users..hashedPasswordFile` to any secrets managed by sops-nix. -To work around this issue, it's possible to set `neededForUsers = true` in a secret. -This will cause the secret to be decrypted to `/run/secrets-for-users` instead of `/run/secrets` before NixOS creates users. -As users are not created yet, it's not possible to set an owner for these secrets. +sops-nix has to run after NixOS creates users (in order to specify what users +own a secret.) This means that it's not possible to set +`users.users..hashedPasswordFile` to any secrets managed by sops-nix. To +work around this issue, it's possible to set `neededForUsers = true` in a +secret. This will cause the secret to be decrypted to `/run/secrets-for-users` +instead of `/run/secrets` before NixOS creates users. As users are not created +yet, it's not possible to set an owner for these secrets. + +The password must be stored as a hash for this to work, which can be created +with the command `mkpasswd` -The password must be stored as a hash for this to work, which can be created with the command `mkpasswd` ```console $ echo "password" | mkpasswd -s $y$j9T$WFoiErKnEnMcGq0ruQK4K.$4nJAY3LBeBsZBTYSkdTOejKU6KlDmhnfUV3Ll1K/1b. @@ -571,13 +626,15 @@ $y$j9T$WFoiErKnEnMcGq0ruQK4K.$4nJAY3LBeBsZBTYSkdTOejKU6KlDmhnfUV3Ll1K/1b. } ``` -**Note:** If you are using Impermanence, you must set `sops.age.keyFile` to a keyfile inside your persist directory or it will not exist at boot time. -For example: `/nix/persist/var/lib/sops-nix/key.txt` -Similarly if ssh host keys are used instead, they also need to be placed inside the persisted storage. +**Note:** If you are using Impermanence, you must set `sops.age.keyFile` to a +keyfile inside your persist directory or it will not exist at boot time. For +example: `/nix/persist/var/lib/sops-nix/key.txt` Similarly if ssh host keys are +used instead, they also need to be placed inside the persisted storage. ## Different file formats -At the moment we support the following file formats: YAML, JSON, INI, dotenv and binary. +At the moment we support the following file formats: YAML, JSON, INI, dotenv and +binary. sops-nix allows specifying multiple sops files in different file formats: @@ -644,7 +701,7 @@ $ sops secrets.json Then, put in the following content: -``` json +```json { "github_token": "4a6c73f74928a9c4c4bc47379256b72e598e2bd3", "ssh_key": "-----BEGIN OPENSSH PRIVATE KEY-----\\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\\nQyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQAAAJht4at6beGr\\negAAAAtzc2gtZWQyNTUxOQAAACDENhLwQI4v/Ecv65iCMZ7aZAL+Sdc0Cqyjkd012XwJzQ\\nAAAEBizgX7v+VMZeiCtWRjpl95dxqBWUkbrPsUSYF3DGV0rsQ2EvBAji/8Ry/rmIIxntpk\\nAv5J1zQKrKOR3TXZfAnNAAAAE2pvZXJnQHR1cmluZ21hY2hpbmUBAg==\\n-----END OPENSSH PRIVATE KEY-----\\n" @@ -669,11 +726,12 @@ You can include it like this in your `configuration.nix`: ### Binary This format allows to encrypt an arbitrary binary format that can't be put into -JSON/YAML files. Unlike the other two formats, for binary files, one file corresponds to one secret. +JSON/YAML files. Unlike the other two formats, for binary files, one file +corresponds to one secret. To encrypt an binary file use the following command: -``` console +```console $ sops -e /etc/krb5/krb5.keytab > krb5.keytab # an example of what this might result in: $ head krb5.keytab @@ -687,12 +745,11 @@ $ head krb5.keytab "mac": "ENC[AES256_GCM,data:ISjUzaw/5mNiwypmUrOk2DAZnlkbnhURHmTTYA3705NmRsSyUh1PyQvCuwglmaHscwl4GrsnIz4rglvwx1zYa+UUwanR0+VeBqntHwzSNiWhh7qMAQwdUXmdCNiOyeGy6jcSDsXUeQmyIWH6yibr7hhzoQFkZEB7Wbvcw6Sossk=,iv:UilxNvfHN6WkEvfY8ZIJCWijSSpLk7fqSCWh6n8+7lk=,tag:HUTgyL01qfVTCNWCTBfqXw==,type:str]", "pgp": [ { - ``` It can be decrypted again like this: -``` console +```console $ sops -d krb5.keytab > /tmp/krb5.keytab ``` @@ -709,9 +766,9 @@ 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. +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: @@ -719,10 +776,10 @@ For example, the input document `my-config.yaml` likes this: 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: [] + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] ... ``` @@ -747,16 +804,25 @@ my-secret2: hello ## Use with home manager -sops-nix also provides a home-manager module. -This module provides a subset of features provided by the system-wide sops-nix since features like the creation of the ramfs and changing the owner of the secrets are not available for non-root users. - -Instead of running as an activation script, sops-nix runs as a systemd user service called `sops-nix.service`. -While the sops-nix _system_ module decrypts secrets to the system non-persistent `/run/secrets`, the _home-manager_ module places them in the users non-persistent `$XDG_RUNTIME_DIR/secrets.d`. -Additionally secrets are symlinked to the users home at `$HOME/.config/sops-nix/secrets` which are referenced for the `.path` value in sops-nix. -This requires that the home-manager option `home.homeDirectory` is set to determine the home-directory on evaluation. It will have to be manually set if home-manager is configured as stand-alone or on non NixOS systems. - -Depending on whether you use home-manager system-wide or stand-alone using a home.nix, you have to import it in a different way. -This example shows the `flake` approach from the recommended example [Install: Flakes (current recommendation)](#Flakes (current recommendation)) +sops-nix also provides a home-manager module. This module provides a subset of +features provided by the system-wide sops-nix since features like the creation +of the ramfs and changing the owner of the secrets are not available for +non-root users. + +Instead of running as an activation script, sops-nix runs as a systemd user +service called `sops-nix.service`. While the sops-nix _system_ module decrypts +secrets to the system non-persistent `/run/secrets`, the _home-manager_ module +places them in the users non-persistent `$XDG_RUNTIME_DIR/secrets.d`. +Additionally secrets are symlinked to the users home at +`$HOME/.config/sops-nix/secrets` which are referenced for the `.path` value in +sops-nix. This requires that the home-manager option `home.homeDirectory` is set +to determine the home-directory on evaluation. It will have to be manually set +if home-manager is configured as stand-alone or on non NixOS systems. + +Depending on whether you use home-manager system-wide or stand-alone using a +home.nix, you have to import it in a different way. This example shows the +`flake` approach from the recommended example +[Install: Flakes (current recommendation)](#Flakes "current recommendation") ```nix { @@ -776,7 +842,8 @@ This example shows the `flake` approach from the recommended example [Install: F } ``` -This example show the `channel` approach from the example [Install: nix-channel](#nix-channel). All other methods work as well. +This example show the `channel` approach from the example +[Install: nix-channel](#nix-channel). All other methods work as well. ```nix { @@ -796,7 +863,9 @@ This example show the `channel` approach from the example [Install: nix-channel] } ``` -The actual sops configuration is in the `sops` namespace in your home.nix (or in the `home-manager.users.` namespace when using home-manager system-wide): +The actual sops configuration is in the `sops` namespace in your home.nix (or in +the `home-manager.users.` namespace when using home-manager system-wide): + ```nix { sops = { @@ -816,7 +885,9 @@ The actual sops configuration is in the `sops` namespace in your home.nix (or in } ``` -The secrets are decrypted in a systemd user service called `sops-nix`, so other services needing secrets must order after it: +The secrets are decrypted in a systemd user service called `sops-nix`, so other +services needing secrets must order after it: + ```nix { systemd.user.services.mbsync.Unit.After = [ "sops-nix.service" ]; @@ -825,9 +896,11 @@ 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: +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 = { @@ -850,7 +923,8 @@ The example above updated looks like this: ## 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`: +If you prefer having a separate GPG key, sops-nix also comes with a helper tool, +`sops-init-gpg-key`: ```console $ nix run github:Mic92/sops-nix#sops-init-gpg-key -- --hostname server01 --gpghome /tmp/newkey @@ -904,8 +978,9 @@ fingerprint: 4413684FC623628CEA3E0929AB2F16C6B5EF89EF F0477297E369CD1D189DD901278D1535AB473B9E ``` -In both cases, you must upload the GPG key directory `/tmp/newkey` onto the server. -If you uploaded it to `/var/lib/sops` than your sops configuration will look like this: +In both cases, you must upload the GPG key directory `/tmp/newkey` onto the +server. If you uploaded it to `/var/lib/sops` than your sops configuration will +look like this: ```nix { @@ -917,10 +992,11 @@ If you uploaded it to `/var/lib/sops` than your sops configuration will look lik ``` However be aware that this will also run GnuPG on your server including the -GnuPG daemon. [GnuPG is in general not great software](https://latacora.micro.blog/2019/07/16/the-pgp-problem.html) and might break in -hilarious ways. If you experience problems, you are on your own. If you want a -more stable and predictable solution go with SSH keys or one of the KMS services. - +GnuPG daemon. +[GnuPG is in general not great software](https://latacora.micro.blog/2019/07/16/the-pgp-problem.html) +and might break in hilarious ways. If you experience problems, you are on your +own. If you want a more stable and predictable solution go with SSH keys or one +of the KMS services. ## Share secrets between different users @@ -945,8 +1021,8 @@ example the `drone` secret is exposed as `/run/secrets/drone-server` for ## Migrate from pass/krops If you have used [pass](https://www.passwordstore.org) before (e.g. in -[krops](https://github.com/krebs/krops)) than you can use the following one-liner -to convert all your secrets to a YAML structure: +[krops](https://github.com/krebs/krops)) than you can use the following +one-liner to convert all your secrets to a YAML structure: ```console $ for i in *.gpg; do echo "$(basename $i .gpg): |\n$(pass $(dirname $i)/$(basename $i .gpg)| sed 's/^/ /')"; done @@ -956,21 +1032,23 @@ Copy the output to the editor you have opened with sops. ## Real-world examples -The [nix-community infra](https://github.com/nix-community/infra) makes extensive usage of sops-nix. -Each host has a [secrets.yaml](https://github.com/nix-community/infra/tree/master/hosts/build01) containing secrets for the host. -Also Samuel Leathers explains his personal setup in this [blog article](https://samleathers.com/posts/2022-02-11-my-new-network-and-sops.html). +The [nix-community infra](https://github.com/nix-community/infra) makes +extensive usage of sops-nix. Each host has a +[secrets.yaml](https://github.com/nix-community/infra/tree/master/hosts/build01) +containing secrets for the host. Also Samuel Leathers explains his personal +setup in this +[blog article](https://samleathers.com/posts/2022-02-11-my-new-network-and-sops.html). ## Known limitations ### Initrd secrets -sops-nix does not fully support initrd secrets. -This is because `nixos-rebuild switch` installs -the bootloader before running sops-nix's activation hook. -As a workaround, it is possible to run `nixos-rebuild test` -before `nixos-rebuild switch` to provision initrd secrets -before actually using them in the initrd. -In the future, we hope to extend NixOS to allow keys to be +sops-nix does not fully support initrd secrets. This is because +`nixos-rebuild switch` installs the bootloader before running sops-nix's +activation hook.\ +As a workaround, it is possible to run `nixos-rebuild test` before +`nixos-rebuild switch` to provision initrd secrets before actually using them in +the initrd. In the future, we hope to extend NixOS to allow keys to be provisioned in the bootloader install phase. ### Using secrets at evaluation time @@ -985,13 +1063,15 @@ can be used together with sops-nix. ## Templates -If your setup requires embedding secrets within a configuration file, the `template` feature of `sops-nix` provides a seamless way to do this. +If your setup requires embedding secrets within a configuration file, the +`template` feature of `sops-nix` provides a seamless way to do this. Here's how to use it: 1. **Define Your Secret** - Specify the secrets you intend to use. This will be encrypted and managed securely by `sops-nix`. + Specify the secrets you intend to use. This will be encrypted and managed + securely by `sops-nix`. ```nix { @@ -1001,8 +1081,9 @@ Here's how to use it: 2. **Use Templates for Configuration with Secrets** - Create a template for your configuration file and utilize the placeholder where you'd like the secret to be inserted. - During the activation phase, `sops-nix` will substitute the placeholder with the actual secret content. + Create a template for your configuration file and utilize the placeholder + where you'd like the secret to be inserted. During the activation phase, + `sops-nix` will substitute the placeholder with the actual secret content. ```nix { @@ -1022,7 +1103,8 @@ Here's how to use it: 3. **Reference the Rendered Configuration in Services** - When defining a service (e.g., using `systemd`), refer to the rendered configuration (with secrets in place) by leveraging the `.path` attribute. + When defining a service (e.g., using `systemd`), refer to the rendered + configuration (with secrets in place) by leveraging the `.path` attribute. ```nix { @@ -1045,12 +1127,14 @@ Here's how to use it: mechanism to inject secrets into configuration files in the nixos activation phase - # Need more commercial support? +We are building sops-nix very much as contributors to the community and are +committed to keeping it open source. -We are building sops-nix very much as contributors to the community and are committed to keeping it open source. +That said, many of us that are contributing to sops-nix also work for +consultancies. If you want to contact one of those for paid-for support setting +up sops-nix in your infrastructure you can do so here: -That said, many of us that are contributing to sops-nix also work for consultancies. If you want to contact one of those for paid-for support setting up sops-nix in your infrastructure you can do so here: -* [Numtide](https://numtide.com/contact) -* [Helsinki Systems](https://helsinki-systems.de/) +- [Numtide](https://numtide.com/contact) +- [Helsinki Systems](https://helsinki-systems.de/) diff --git a/checks/darwin.nix b/checks/darwin.nix index 8b74e334..8f7308bb 100644 --- a/checks/darwin.nix +++ b/checks/darwin.nix @@ -1,4 +1,3 @@ - { imports = [ ../modules/nix-darwin/default.nix diff --git a/checks/home-manager.nix b/checks/home-manager.nix index d1e63552..06280cda 100644 --- a/checks/home-manager.nix +++ b/checks/home-manager.nix @@ -1,5 +1,5 @@ - -{ config, ... }: { +{ config, ... }: +{ imports = [ ../modules/home-manager/sops.nix ]; From a33e8cc43f315c04f6a71f6413e8d3cbc7e7f617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:34:12 +0100 Subject: [PATCH 04/32] enable shellcheck --- .envrc | 1 + formatter.nix | 1 + pkgs/sops-import-keys-hook/sops-import-keys-hook.bash | 3 ++- scripts/update-vendor-hash.sh | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.envrc b/.envrc index 3550a30f..0f94eede 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +# shellcheck shell=bash use flake diff --git a/formatter.nix b/formatter.nix index ae1bd3a2..e67bc392 100644 --- a/formatter.nix +++ b/formatter.nix @@ -7,6 +7,7 @@ inputs.treefmt-nix.lib.evalModule pkgs { deadnix.enable = true; deno.enable = true; + shellcheck.enable = true; }; settings = { diff --git a/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash b/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash index 02246332..c05a96ea 100644 --- a/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash +++ b/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash @@ -2,7 +2,8 @@ sopsImportKeysHook() { local key dir if [ -n "${sopsCreateGPGHome}" ]; then export GNUPGHOME=${sopsGPGHome:-$(pwd)/.git/gnupg} - mkdir -m 700 -p $GNUPGHOME + # shellcheck disable=SC2174 + mkdir -m 700 -p "$GNUPGHOME" fi for key in ${sopsPGPKeys-}; do if [[ -f "$key" ]]; then diff --git a/scripts/update-vendor-hash.sh b/scripts/update-vendor-hash.sh index 4cd02a75..bda4a08c 100755 --- a/scripts/update-vendor-hash.sh +++ b/scripts/update-vendor-hash.sh @@ -1,5 +1,6 @@ #!/usr/bin/env nix-shell #!nix-shell -i bash -p nix -p coreutils -p gnused -p gawk +# shellcheck shell=bash set -exuo pipefail From 887d4b73228faa3354322fa374712ed1c48fd4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:36:35 +0100 Subject: [PATCH 05/32] enable gofumpt --- formatter.nix | 2 ++ pkgs/sops-install-secrets/linux.go | 4 ++-- pkgs/sops-install-secrets/main.go | 26 +++++++++++++------------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/formatter.nix b/formatter.nix index e67bc392..4e6f13a0 100644 --- a/formatter.nix +++ b/formatter.nix @@ -3,6 +3,8 @@ inputs.treefmt-nix.lib.evalModule pkgs { projectRootFile = ".git/config"; programs = { + gofumpt.enable = true; + nixfmt.enable = true; deadnix.enable = true; diff --git a/pkgs/sops-install-secrets/linux.go b/pkgs/sops-install-secrets/linux.go index b551e4b0..02412d1e 100644 --- a/pkgs/sops-install-secrets/linux.go +++ b/pkgs/sops-install-secrets/linux.go @@ -60,8 +60,8 @@ func MountSecretFs(mountpoint string, keysGID int, useTmpfs bool, userMode bool) return nil } - var fstype = "ramfs" - var fsmagic = RamfsMagic + fstype := "ramfs" + fsmagic := RamfsMagic if useTmpfs { fstype = "tmpfs" fsmagic = TmpfsMagic diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 280df4c9..01a7abc4 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -70,19 +70,19 @@ type template struct { } type manifest struct { - 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"` + 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 { From 15541d542b49b10fe86b592a7f3bd7a1518e2e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:39:54 +0100 Subject: [PATCH 06/32] bump go version to 1.22 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 060734f9..9861c426 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Mic92/sops-nix -go 1.18 +go 1.22 require ( github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 From 9190dee4086c41debe0e71e055f4115eb3bca2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:42:20 +0100 Subject: [PATCH 07/32] sops-pgp-hook: set parallel and helper --- pkgs/sops-pgp-hook/hook_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkgs/sops-pgp-hook/hook_test.go b/pkgs/sops-pgp-hook/hook_test.go index acf6b1d7..452611e3 100644 --- a/pkgs/sops-pgp-hook/hook_test.go +++ b/pkgs/sops-pgp-hook/hook_test.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path" - "path/filepath" "runtime" "strings" "testing" @@ -14,14 +13,17 @@ import ( // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { + tb.Helper() + if err != nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + fmt.Printf("\033[31munexpected error: %s\033[39m\n\n", err.Error()) tb.FailNow() } } func TestShellHook(t *testing.T) { + t.Parallel() + assets := os.Getenv("TEST_ASSETS") if assets == "" { _, filename, _, _ := runtime.Caller(0) From 3ba597a5e6236b0d729a7e6b22aa8a69680417a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:43:59 +0100 Subject: [PATCH 08/32] remove sops-pgp-hook --- default.nix | 4 - pkgs/sops-pgp-hook-test.nix | 11 --- pkgs/sops-pgp-hook/default.nix | 25 ------- pkgs/sops-pgp-hook/hook_test.go | 70 ------------------ pkgs/sops-pgp-hook/sops-pgp-hook.bash | 32 -------- .../test-assets/existing-key.gpg | Bin 1815 -> 0 bytes .../test-assets/keys/key-with-subkeys.asc | 61 --------------- pkgs/sops-pgp-hook/test-assets/keys/key.asc | 1 - pkgs/sops-pgp-hook/test-assets/keys/key.gpg | Bin 1815 -> 0 bytes pkgs/sops-pgp-hook/test-assets/shell.nix | 14 ---- pkgs/unit-tests.nix | 27 +++---- 11 files changed, 10 insertions(+), 235 deletions(-) delete mode 100644 pkgs/sops-pgp-hook-test.nix delete mode 100644 pkgs/sops-pgp-hook/default.nix delete mode 100644 pkgs/sops-pgp-hook/hook_test.go delete mode 100644 pkgs/sops-pgp-hook/sops-pgp-hook.bash delete mode 100644 pkgs/sops-pgp-hook/test-assets/existing-key.gpg delete mode 100644 pkgs/sops-pgp-hook/test-assets/keys/key-with-subkeys.asc delete mode 120000 pkgs/sops-pgp-hook/test-assets/keys/key.asc delete mode 100644 pkgs/sops-pgp-hook/test-assets/keys/key.gpg delete mode 100644 pkgs/sops-pgp-hook/test-assets/shell.nix diff --git a/default.nix b/default.nix index 63abb9b1..3546d4fc 100644 --- a/default.nix +++ b/default.nix @@ -17,10 +17,6 @@ rec { # backwards compatibility inherit (pkgs) ssh-to-pgp; - # used in the CI only - 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 { diff --git a/pkgs/sops-pgp-hook-test.nix b/pkgs/sops-pgp-hook-test.nix deleted file mode 100644 index 7f9f7df3..00000000 --- a/pkgs/sops-pgp-hook-test.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ buildGoModule, vendorHash }: - -buildGoModule { - name = "sops-pgp-hook-test"; - src = ../.; - inherit vendorHash; - buildPhase = '' - go test -c ./pkgs/sops-pgp-hook - install -D sops-pgp-hook.test $out/bin/sops-pgp-hook.test - ''; -} diff --git a/pkgs/sops-pgp-hook/default.nix b/pkgs/sops-pgp-hook/default.nix deleted file mode 100644 index 300b3c46..00000000 --- a/pkgs/sops-pgp-hook/default.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ - 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"; -in -(makeSetupHook { - name = "sops-pgp-hook"; - substitutions = { - gpg = "${gnupg}/bin/gpg"; - }; - ${propagatedBuildInputs} = [ - sops - gnupg - ]; -} ./sops-pgp-hook.bash) diff --git a/pkgs/sops-pgp-hook/hook_test.go b/pkgs/sops-pgp-hook/hook_test.go deleted file mode 100644 index 452611e3..00000000 --- a/pkgs/sops-pgp-hook/hook_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "path" - "runtime" - "strings" - "testing" -) - -// ok fails the test if an err is not nil. -func ok(tb testing.TB, err error) { - tb.Helper() - - if err != nil { - fmt.Printf("\033[31munexpected error: %s\033[39m\n\n", err.Error()) - tb.FailNow() - } -} - -func TestShellHook(t *testing.T) { - t.Parallel() - - assets := os.Getenv("TEST_ASSETS") - if assets == "" { - _, filename, _, _ := runtime.Caller(0) - assets = path.Join(path.Dir(filename), "test-assets") - } - tempdir, err := os.MkdirTemp("", "testdir") - ok(t, err) - defer os.RemoveAll(tempdir) - - cmd := exec.Command("nix-shell", "shell.nix", "--run", "echo SOPS_PGP_FP=$SOPS_PGP_FP") - cmd.Env = append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", tempdir)) - var stdoutBuf, stderrBuf bytes.Buffer - cmd.Stdout = &stdoutBuf - cmd.Stderr = &stderrBuf - cmd.Dir = assets - err = cmd.Run() - stdout := stdoutBuf.String() - stderr := stderrBuf.String() - fmt.Printf("$ %s\nstdout: \n%s\nstderr: \n%s\n", strings.Join(cmd.Args, " "), stdout, stderr) - ok(t, err) - - expectedKeys := []string{ - "C6DA56E69A7C756564A8AFEB4A6B05B714D13EFD", - "4EC40F8E04A945339F7F7C0032C5225271038E3F", - "7FB89715AADA920D65D25E63F9BA9DEBD03F57C0", - "E3B7464FBE89F5378ED4BC60FC925B42FC8B773D", - } - for _, key := range expectedKeys { - if !strings.Contains(stdout, key) { - t.Fatalf("'%v' not in '%v'", key, stdout) - } - } - - // it should ignore subkeys from ./keys/key-with-subkeys.asc - subkey := "94F174F588090494E73D0835A79B1680BC4D9A54" - if strings.Contains(stdout, subkey) { - t.Fatalf("subkey found in %s", stdout) - } - - expectedStderr := "./non-existing-key.gpg does not exists" - if !strings.Contains(stderr, expectedStderr) { - t.Fatalf("'%v' not in '%v'", expectedStderr, stdout) - } -} diff --git a/pkgs/sops-pgp-hook/sops-pgp-hook.bash b/pkgs/sops-pgp-hook/sops-pgp-hook.bash deleted file mode 100644 index e0ced80f..00000000 --- a/pkgs/sops-pgp-hook/sops-pgp-hook.bash +++ /dev/null @@ -1,32 +0,0 @@ -_sopsAddKey() { - @gpg@ --quiet --import "$key" - local fpr - # only add the first fingerprint, this way we ignore subkeys - fpr=$(@gpg@ --with-fingerprint --with-colons --show-key "$key" \ - | awk -F: '$1 == "fpr" { print $10; exit }') - if [[ $fpr != "" ]]; then - export SOPS_PGP_FP=''${SOPS_PGP_FP-}''${SOPS_PGP_FP:+','}$fpr - fi -} - -sopsPGPHook() { - local key dir - for key in ${sopsPGPKeys-}; do - if [[ -f "$key" ]]; then - _sopsAddKey "$key" - else - echo "$key does not exists" >&2 - fi - done - for dir in ${sopsPGPKeyDirs-}; do - while IFS= read -r -d '' key; do - _sopsAddKey "$key" - done < <(find -L "$dir" -type f \( -name '*.gpg' -o -name '*.asc' \) -print0) - done -} - -if [ -z "${shellHook-}" ]; then - shellHook=sopsPGPHook -else - shellHook="sopsPGPHook;${shellHook}" -fi diff --git a/pkgs/sops-pgp-hook/test-assets/existing-key.gpg b/pkgs/sops-pgp-hook/test-assets/existing-key.gpg deleted file mode 100644 index eba373876ddadb4d792d45c1b0e633a50bd7044d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1815 zcmV+y2k7|4#FzvC000013;@w8fCz8TD;LU?M3a#c7pd+QKD_o6DBO|Av^mcQ#md7i zzR*AAXboz#JKQ7V049*Zwv?ywsaugwly3-od&P}t!&jwRSxzn}rS~U>_=Z_UG{XL% z!06wju0^kKtJV7;myMZdoA7qdYSFwEj1}1_7o*yd)cA18pAZalQ~V58HPz^;L;D}8 zTZM_WVsLe$zz}E$L#WU2%{xNQH60I~gh1a0a;3+Iukv+*wv&ioCR^KN2*u;?Z`y8A9rw6GtfMHgH z2D+uP+Q*;5RAf4;z06(>fC+i^Ef^TliU$X?M(mB?P8}e2*jnlq2P{9p*WkfPNef+w z-_i>5qMMbsm-mRA@iAWgE{*Hr)!0RRC23;@COdIH$P zFF%2Q8B_@h(A=j)76=^Mw!}$(<@iwy@exA_kxm3+5~-F?h|X$^onR%6D`XFooT{fp zNr~t{u@(?HoFWp}gT`ht6)fogR7WUMZ%KXG%;;n=pmIm+-@C1WoM#qc6>!#yG%ZFB zX!iYw2tUnsyBMKmq|~|Q+j91Cp1eC@asyOpv(Hgc+x1d!*H52URT~|zIw&Srd-!)| zxWN$U1PP}PYv7XL(>PLai)=X_f96x8%}`5EaE2Y`WKs-9<#b;Y`LGOn6yK3x?M21b zFFwq1?q!M1B)6j9aK?QWF$E*myB|u&@5h`o6xO3{`#6`vD_0K*j&{TftN((SSn_`v z6X#_)P#!&FB$%9*_^$39_9Z6R#A4x@qk3d`-zzH}Z6VndpBJ=)%BR(Ic0XXQip;?Y zWrp|(>uOV}ZW0Nenq*-QNv1--fID{0nn@;n2Uv7Yb=3g6;x(B^J0sQsdo>Y$P&UFJ zEUTc9E<`Jd2t1HzJXL)7Qr2ib5)LTmuM{ z9uoV#tC&oQz!!e5&Ad5dfmP?jv<~uZ+16C}cLNW8A;y7yxe~I6%t-r&>S5-L5czdywKuDmFEvIUATk{ zCniW9mOUJK(Hy1}d;hNOT=*cR5cItJN0?uLKgRJnH%K}KO>8Ad#bzr`Saa~%h@$MX z1_1t8122=rN1-OjM6fD(Fph5gK4`^}GUkqsHv7;s81~vJaTb&a4d*| zS|&^1c5aWfCvmiits-3;wCJdUonx|8;_hKRwQ)xNHr_!h`yy{HJ6c{GwG?Ot;Ivvq z(eGtq_pJ69urefQKMg41Dx?7GtW-PsO!EyFe70Qun;nxG87$=t-G8FtiwHP+Rzp`N zx^0OYr@`BkF9tcd*Hzp-Tl{qgJuu2o)bq_1a&K>RAUtw!Z*)LxZ)0I>Xm4|LKElA_ z1QP)W02T!T000002@pza1-BH@KK%k44+0qh005i13;?~TP{p5iT4N#X-_8A@6D2*hZjy5->FBtgS_iP%7-@) z@(K&`BN*&KpJILob-)mCn_LJ}DKdB|{<=p8@^7j1n@-P8{vkLADvUCM6p<7AxwvtC z3Vu1=X+Lj6STahNY$6k-Xywrrt%fz@J$_wosw>D`3NO%2*y4H5Dr}=TFP95j2lrd7 ztnX38E|-a^f-BEX8Qa;iADotr{7`9bs^b@B0$&mJ zEFrn}N~g$SdnVDkS)qqG2u$CQ8&%`vhna|>_JFWDw26b+F>U)Ob$rPLvWr2}9($s% FfW51wXH5VA diff --git a/pkgs/sops-pgp-hook/test-assets/keys/key-with-subkeys.asc b/pkgs/sops-pgp-hook/test-assets/keys/key-with-subkeys.asc deleted file mode 100644 index 71f54050..00000000 --- a/pkgs/sops-pgp-hook/test-assets/keys/key-with-subkeys.asc +++ /dev/null @@ -1,61 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBF8YRjUBCACfdPLn/dUxr3SHZR2p6+aFgnu0jFA1KESBAgqA5TzDNIjaecff -MV2nP7Z+vmcyRq2oJb7zAd2UfavjH0jPzRJi+TP6NvJepfMj8SaflKEh8kZN6Gv0 -Zl0Fr6WtTPuenATuesAYvFDW+b2ZYRIs/XzEI+HP96XaW4MCWgTPwMPP8gMPZO3c -Cv+A5T9p1RHZjezfHktA0z+3F07IDquIT9K5d5Iapy0illnV7TziCdN6EbPUQZis -FqAP1kxgWUzJvYLswIncGb9WAw8T49GMVUtP8hoBiw3g0mNfnvzJUTBjYQr/e5X2 -+ZnGM4qqdrMTdTHFdQtzKHlsh3S1EI9Z5qB9ABEBAAG0H0pvaG4gRG9lIDxqb2hu -LmRvZUB0aGFsaGVpbS5pbz6JAU4EEwEIADgWIQTjt0ZPvon1N47UvGD8kltC/It3 -PQUCXxhGNQIbAQULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD8kltC/It3PTqF -B/9fbQmuDb0mg+rt8ALndJUXkiUK3osGTcmPhBXWPZpViCRsP4nOmBsM0yv5aA2y -Gsei+dHfLXK48UDkUFo/bt2ACEywCE+7QFBrhCnQFKS5sbPpE6EcqKF3eWzfR0I/ -PnzXQNA/igryuvaPxvQN9lIdY/Gzfi/erhv+f4/PgR53TzIhXYw2f2rwD4dCoiH3 -QkmKez3tasTc8zq7nwhlZ0d1pnbFn0qlCJCntrQT6caCkcWh9IiutrK0ozxfoa9H -Yqt/FdTWuRgEG1vj+/0RG2pggqE9D2LSkX6+gW0vai2OzTCn1a8VlrX2uYmDnXVF -b/bQBlAFW6wyGC6HhH+xckmHuQENBF8YRk0BCADCB2ov5gXA6X388bBeJ7YwWTMr -YuSAe2PZzZ3GipuQ4PRIpFvSLXHx4G4NT60J0G48cFL8M6dZCyJbCe+dZPyCEYLl -3V+5txpN0dYcbUTiG07uEAyDbuhkuda9goSJlfvJF8vUxGPNNHbYWPOO3hLsGQse -aQVGHSqu8WlRCWSDtNEyc11cOlty/zhEv3M5ZtBrJTahfy0u5RrCzk/x9SRea+MV -0xhYd1cKfi5ud/mNpQnnrbLuD+Gy9YgcqJUyxi6zvdfoCDYR4Sv7Rf0fxafxDkNZ -GQlqmPkaEuw21eedczmwUqMC57ZJz3avgDxKcLZG8uFC+6DY4thTSERPRb85ABEB -AAGJAmwEGAEIACAWIQTjt0ZPvon1N47UvGD8kltC/It3PQUCXxhGTQIbAgFACRD8 -kltC/It3PcB0IAQZAQgAHRYhBJTxdPWICQSU5z0INaebFoC8TZpUBQJfGEZNAAoJ -EKebFoC8TZpUWpQH/3de056tFqVIvsFjkYUW3oGylexVQEXeQljoqYx7NWsSxNX6 -NMEwYYJdNWgwXhL4CD8Tn0/3sVx/mMUDtbgQnQ8rKMB3lXZ3U6yzGghh5RdSmhAk -EQGhiYkZhIONce46i7rk+AE+hGi57p1IqsZ0UketOKoWN7rVYXbVLPf78cphD7G+ -Q7v7KWJYx8i3VkXDHJXP3wRlhbkbqVJAyUTmi63c7femOB+mDPJMBHBFmw6Opxt4 -AZR+qYczOLAyJCGA2MBx2U/26mVztkMYl5rJ80VKgUe/CEb8kD/uaOBYXeokGfqh -i6TV9fQxYokkmSU/4SIa+F+VcTu0xfRC46+EosL2Pwf+NpMRgpWihbF9EEh6RqX4 -NUxN4IVV/6frG19AJD8XNq0E8+bXvKVhHEy/Ea68ILKaJb/SIpcFY0aIJ3tHC0b2 -mh97nm5FdyRXRUNXoQ/u2wsOcD+HGK3P/jdrJDkNETuLTNr4Uff5Nn1Y6XydKviK -i7UwexDtX+wmyr1JxRdu7AJhdSi3rWY2lQxMMem7+9xyyqZ8uY2SixroMjcV/DL/ -7AjvfucWL6e/pESpvTp29sAKM5PUtMWqjm/vgapiFVLhXIEYWqe6OowXQ+smlkah -zQ00HJxLILNy3Mu2Vic543OVbLNRoWlJYQ1/zAqMxU5GLmdZA1hwncQT/3UCZ5zI -L7kBDQRfGEZvAQgAoPiXUlpQFLISXSHobzPtUwx1O3x+hN7XH57+VV0Hktz94+gb -NMj+3UBd67NZeseqUG6PMQ1ztEAuht7UX/LjLlmcBwmTD7iFeT8Y+hlo1+7AeKE6 -a3RGycTMOm5HFra1n3KcQqkmh6RMlTPxcpvb5wXHJXIiWvoW/k7C3nbFbJlzVZtK -dW2x4tcU/INsk2qgpir4Ou2nCwAXOOb91E/SDR+isPj4lYOp69AZa266YvShX1/X -UObG5UXSsPGs7CbZC9i+DcgJFhGjicrjgoEbAhPBmAdUwWaFiMls2WXmIkq9utv+ -uxQmQixEXL+/OQgXPJGzCmGaq4h/2JC9nCf5swARAQABiQE2BBgBCAAgFiEE47dG -T76J9TeO1Lxg/JJbQvyLdz0FAl8YRm8CGwwACgkQ/JJbQvyLdz01cAf9EsfZye6j -p7GuxInoZaJBblWW3tbJjOOH3GdeOhcY8ygImsRDcYFRIsp9QLp91eCRxGsT/EMz -q0vgQk4zsZOyTXMcK4TUMgUtsRY6zmiHSRez7sw0CA919KY/PAbMfB5F0qkuR5FL -auoAeYOUY1oYpiE7AG5rdtNNI1PC+EUeiivs+raczH3kLJr71fwjFD6Jnh9FDgPZ -QsYaWIe6t0quho6cNaL8DYfXtdJZh2vKgWX8h/qu5dUB/aHx18rWTvcQ7zmQ/ADn -oweTR94hbSL9O9mm3LoWogr/vtUGWvs8LlIYjFDUXj4TRx2svclcBdKI0qrjrCDx -Ed+ons5QiTE1LLkBDQRfGEaGAQgArDpYiwBV9Xml93knxoGVFi+rj0YL35gdVraT -ZqbeN+s0t9QPshzVpZz0jyqZSxFE/ojUmO7WMrH/Jb8nLVGvm/fq/jLEMfnbpJnb -Cu6ym7ed1QP7Y2JDMYJorlcS8BQCOSGSe2QRRD6h0nvgygrg70XKnkIhH6YfGCLt -pC96WWdbEr78d/dMloPRIW1Tsp58bXVkTfIseXpdCB5zVGj58GBtelWibvIms+/T -SRzw7QU9uiPjcrl5iZ8UMcRlE4mdMEBhlZ+eZaKgRdDNNDpcsd38xtktA52hs3uY -AgFKUGQ+PxY9cG9haVyCwwYwCVKo24/hTreTL1DydFLmAxaonQARAQABiQE2BBgB -CAAgFiEE47dGT76J9TeO1Lxg/JJbQvyLdz0FAl8YRoYCGyAACgkQ/JJbQvyLdz1d -gggAj+Gcxy6irGlkX9mxoq+sZv9WzRjXRT8xkB8H10tzqqOLQ0uzXeob07vDi3MC -6dBahE8sJq4ByOruy4hNhKUa/vtBm/G4ijTDNFzS/fmafDxZ+FObUDz6gLHGVbf0 -/NpwOmfcc/UeDCgI5t3TRcbQ9PugwCfw7A7eCYS34NspS549WJfzdNj8FcNBzsbi -yx1/wnXb7Eq5+kvZaPR1vodAW7YptYrUQCbCbioFGwq+zd1SHPXMS2h2D0ncMNbP -+C/y/AXliH+P08WRJ6kazSkSHv93UNM2nOt6x04vlk652WejLDc0t3wWNQEp0Q4U -W1YR5NNzw2GqjhH3nhj/SnUwXg== -=jshU ------END PGP PUBLIC KEY BLOCK----- diff --git a/pkgs/sops-pgp-hook/test-assets/keys/key.asc b/pkgs/sops-pgp-hook/test-assets/keys/key.asc deleted file mode 120000 index 34bc2403..00000000 --- a/pkgs/sops-pgp-hook/test-assets/keys/key.asc +++ /dev/null @@ -1 +0,0 @@ -../../../sops-install-secrets/test-assets/key.asc \ No newline at end of file diff --git a/pkgs/sops-pgp-hook/test-assets/keys/key.gpg b/pkgs/sops-pgp-hook/test-assets/keys/key.gpg deleted file mode 100644 index c168d7400078d06ffc04e81b851b888c91ee3ae3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1815 zcmV+y2k7|4#FzvC000013;?oe7g+@;Sot|5gSTwVwk}!&shU(88;2vCqX&Fv@Py>; z?3S<{X^fd0cdV>DW zA8qd7=SkF4vTJhB+1XNf)BNgVn0iSf-YElvmJVNP5E@Vi6LBk|urf)47E!Rk)GI%K zchd7$C%v>W05p|!!(A|7o8*d(5cl?W-R*(rh1MN9i}I!cQI-Y*fdP5cS8K9(H5l`h zxMyU{LrJn;QtYuA%yM3j?+G&OzZCnhNy$e6H6CZ+%Vwm~Y;S+TzB>aANxFnM#6>zG z=2$6`K^48y>^6=$3jECF^rOh(>y@Ro67}EzTzO7FnL4h`g{thxptLd{^+=GM$s-wu zV?x~B%DK`5G;=OLG1>lDZ$;!gw%W)SbM{ZnAh2P~HC#(KwCZVTqDc=)ur2CuQKyLX zQ2ipL4CkO%-;OJi`Rl zk@dXCFA1+=-595Q^pfxgJ$*-#Boak?HIl&5eGh#@-jia&u2!%{GEY0->p}WT z6a)7y5-@bRn<*5<8F1AZXG?|t!1cs0O^QVXXGH((LffpxS|jV+rmRK0w0%xME#Xb=&unW177;O)?<-44e|pCj!AL(HEETnBk3OaGj>62-=+>{WCZ$ zu9fJ+GISr;a6(pK_!p-aur!6=Ab&Iny2%mAEs~ONf$TvlqfHX14cm#&=+)LRj)P1a z?$nYlJo~C%h*T8Y(#eP#Gy-mf{fAFtYYa2btE)%<)S5Icr_zORgq~8Bc|%o((5NCF zioX}n7}3G5_(((=r_lxg(R(A2VEHDloq%f#Djl+NtF6YxucmwONkAHsw zX`=G^okEpvOC9Hw=Mr7SQ`_@X{`WMfY{FUng7%ETf)@D9rPzs*AsBUwbn3-X#hV90 z$Ks=1B8bijEzB7sAa@N-=%fF#T!vOjQ&uw=jnJe2hLN`1q<;TDG>!VJ;S9&i)(K! z1_0UzokdgCu&(b-^fOT*GO=OE&8J^lmD!H{4C!Jfmp5W`5KW5GmDAi{bV-njLGf9VGZ`nsC}YhJc59&I#^D>_Jqt>F?uX{;h$5Jp zd#;Sh)aCts(y=ZVpWox2VJ`SQ(rdv?w8d#Am`TIo!vv5dKK@KKVdUqwrJpKCTdFbj z(6^=+Nq^e7%(AkYKMi4K_6a(57hB=YHhIq1{j)6;+SMVOIBf<1h8tJ7vfywsFl$~K zT5yD5+xx{5h_q`8s%mZDSfc--il%iu)MrOQfAP8Y?-mkZF-L6Rn~yr!`_rLm?y%@# z*LTQWa`U{P0W*`j3fi@llf?AuRJ5C41+SkEcS<=y6WJ50963y6f9c?hKaHg~pokGT zTSWzKZ|T)_l8I0vF(lkgf0pEiVp!x<6YN+jfb&~;-BO4E>ff|uRNd4$Lu2hGe})Jp zV$%p5+fj$vLS*=9@d12A*VqiIhmcg<=Df`na&K>RAUtw!Z*)LxZ)0I>Xm4|LKElA_ z1QP)W02T!T000002@o>HB2sY!jz0n$4+0qh002S>3;;|@#n@du4K_vk${hM>*RY8G zcCMlfYxW5`UN9QeJ?qqY=ID@aGkuOU3BAxlNP@0U(dF}n1`1fEHcwEBoV_88#+3uPSgxN=)xo=t<_Sk|mGC5q}d5{DJLuh}125$w%vnZb) zB^fIJ*t`gjk`Xj z5#?8r;3fn+Dpj}-|1#HX3wNxg4GzA%!^l=5Q=2E_o$f$;R@DWv)FW;`3p{}BnUpJM z1m!V!NSuK)9ufG#GNiuh!dW3FT+JL@F*+MwCg3nNg(>K*SUDFGYQY z!U93?tL { }; -mkShell { - sopsPGPKeyDirs = [ - "./keys" - ]; - sopsPGPKeys = [ - "./existing-key.gpg" - "./non-existing-key.gpg" - ]; - nativeBuildInputs = [ - (pkgs.callPackage ../../.. { }).sops-pgp-hook - ]; -} diff --git a/pkgs/unit-tests.nix b/pkgs/unit-tests.nix index f3d3678b..9fc14fcd 100644 --- a/pkgs/unit-tests.nix +++ b/pkgs/unit-tests.nix @@ -5,17 +5,14 @@ let sopsPkgs = import ../. { inherit pkgs; }; 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; + name = "unit-tests"; + nativeBuildInputs = with pkgs; [ + bashInteractive + gnupg + util-linux + nix + sopsPkgs.sops-install-secrets.unittest + ]; # allow to prefetch shell dependencies in build phase dontUnpack = true; installPhase = '' @@ -23,11 +20,7 @@ pkgs.stdenv.mkDerivation { ''; shellHook = '' set -x - NIX_PATH=nixpkgs=${toString pkgs.path} TEST_ASSETS=$(realpath ./pkgs/sops-pgp-hook/test-assets) \ - sops-pgp-hook.test - ${pkgs.lib.optionalString (pkgs.stdenv.isLinux) '' - sudo TEST_ASSETS=$(realpath ./pkgs/sops-install-secrets/test-assets) \ - unshare --mount --fork sops-install-secrets.test - ''} + sudo TEST_ASSETS=$(realpath ./pkgs/sops-install-secrets/test-assets) \ + unshare --mount --fork sops-install-secrets.test ''; } From ae893d14fbe91e2e900c855a1ef2fbfce47c8c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 18:48:42 +0100 Subject: [PATCH 09/32] hook_test: fix linter errors --- pkgs/sops-import-keys-hook/hook_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkgs/sops-import-keys-hook/hook_test.go b/pkgs/sops-import-keys-hook/hook_test.go index 31e2fae8..35b0bff3 100644 --- a/pkgs/sops-import-keys-hook/hook_test.go +++ b/pkgs/sops-import-keys-hook/hook_test.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path" - "path/filepath" "runtime" "strings" "testing" @@ -14,22 +13,27 @@ import ( // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { + tb.Helper() + if err != nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + fmt.Printf("\033[31m unexpected error: %s\033[39m\n\n", err.Error()) tb.FailNow() } } func TestShellHook(t *testing.T) { + t.Parallel() + assets := os.Getenv("TEST_ASSETS") if assets == "" { _, filename, _, _ := runtime.Caller(0) assets = path.Join(path.Dir(filename), "test-assets") } + tempdir, err := os.MkdirTemp("", "testdir") ok(t, err) - cmd := exec.Command("cp", "-vra", assets+"/.", tempdir) // nolint:gosec + + cmd := exec.Command("cp", "-vra", assets+"/.", tempdir) //nolint:gosec fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -37,12 +41,14 @@ func TestShellHook(t *testing.T) { defer os.RemoveAll(tempdir) - cmd = exec.Command("nix-shell", path.Join(assets, "shell.nix"), "--run", "gpg --list-keys") // nolint:gosec + cmd = exec.Command("nix-shell", path.Join(assets, "shell.nix"), "--run", "gpg --list-keys") //nolint:gosec + var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = &stdoutBuf cmd.Stderr = &stderrBuf cmd.Dir = tempdir fmt.Println(tempdir) + err = cmd.Run() stdout := stdoutBuf.String() stderr := stderrBuf.String() From 975c6853085ece542edf0942215cabaa93e03822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 19:21:59 +0100 Subject: [PATCH 10/32] unittest: set t.Helper() and t.Parallel() --- pkgs/sops-install-secrets/main_test.go | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 8c09e68c..99a7abad 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -10,7 +10,6 @@ import ( "os/exec" "os/user" "path" - "path/filepath" "reflect" "runtime" "strconv" @@ -21,22 +20,24 @@ import ( // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { + tb.Helper() if err != nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + fmt.Printf("\033[31munexpected error: %s\033[39m\n\n", err.Error()) tb.FailNow() } } func equals(tb testing.TB, exp, act interface{}) { + tb.Helper() if !reflect.DeepEqual(exp, act) { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + fmt.Printf("\033[31m\texp: %#v\n\n\tgot: %#v\033[39m\n\n", exp, act) tb.FailNow() } } func writeManifest(t *testing.T, dir string, m *manifest) string { + t.Helper() + filename := path.Join(dir, "manifest.json") f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o755) ok(t, err) @@ -64,17 +65,21 @@ func (dir testDir) Remove() { } func newTestDir(t *testing.T) testDir { + t.Helper() tempdir, err := os.MkdirTemp("", "symlinkDir") ok(t, err) return testDir{tempdir, path.Join(tempdir, "secrets.d"), path.Join(tempdir, "secrets")} } func testInstallSecret(t *testing.T, testdir testDir, m *manifest) { + t.Helper() + path := writeManifest(t, testdir.path, m) ok(t, installSecrets([]string{"sops-install-secrets", path})) } -func testGPG(t *testing.T) { +// cannot run in parellel with TestSSHKey because we rely on GNUPGHOME environment variable +func TestGPG(t *testing.T) { //nolint:paralleltest assets := testAssetPath() testdir := newTestDir(t) @@ -206,7 +211,9 @@ func testGPG(t *testing.T) { equals(t, path.Join(testdir.secretsPath, "2"), target) } -func testSSHKey(t *testing.T) { +func TestSSHKey(t *testing.T) { + t.Parallel() + assets := testAssetPath() testdir := newTestDir(t) @@ -242,6 +249,8 @@ func testSSHKey(t *testing.T) { } func TestAge(t *testing.T) { + t.Parallel() + assets := testAssetPath() testdir := newTestDir(t) @@ -277,6 +286,8 @@ func TestAge(t *testing.T) { } func TestAgeWithSSH(t *testing.T) { + t.Parallel() + assets := testAssetPath() testdir := newTestDir(t) @@ -311,13 +322,9 @@ func TestAgeWithSSH(t *testing.T) { testInstallSecret(t, testdir, &m) } -func TestAll(t *testing.T) { - // we can't test in parallel because we rely on GNUPGHOME environment variable - testGPG(t) - testSSHKey(t) -} - func TestValidateManifest(t *testing.T) { + t.Parallel() + assets := testAssetPath() testdir := newTestDir(t) @@ -351,6 +358,8 @@ func TestValidateManifest(t *testing.T) { } func TestIsValidFormat(t *testing.T) { + t.Parallel() + generateCase := func(input string, mustBe bool) { result := IsValidFormat(input) if result != mustBe { From 4d5d1b75591fa8b05040610982f7ac1881593849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 19:44:37 +0100 Subject: [PATCH 11/32] fix wsl lints --- pkgs/sops-install-secrets/linux.go | 6 ++ pkgs/sops-install-secrets/main.go | 99 +++++++++++++++++++- pkgs/sops-install-secrets/main_test.go | 10 ++ pkgs/sops-install-secrets/sshkeys/convert.go | 1 + 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pkgs/sops-install-secrets/linux.go b/pkgs/sops-install-secrets/linux.go index 02412d1e..9e1e7cf6 100644 --- a/pkgs/sops-install-secrets/linux.go +++ b/pkgs/sops-install-secrets/linux.go @@ -27,18 +27,23 @@ func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) defer unix.Close(fd) buf := make([]byte, len(expectedTarget)+1) // oversize by one to detect trunc + n, err := unix.Readlinkat(fd, "", buf) if err != nil { return fmt.Errorf("couldn't readlinkat %s", symlinkToCheck) } + if n > len(expectedTarget) || string(buf[:n]) != expectedTarget { return fmt.Errorf("symlink %s does not point to %s", symlinkToCheck, expectedTarget) } + stat := unix.Stat_t{} + err = unix.Fstat(fd, &stat) if err != nil { return fmt.Errorf("cannot stat '%s': %w", symlinkToCheck, err) } + if stat.Uid == uint32(owner) && stat.Gid == uint32(group) { return nil // already correct } @@ -62,6 +67,7 @@ func MountSecretFs(mountpoint string, keysGID int, useTmpfs bool, userMode bool) fstype := "ramfs" fsmagic := RamfsMagic + if useTmpfs { fstype = "tmpfs" fsmagic = TmpfsMagic diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 01a7abc4..7166006f 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -120,7 +120,9 @@ func (f *FormatType) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &s); err != nil { return err } + t := FormatType(s) + switch t { case "": *f = Yaml @@ -165,12 +167,15 @@ func readManifest(path string) (*manifest, error) { if err != nil { return nil, fmt.Errorf("failed to open manifest: %w", err) } + defer file.Close() dec := json.NewDecoder(file) + var m manifest if err := dec.Decode(&m); err != nil { return nil, fmt.Errorf("failed to parse manifest: %w", err) } + return &m, nil } @@ -192,6 +197,7 @@ func createSymlink(targetFile string, path string, owner int, group int, userMod if err = os.Symlink(targetFile, path); err != nil { return fmt.Errorf("cannot create symlink '%s': %w", path, err) } + if !userMode { if err = SecureSymlinkChown(path, targetFile, owner, group); err != nil { return fmt.Errorf("cannot chown symlink '%s': %w", path, err) @@ -201,6 +207,7 @@ func createSymlink(targetFile string, path string, owner int, group int, userMod } else if err != nil { return fmt.Errorf("cannot stat '%s': %w", path, err) } + if stat.Mode()&os.ModeSymlink == os.ModeSymlink { linkTarget, err := os.Readlink(path) if os.IsNotExist(err) { @@ -211,6 +218,7 @@ func createSymlink(targetFile string, path string, owner int, group int, userMod return nil } } + if err := os.Remove(path); err != nil { return fmt.Errorf("cannot override %s: %w", path, err) } @@ -223,10 +231,12 @@ func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates [] if targetFile == secret.Path { continue } + parent := filepath.Dir(secret.Path) if err := os.MkdirAll(parent, os.ModePerm); err != nil { return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err) } + 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) } @@ -237,10 +247,12 @@ func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates [] 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) } @@ -256,7 +268,9 @@ type plainData struct { func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, error) { var val interface{} + var ok bool + currentKey := wantedKey currentData := keys keyUntilNow := "" @@ -274,23 +288,31 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er } break } + thisKey := currentKey[:slashIndex] + if keyUntilNow == "" { keyUntilNow = thisKey } else { keyUntilNow += "/" + thisKey } + currentKey = currentKey[(slashIndex + 1):] val, ok = currentData[thisKey] + if !ok { return "", fmt.Errorf("the key '%s' cannot be found", keyUntilNow) } + var valWithWrongType map[interface{}]interface{} valWithWrongType, ok = val.(map[interface{}]interface{}) + if !ok { return "", fmt.Errorf("key '%s' does not refer to a dictionary", keyUntilNow) } + currentData = make(map[string]interface{}) + for key, value := range valWithWrongType { currentData[key.(string)] = value } @@ -334,6 +356,7 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { return fmt.Errorf("secret of type %s in %s is not supported", s.Format, s.SopsFile) } } + switch s.Format { case Binary, Dotenv, Ini: s.value = sourceFile.binary @@ -345,9 +368,11 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { if err != nil { return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) } + s.value = []byte(strVal) } } + sourceFiles[s.SopsFile] = sourceFile return nil } @@ -369,10 +394,12 @@ const ( func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, userMode bool) (*string, error) { var generation uint64 + linkTarget, err := os.Readlink(linkName) if err == nil { if strings.HasPrefix(linkTarget, secretMountpoint) { targetBasename := filepath.Base(linkTarget) + generation, err = strconv.ParseUint(targetBasename, 10, 64) if err != nil { return nil, fmt.Errorf("cannot parse %s of %s as a number: %w", targetBasename, linkTarget, err) @@ -381,16 +408,20 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us } else if !os.IsNotExist(err) { return nil, fmt.Errorf("cannot access %s: %w", linkName, err) } + generation++ dir := filepath.Join(secretMountpoint, strconv.Itoa(int(generation))) + if _, err := os.Stat(dir); !os.IsNotExist(err) { if err := os.RemoveAll(dir); err != nil { return nil, fmt.Errorf("cannot remove existing %s: %w", dir, err) } } + if err := os.Mkdir(dir, os.FileMode(0o751)); err != nil { return nil, fmt.Errorf("mkdir(): %w", err) } + if !userMode { if err := os.Chown(dir, 0, int(keysGID)); err != nil { return nil, fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", dir, keysGID, err) @@ -402,11 +433,13 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us 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) @@ -423,9 +456,11 @@ func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool 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) } + if !userMode { if err := os.Chown(fp, secret.owner, secret.group); err != nil { return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err) @@ -440,6 +475,7 @@ func lookupGroup(groupname string) (int, error) { if err != nil { return 0, fmt.Errorf("failed to lookup 'keys' group: %w", err) } + gid, err := strconv.ParseInt(group.Gid, 10, 64) if err != nil { return 0, fmt.Errorf("cannot parse keys gid %s: %w", group.Gid, err) @@ -452,6 +488,7 @@ func lookupKeysGroup() (int, error) { if err1 == nil { return gid, nil } + gid, err2 := lookupGroup("nogroup") if err2 == nil { return gid, nil @@ -486,7 +523,9 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { if err != nil { return nil, fmt.Errorf("cannot parse dotenv of '%s': %w", s.SopsFile, err) } + keys = map[string]interface{}{} + for k, v := range env { keys[k] = v } @@ -495,11 +534,11 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { return nil, fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err) } case Ini: + // TODO: we do not actually check the contents of the ini here... _, err := ini.Load(bytes.NewReader(cipherText)) if err != nil { return nil, fmt.Errorf("cannot parse ini of '%s': %w", s.SopsFile, err) } - // TODO: we do not actually check the contents of the ini here... } return &secretFile{ @@ -515,6 +554,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) && s.Key != "" { _, err := recurseSecretKey(file.keys, s.Key) if err != nil { @@ -537,6 +577,7 @@ func validateOwner(owner string) (int, error) { 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) @@ -549,6 +590,7 @@ func validateGroup(group string) (int, error) { 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) @@ -561,6 +603,7 @@ func (app *appContext) validateSecret(secret *secret) error { if err != nil { return err } + secret.mode = mode if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { @@ -574,6 +617,7 @@ func (app *appContext) validateSecret(secret *secret) error { if err != nil { return err } + secret.owner = owner } @@ -584,6 +628,7 @@ func (app *appContext) validateSecret(secret *secret) error { if err != nil { return err } + secret.group = group } } @@ -602,6 +647,7 @@ func (app *appContext) validateSecret(secret *secret) error { if err != nil { return err } + app.secretFiles[secret.SopsFile] = *maybeFile file = *maybeFile @@ -631,6 +677,7 @@ func (app *appContext) validateTemplate(template *template) error { if err != nil { return err } + template.mode = mode if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { @@ -644,6 +691,7 @@ func (app *appContext) validateTemplate(template *template) error { if err != nil { return err } + template.owner = owner } @@ -654,11 +702,13 @@ func (app *appContext) validateTemplate(template *template) error { if err != nil { return err } + template.group = group } } var templateText string + if template.Content != "" { templateText = template.Content } else if template.File != "" { @@ -666,6 +716,7 @@ func (app *appContext) validateTemplate(template *template) error { 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) @@ -684,6 +735,7 @@ func (app *appContext) validateManifest() error { if len(m.SSHKeyPaths) > 0 { return fmt.Errorf(errorFmt, "sshKeyPaths") } + if m.AgeKeyFile != "" { return fmt.Errorf(errorFmt, "ageKeyFile") } @@ -729,7 +781,9 @@ func atomicSymlink(oldname, newname string) error { if err != nil { return err } + cleanup := true + defer func() { if cleanup { os.RemoveAll(d) @@ -771,6 +825,7 @@ func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int) if err != nil { return fmt.Errorf("cannot read %s: %w", secretsMountPoint, err) } + for _, generationName := range generations { generationNum, err := strconv.Atoi(generationName) // Not a number? Not relevant @@ -782,6 +837,7 @@ func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int) if generationNum == currentGeneration { continue } + if currentGeneration-keepGenerations >= generationNum { os.RemoveAll(path.Join(secretsMountPoint, generationName)) } @@ -812,6 +868,7 @@ func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) erro fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err) continue } + gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) @@ -881,10 +938,12 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) if err != nil { return err } + info, err := os.Lstat(finalPath) if err != nil { return walkFn(path, info, err) } + if info.IsDir() { return symlinkWalk(finalPath, path, walkFn) } @@ -897,6 +956,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error { var restart []string + var reload []string newSecrets := make(map[string]bool) @@ -989,7 +1049,9 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if err != nil { return err } + defer f.Close() + for _, unit := range list { if _, err = f.WriteString(unit + "\n"); err != nil { return err @@ -998,15 +1060,18 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } return nil } + var dryPrefix string if isDry { dryPrefix = "/run/nixos/dry-activation" } else { dryPrefix = "/run/nixos/activation" } + if err := writeLines(restart, dryPrefix+"-restart-list"); err != nil { return err } + if err := writeLines(reload, dryPrefix+"-reload-list"); err != nil { return err } @@ -1018,10 +1083,12 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s // 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 } + if info.IsDir() { return nil } @@ -1032,6 +1099,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if err != nil { return err } + isSecret := strings.HasPrefix(rel, "..") if isSecret { @@ -1041,6 +1109,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s return nil } } + removedSecrets[path] = true } else { path = strings.TrimPrefix(path, symlinkRenderedPath+string(os.PathSeparator)) @@ -1049,6 +1118,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s return nil } } + removedTemplates[path] = true } return nil @@ -1064,6 +1134,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if len(changed) != 1 { s = "s" } + if isDry { fmt.Printf("%s %s%s: ", dryPrefix, noun, s) } else { @@ -1075,6 +1146,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s for key := range changed { keys = append(keys, key) } + sort.Strings(keys) fmt.Println(strings.Join(keys, ", ")) @@ -1104,12 +1176,14 @@ func setupGPGKeyring(logcfg loggingConfig, sshKeys []string, parentDir string) ( if err != nil { return nil, fmt.Errorf("cannot create gpg home in '%s': %w", parentDir, err) } + k := keyring{dir} if err := importSSHKeys(logcfg, sshKeys, dir); err != nil { os.RemoveAll(dir) return nil, err } + os.Setenv("GNUPGHOME", dir) return &k, nil @@ -1117,14 +1191,17 @@ func setupGPGKeyring(logcfg loggingConfig, sshKeys []string, parentDir string) ( func parseFlags(args []string) (*options, error) { var opts options + fs := flag.NewFlagSet(args[0], flag.ContinueOnError) fs.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [OPTION] manifest.json\n", args[0]) fs.PrintDefaults() } + var checkMode string fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`) fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root or the user executing the tool in user mode`) + if err := fs.Parse(args[1:]); err != nil { return nil, err } @@ -1140,6 +1217,7 @@ func parseFlags(args []string) (*options, error) { flag.Usage() return nil, flag.ErrHelp } + opts.manifest = fs.Arg(0) return &opts, nil } @@ -1147,10 +1225,12 @@ func parseFlags(args []string) (*options, error) { func replaceRuntimeDir(path, rundir string) (ret string) { parts := strings.Split(path, "%%") first := true + for _, part := range parts { if !first { ret += "%" } + first = false ret += strings.ReplaceAll(part, "%r", rundir) } @@ -1168,6 +1248,7 @@ func writeTemplates(targetDir string, templates []template, keysGID int, userMod 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) @@ -1191,16 +1272,21 @@ func installSecrets(args []string) error { if manifest.UserMode { var rundir string rundir, err = RuntimeDir() + if opts.checkMode == Off && err != nil { return fmt.Errorf("cannot figure out runtime directory: %w", err) } + manifest.SecretsMountPoint = replaceRuntimeDir(manifest.SecretsMountPoint, rundir) manifest.SymlinkPath = replaceRuntimeDir(manifest.SymlinkPath, rundir) + var newSecrets []secret + for _, secret := range manifest.Secrets { secret.Path = replaceRuntimeDir(secret.Path, rundir) newSecrets = append(newSecrets, secret) } + manifest.Secrets = newSecrets } @@ -1238,10 +1324,12 @@ func installSecrets(args []string) error { if len(manifest.SSHKeyPaths) != 0 { var keyring *keyring + keyring, err = setupGPGKeyring(manifest.Logging, manifest.SSHKeyPaths, manifest.SecretsMountPoint) if err != nil { return fmt.Errorf("error setting up gpg keyring: %w", err) } + defer keyring.Remove() } else if manifest.GnupgHome != "" { os.Setenv("GNUPGHOME", manifest.GnupgHome) @@ -1253,11 +1341,14 @@ func installSecrets(args []string) error { os.Setenv("SOPS_AGE_KEY_FILE", keyfile) // Create the keyfile var ageFile *os.File + ageFile, err = os.OpenFile(keyfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { return fmt.Errorf("cannot create '%s': %w", keyfile, err) } + defer ageFile.Close() + fmt.Fprintf(ageFile, "# generated by sops-nix at %s\n", time.Now().Format(time.RFC3339)) // Import SSH keys @@ -1271,10 +1362,12 @@ func installSecrets(args []string) error { if manifest.AgeKeyFile != "" { // Read the keyfile var contents []byte + contents, err = os.ReadFile(manifest.AgeKeyFile) if err != nil { return fmt.Errorf("cannot read keyfile '%s': %w", manifest.AgeKeyFile, err) } + // Append it to the file _, err = ageFile.WriteString(string(contents) + "\n") if err != nil { @@ -1294,6 +1387,7 @@ func installSecrets(args []string) error { if err != nil { return fmt.Errorf("failed to prepare new secrets directory: %w", err) } + if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil { return fmt.Errorf("cannot write secrets: %w", err) } @@ -1314,9 +1408,11 @@ func installSecrets(args []string) error { 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 { return fmt.Errorf("cannot update secrets symlink: %w", err) } + if err := pruneGenerations(manifest.SecretsMountPoint, *secretDir, manifest.KeepGenerations); err != nil { return fmt.Errorf("cannot prune old secrets generations: %w", err) } @@ -1329,6 +1425,7 @@ func main() { if errors.Is(err, flag.ErrHelp) { return } + fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) os.Exit(1) } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 99a7abad..cece77dc 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -21,6 +21,7 @@ import ( // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { tb.Helper() + if err != nil { fmt.Printf("\033[31munexpected error: %s\033[39m\n\n", err.Error()) tb.FailNow() @@ -29,6 +30,7 @@ func ok(tb testing.TB, err error) { func equals(tb testing.TB, exp, act interface{}) { tb.Helper() + if !reflect.DeepEqual(exp, act) { fmt.Printf("\033[31m\texp: %#v\n\n\tgot: %#v\033[39m\n\n", exp, act) tb.FailNow() @@ -41,6 +43,7 @@ func writeManifest(t *testing.T, dir string, m *manifest) string { filename := path.Join(dir, "manifest.json") f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o755) ok(t, err) + encoder := json.NewEncoder(f) ok(t, encoder.Encode(m)) f.Close() @@ -66,6 +69,7 @@ func (dir testDir) Remove() { func newTestDir(t *testing.T) testDir { t.Helper() + tempdir, err := os.MkdirTemp("", "symlinkDir") ok(t, err) return testDir{tempdir, path.Join(tempdir, "secrets.d"), path.Join(tempdir, "secrets")} @@ -88,15 +92,18 @@ func TestGPG(t *testing.T) { //nolint:paralleltest gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome)) ok(t, os.Mkdir(gpgHome, os.FileMode(0o700))) + cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc")) // nolint:gosec cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = gpgEnv ok(t, cmd.Run()) + stopGpgCmd := exec.Command("gpgconf", "--kill", "gpg-agent") stopGpgCmd.Stdout = os.Stdout stopGpgCmd.Stderr = os.Stderr stopGpgCmd.Env = gpgEnv + defer func() { if err := stopGpgCmd.Run(); err != nil { fmt.Printf("failed to stop gpg-agent: %s\n", err) @@ -119,6 +126,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest } var jsonSecret, binarySecret, dotenvSecret, iniSecret secret + root := "root" // should not create a symlink jsonSecret = yamlSecret @@ -179,6 +187,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest equals(t, 0o400, int(yamlStat.Mode().Perm())) stat, success := yamlStat.Sys().(*syscall.Stat_t) equals(t, true, success) + content, err := os.ReadFile(yamlSecret.Path) ok(t, err) equals(t, "test_value", string(content)) @@ -195,6 +204,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest ok(t, err) equals(t, true, jsonStat.Mode().IsRegular()) equals(t, 0o700, int(jsonStat.Mode().Perm())) + if stat, ok := jsonStat.Sys().(*syscall.Stat_t); ok { equals(t, 0, int(stat.Uid)) equals(t, 0, int(stat.Gid)) diff --git a/pkgs/sops-install-secrets/sshkeys/convert.go b/pkgs/sops-install-secrets/sshkeys/convert.go index 070cf1e4..495124a1 100644 --- a/pkgs/sops-install-secrets/sshkeys/convert.go +++ b/pkgs/sops-install-secrets/sshkeys/convert.go @@ -60,6 +60,7 @@ func SSHPrivateKeyToPGP(sshPrivateKey []byte) (*openpgp.Entity, error) { IssuerKeyId: &gpgKey.PrimaryKey.KeyId, }, } + err = gpgKey.Identities[uid.Id].SelfSignature.SignUserId(uid.Id, gpgKey.PrimaryKey, gpgKey.PrivateKey, nil) if err != nil { return nil, err From 582b2a83009c15bd702de4ded99e42a4ad07b290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 19:48:07 +0100 Subject: [PATCH 12/32] remove space before nolint --- pkgs/sops-install-secrets/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index cece77dc..e5043e2c 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -93,7 +93,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest ok(t, os.Mkdir(gpgHome, os.FileMode(0o700))) - cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc")) // nolint:gosec + cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc")) //nolint:gosec cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = gpgEnv From d5e0983eb9cc972ac7ecd80ffa6ee5d4a13222db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:07:46 +0100 Subject: [PATCH 13/32] tests: move NOBODY/NOGROUP into a constant --- pkgs/sops-install-secrets/main_test.go | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index e5043e2c..872ad2fb 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -18,6 +18,11 @@ import ( "testing" ) +const ( + NOBODY = "nobody" + NOGROUP = "nogroup" +) + // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { tb.Helper() @@ -110,9 +115,9 @@ func TestGPG(t *testing.T) { //nolint:paralleltest } }() - nobody := "nobody" - nogroup := "nogroup" // should create a symlink + nobody := NOBODY + nogroup := NOGROUP yamlSecret := secret{ Name: "test", Key: "test_key", @@ -194,11 +199,11 @@ func TestGPG(t *testing.T) { //nolint:paralleltest u, err := user.LookupId(strconv.Itoa(int(stat.Uid))) ok(t, err) - equals(t, "nobody", u.Username) + equals(t, NOBODY, u.Username) g, err := user.LookupGroupId(strconv.Itoa(int(stat.Gid))) ok(t, err) - equals(t, "nogroup", g.Name) + equals(t, NOGROUP, g.Name) jsonStat, err := os.Stat(jsonSecret.Path) ok(t, err) @@ -234,8 +239,8 @@ func TestSSHKey(t *testing.T) { ok(t, err) file.Close() - nobody := "nobody" - nogroup := "nogroup" + nobody := NOBODY + nogroup := NOGROUP s := secret{ Name: "test", Key: "test_key", @@ -271,8 +276,8 @@ func TestAge(t *testing.T) { ok(t, err) file.Close() - nobody := "nobody" - nogroup := "nogroup" + nobody := NOBODY + nogroup := NOGROUP s := secret{ Name: "test", Key: "test_key", @@ -308,8 +313,8 @@ func TestAgeWithSSH(t *testing.T) { ok(t, err) file.Close() - nobody := "nobody" - nogroup := "nogroup" + nobody := NOBODY + nogroup := NOGROUP s := secret{ Name: "test", Key: "test_key", @@ -340,8 +345,8 @@ func TestValidateManifest(t *testing.T) { testdir := newTestDir(t) defer testdir.Remove() - nobody := "nobody" - nogroup := "nogroup" + nobody := NOBODY + nogroup := NOGROUP s := secret{ Name: "test", Key: "test_key", From f57a556af4a653e25a04b50b99b34085b703645b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:08:17 +0100 Subject: [PATCH 14/32] apply golangci-lints --- pkgs/sops-install-secrets/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 7166006f..f771df4f 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -19,7 +19,6 @@ import ( "github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys" agessh "github.com/Mic92/ssh-to-age" - "github.com/getsops/sops/v3/decrypt" "github.com/joho/godotenv" "github.com/mozilla-services/yaml" @@ -886,7 +885,7 @@ func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) erro } if logcfg.KeyImport { - fmt.Printf("%s: Imported %s as GPG key with fingerprint %s\n", path.Base(os.Args[0]), p, hex.EncodeToString(gpgKey.PrimaryKey.Fingerprint[:])) + fmt.Printf("%s: Imported %s as GPG key with fingerprint %s\n", path.Base(os.Args[0]), p, hex.EncodeToString(gpgKey.PrimaryKey.Fingerprint)) } } From 14753257fb937f3301420e4784062a3342c8236c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:20:00 +0100 Subject: [PATCH 15/32] tests: avoid sprint for simple string concatination --- pkgs/sops-install-secrets/linux.go | 3 ++- pkgs/sops-install-secrets/main_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/sops-install-secrets/linux.go b/pkgs/sops-install-secrets/linux.go index 9e1e7cf6..a92e1a36 100644 --- a/pkgs/sops-install-secrets/linux.go +++ b/pkgs/sops-install-secrets/linux.go @@ -4,6 +4,7 @@ package main import ( + "errors" "fmt" "os" @@ -13,7 +14,7 @@ import ( func RuntimeDir() (string, error) { rundir, ok := os.LookupEnv("XDG_RUNTIME_DIR") if !ok { - return "", fmt.Errorf("$XDG_RUNTIME_DIR is not set") + return "", errors.New("$XDG_RUNTIME_DIR is not set") } return rundir, nil } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 872ad2fb..1cc94d0f 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -94,7 +94,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest testdir := newTestDir(t) defer testdir.Remove() gpgHome := path.Join(testdir.path, "gpg-home") - gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome)) + gpgEnv := append(os.Environ(), "GNUPGHOME="+gpgHome) ok(t, os.Mkdir(gpgHome, os.FileMode(0o700))) From 4bc1bfdec2bdd4a8b9ee4c374e6492b83b697567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:20:14 +0100 Subject: [PATCH 16/32] tests: avoid type shadowing --- pkgs/sops-install-secrets/main_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 1cc94d0f..e017350c 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -165,19 +165,19 @@ func TestGPG(t *testing.T) { //nolint:paralleltest iniSecret.SopsFile = path.Join(assets, "secrets.ini") iniSecret.Path = path.Join(testdir.secretsPath, "test5") - manifest := manifest{ + m := manifest{ Secrets: []secret{yamlSecret, jsonSecret, binarySecret, dotenvSecret, iniSecret}, SecretsMountPoint: testdir.secretsPath, SymlinkPath: testdir.symlinkPath, GnupgHome: gpgHome, } - testInstallSecret(t, testdir, &manifest) + testInstallSecret(t, testdir, &m) - _, err := os.Stat(manifest.SecretsMountPoint) + _, err := os.Stat(m.SecretsMountPoint) ok(t, err) - _, err = os.Stat(manifest.SymlinkPath) + _, err = os.Stat(m.SymlinkPath) ok(t, err) yamlLinkStat, err := os.Lstat(yamlSecret.Path) @@ -219,7 +219,7 @@ func TestGPG(t *testing.T) { //nolint:paralleltest ok(t, err) equals(t, 13, len(content)) - testInstallSecret(t, testdir, &manifest) + testInstallSecret(t, testdir, &m) target, err := os.Readlink(testdir.symlinkPath) ok(t, err) From d1b8b2a00a2d0a620141fd0dff08f8f0d72929af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:20:59 +0100 Subject: [PATCH 17/32] fix wsl lints --- pkgs/sops-install-secrets/main.go | 2 ++ pkgs/sops-install-secrets/main_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index f771df4f..4710716d 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -1198,6 +1198,7 @@ func parseFlags(args []string) (*options, error) { } var checkMode string + fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`) fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root or the user executing the tool in user mode`) @@ -1404,6 +1405,7 @@ func installSecrets(args []string) error { if isDry { return 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) } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index e017350c..cd0b2f38 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -60,6 +60,7 @@ func testAssetPath() string { if assets != "" { return assets } + _, filename, _, _ := runtime.Caller(0) return path.Join(path.Dir(filename), "test-assets") } From 3e7cba9a3890d818f214d4d580ffb8536283e7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:30:12 +0100 Subject: [PATCH 18/32] wrap all external errors --- pkgs/sops-install-secrets/main.go | 78 ++++++++++++++------ pkgs/sops-install-secrets/sshkeys/convert.go | 4 +- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 4710716d..53ca6cb2 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -117,7 +117,7 @@ func IsValidFormat(format string) bool { func (f *FormatType) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { - return err + return fmt.Errorf("failed to unmarshal format: %w", err) } t := FormatType(s) @@ -133,7 +133,12 @@ func (f *FormatType) UnmarshalJSON(b []byte) error { } func (f FormatType) MarshalJSON() ([]byte, error) { - return json.Marshal(string(f)) + data, err := json.Marshal(string(f)) + if err != nil { + return nil, fmt.Errorf("failed to marshal format: %w", err) + } + + return data, nil } type CheckMode string @@ -765,20 +770,20 @@ func (app *appContext) validateManifest() error { func atomicSymlink(oldname, newname string) error { if err := os.MkdirAll(filepath.Dir(newname), 0o755); err != nil { - return err + return fmt.Errorf("cannot create directory for %s: %w", newname, err) } // Fast path: if newname does not exist yet, we can skip the whole dance // below. if err := os.Symlink(oldname, newname); err == nil || !os.IsExist(err) { - return err + return fmt.Errorf("cannot create symlink %s -> %s: %w", newname, oldname, err) } // We need to use ioutil.TempDir, as we cannot overwrite a ioutil.TempFile, // and removing+symlinking creates a TOCTOU race. d, err := os.MkdirTemp(filepath.Dir(newname), "."+filepath.Base(newname)) if err != nil { - return err + return fmt.Errorf("cannot create temporary directory: %w", err) } cleanup := true @@ -791,15 +796,21 @@ func atomicSymlink(oldname, newname string) error { symlink := filepath.Join(d, "tmp.symlink") if err := os.Symlink(oldname, symlink); err != nil { - return err + return fmt.Errorf("cannot create temporary symlink %s -> %s: %w", symlink, oldname, err) } if err := os.Rename(symlink, newname); err != nil { - return err + return fmt.Errorf("cannot rename %s to %s: %w", symlink, newname, err) } cleanup = false - return os.RemoveAll(d) + + err = os.RemoveAll(d) + if err != nil { + return fmt.Errorf("cannot remove temporary directory: %w", err) + } + + return nil } func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int) error { @@ -865,22 +876,26 @@ func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) erro sshKey, err := os.ReadFile(p) if err != nil { fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err) + continue } gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) + continue } if err := gpgKey.SerializePrivate(secring, nil); err != nil { fmt.Fprintf(os.Stderr, "Cannot write secring: %s\n", err) + continue } if err := gpgKey.Serialize(pubring); err != nil { fmt.Fprintf(os.Stderr, "Cannot write pubring: %s\n", err) + continue } @@ -898,23 +913,27 @@ func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File) sshKey, err := os.ReadFile(p) if err != nil { fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err) + continue } // Convert the key to age privKey, pubKey, err := agessh.SSHPrivateKeyToAge(sshKey, []byte{}) if err != nil { fmt.Fprintf(os.Stderr, "Cannot convert ssh key '%s': %s\n", p, err) + continue } // Append it to the file _, err = ageFile.WriteString(*privKey + "\n") if err != nil { fmt.Fprintf(os.Stderr, "Cannot write key to age file: %s\n", err) + continue } if logcfg.KeyImport { fmt.Fprintf(os.Stderr, "%s: Imported %s as age key with fingerprint %s\n", path.Base(os.Args[0]), p, *pubKey) + continue } } @@ -929,28 +948,34 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) if fname, err := filepath.Rel(filename, path); err == nil { path = filepath.Join(linkDirname, fname) } else { - return err + return fmt.Errorf("cannot get relative path: %w", err) } if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { finalPath, err := filepath.EvalSymlinks(path) if err != nil { - return err + return fmt.Errorf("cannot evaluate symlink %s: %w", path, err) } - info, err := os.Lstat(finalPath) + linkInfo, err := os.Lstat(finalPath) if err != nil { - return walkFn(path, info, err) + return walkFn(path, linkInfo, err) } - if info.IsDir() { + if linkInfo.IsDir() { return symlinkWalk(finalPath, path, walkFn) } } return walkFn(path, info, err) } - return filepath.Walk(filename, symWalkFunc) + + err := filepath.Walk(filename, symWalkFunc) + if err != nil { + return fmt.Errorf("error walking %s: %w", filename, err) + } + + return nil } func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error { @@ -986,15 +1011,17 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s restart = append(restart, secret.RestartUnits...) reload = append(reload, secret.ReloadUnits...) newSecrets[secret.Name] = true + continue } - return err + + return fmt.Errorf("cannot read %s: %w", oldPath, err) } // Read the new file newData, err := os.ReadFile(newPath) if err != nil { - return err + return fmt.Errorf("cannot read %s: %w", newPath, err) } if !bytes.Equal(oldData, newData) { @@ -1017,15 +1044,17 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s restart = append(restart, template.RestartUnits...) reload = append(reload, template.ReloadUnits...) newTemplates[template.Name] = true + continue } - return err + + return fmt.Errorf("cannot read %s: %w", oldPath, err) } // Read the new file newData, err := os.ReadFile(newPath) if err != nil { - return err + return fmt.Errorf("cannot read %s: %w", newPath, err) } if !bytes.Equal(oldData, newData) { @@ -1040,20 +1069,21 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if _, err := os.Stat(filepath.Dir(file)); err != nil { if os.IsNotExist(err) { return nil - } else { - return err } + + return fmt.Errorf("cannot stat %s: %w", filepath.Dir(file), err) } + f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { - return err + return fmt.Errorf("cannot open %s: %w", file, err) } defer f.Close() for _, unit := range list { if _, err = f.WriteString(unit + "\n"); err != nil { - return err + return fmt.Errorf("cannot write to %s: %w", file, err) } } } @@ -1096,7 +1126,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s // it's a secret. rel, err := filepath.Rel(symlinkRenderedPath, path) if err != nil { - return err + return fmt.Errorf("cannot get relative path: %w", err) } isSecret := strings.HasPrefix(rel, "..") @@ -1203,7 +1233,7 @@ func parseFlags(args []string) (*options, error) { fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root or the user executing the tool in user mode`) if err := fs.Parse(args[1:]); err != nil { - return nil, err + return nil, fmt.Errorf("error parsing flags: %w", err) } switch CheckMode(checkMode) { diff --git a/pkgs/sops-install-secrets/sshkeys/convert.go b/pkgs/sops-install-secrets/sshkeys/convert.go index 495124a1..6935f526 100644 --- a/pkgs/sops-install-secrets/sshkeys/convert.go +++ b/pkgs/sops-install-secrets/sshkeys/convert.go @@ -15,7 +15,7 @@ import ( func parsePrivateKey(sshPrivateKey []byte) (*rsa.PrivateKey, error) { privateKey, err := ssh.ParseRawPrivateKey(sshPrivateKey) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse private ssh key: %w", err) } rsaKey, ok := privateKey.(*rsa.PrivateKey) @@ -63,7 +63,7 @@ func SSHPrivateKeyToPGP(sshPrivateKey []byte) (*openpgp.Entity, error) { err = gpgKey.Identities[uid.Id].SelfSignature.SignUserId(uid.Id, gpgKey.PrimaryKey, gpgKey.PrivateKey, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to sign user id: %w", err) } return gpgKey, nil From 1b8016259b17ecdfa5b70cc6515ec0712dcf113b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:30:40 +0100 Subject: [PATCH 19/32] don't capatalize errors --- pkgs/sops-install-secrets/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 53ca6cb2..1d40c9c1 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -345,7 +345,7 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { 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) + return fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err) } } case JSON: @@ -353,7 +353,7 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { 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) + return fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err) } } default: From fc20a8fdf9f10bc8a1f9d2e2c063c14dc8bd82c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:37:18 +0100 Subject: [PATCH 20/32] add newlines before return --- pkgs/sops-install-secrets/linux.go | 2 ++ pkgs/sops-install-secrets/main.go | 26 ++++++++++++++++++++++++++ pkgs/sops-install-secrets/main_test.go | 3 +++ 3 files changed, 31 insertions(+) diff --git a/pkgs/sops-install-secrets/linux.go b/pkgs/sops-install-secrets/linux.go index a92e1a36..f044ff8b 100644 --- a/pkgs/sops-install-secrets/linux.go +++ b/pkgs/sops-install-secrets/linux.go @@ -16,6 +16,7 @@ func RuntimeDir() (string, error) { if !ok { return "", errors.New("$XDG_RUNTIME_DIR is not set") } + return rundir, nil } @@ -53,6 +54,7 @@ func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) if err != nil { return fmt.Errorf("cannot change owner of '%s' to %d/%d: %w", symlinkToCheck, owner, group, err) } + return nil } diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 1d40c9c1..b086d93e 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -191,6 +191,7 @@ func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, g } else { panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.") } + return linkTarget == targetFile && validUG } @@ -207,6 +208,7 @@ func createSymlink(targetFile string, path string, owner int, group int, userMod return fmt.Errorf("cannot chown symlink '%s': %w", path, err) } } + return nil } else if err != nil { return fmt.Errorf("cannot stat '%s': %w", path, err) @@ -288,8 +290,10 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er if keyUntilNow != "" { keyUntilNow += "/" } + return "", fmt.Errorf("the key '%s%s' cannot be found", keyUntilNow, currentKey) } + break } @@ -326,6 +330,7 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er if !ok { return "", fmt.Errorf("the value of key '%s' is not a string", keyUntilNow) } + return strVal, nil } @@ -378,6 +383,7 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { } sourceFiles[s.SopsFile] = sourceFile + return nil } @@ -388,6 +394,7 @@ func decryptSecrets(secrets []secret) error { return err } } + return nil } @@ -431,6 +438,7 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us return nil, fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", dir, keysGID, err) } } + return &dir, nil } @@ -450,6 +458,7 @@ func createParentDirs(parent string, target string, keysGID int, userMode bool) } } } + return nil } @@ -471,6 +480,7 @@ func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool } } } + return nil } @@ -484,6 +494,7 @@ func lookupGroup(groupname string) (int, error) { if err != nil { return 0, fmt.Errorf("cannot parse keys gid %s: %w", group.Gid, err) } + return int(gid), nil } @@ -497,6 +508,7 @@ func lookupKeysGroup() (int, error) { if err2 == nil { return gid, nil } + return 0, fmt.Errorf("can't find group 'keys' nor 'nogroup' (%w)", err2) } @@ -517,6 +529,7 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { if err := json.Unmarshal(cipherText, &keys); err != nil { return nil, fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err) } + return &secretFile{cipherText: cipherText, firstSecret: s}, nil case Yaml: if err := yaml.Unmarshal(cipherText, &keys); err != nil { @@ -565,6 +578,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) } } + return nil } @@ -573,6 +587,7 @@ func validateMode(mode string) (os.FileMode, error) { if err != nil { return 0, fmt.Errorf("invalid number in mode: %s: %w", mode, err) } + return os.FileMode(parsed), nil } @@ -586,6 +601,7 @@ func validateOwner(owner string) (int, error) { if err != nil { return 0, fmt.Errorf("cannot parse uid %s: %w", lookedUp.Uid, err) } + return int(ownerNr), nil } @@ -599,6 +615,7 @@ func validateGroup(group string) (int, error) { if err != nil { return 0, fmt.Errorf("cannot parse gid %s: %w", lookedUp.Gid, err) } + return int(groupNr), nil } @@ -673,6 +690,7 @@ func renderTemplate(content *string, secretByPlaceholder map[string]*secret) str for placeholder, secret := range secretByPlaceholder { rendered = strings.ReplaceAll(rendered, placeholder, string(secret.value)) } + return rendered } @@ -765,6 +783,7 @@ func (app *appContext) validateManifest() error { return err } } + return nil } @@ -1087,6 +1106,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } } } + return nil } @@ -1150,6 +1170,7 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s removedTemplates[path] = true } + return nil }) if err != nil { @@ -1210,6 +1231,7 @@ func setupGPGKeyring(logcfg loggingConfig, sshKeys []string, parentDir string) ( if err := importSSHKeys(logcfg, sshKeys, dir); err != nil { os.RemoveAll(dir) + return nil, err } @@ -1245,10 +1267,12 @@ func parseFlags(args []string) (*options, error) { if fs.NArg() != 1 { flag.Usage() + return nil, flag.ErrHelp } opts.manifest = fs.Arg(0) + return &opts, nil } @@ -1264,6 +1288,7 @@ func replaceRuntimeDir(path, rundir string) (ret string) { first = false ret += strings.ReplaceAll(part, "%r", rundir) } + return } @@ -1285,6 +1310,7 @@ func writeTemplates(targetDir string, templates []template, keysGID int, userMod } } } + return nil } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index cd0b2f38..595d6b15 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -52,6 +52,7 @@ func writeManifest(t *testing.T, dir string, m *manifest) string { encoder := json.NewEncoder(f) ok(t, encoder.Encode(m)) f.Close() + return filename } @@ -62,6 +63,7 @@ func testAssetPath() string { } _, filename, _, _ := runtime.Caller(0) + return path.Join(path.Dir(filename), "test-assets") } @@ -78,6 +80,7 @@ func newTestDir(t *testing.T) testDir { tempdir, err := os.MkdirTemp("", "symlinkDir") ok(t, err) + return testDir{tempdir, path.Join(tempdir, "secrets.d"), path.Join(tempdir, "secrets")} } From 1f6602202558cb2ff27c322942920a70e6f42926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:44:56 +0100 Subject: [PATCH 21/32] don't use named returns --- pkgs/sops-install-secrets/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index b086d93e..722b8aea 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -1276,9 +1276,10 @@ func parseFlags(args []string) (*options, error) { return &opts, nil } -func replaceRuntimeDir(path, rundir string) (ret string) { +func replaceRuntimeDir(path, rundir string) string { parts := strings.Split(path, "%%") first := true + ret := "" for _, part := range parts { if !first { @@ -1289,7 +1290,7 @@ func replaceRuntimeDir(path, rundir string) (ret string) { ret += strings.ReplaceAll(part, "%r", rundir) } - return + return ret } func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error { From 35a86416aadac21de80303667940599d69562faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:46:40 +0100 Subject: [PATCH 22/32] always check for errors on type casting --- pkgs/sops-install-secrets/main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 722b8aea..3914eae7 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -322,7 +322,12 @@ func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, er currentData = make(map[string]interface{}) for key, value := range valWithWrongType { - currentData[key.(string)] = value + keyStr, ok := key.(string) + if !ok { + return "", fmt.Errorf("the key '%s' is not a string", key) + } + + currentData[keyStr] = value } } From fa1c48a0c0a1eb7eeee63d1d0d8b6e32bb8ce31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:50:52 +0100 Subject: [PATCH 23/32] check for is dry activation in one place --- pkgs/sops-install-secrets/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 3914eae7..04adb403 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -91,6 +91,8 @@ type secretFile struct { firstSecret *secret } +var isDryActivate = os.Getenv("NIXOS_ACTION") == "dry-activate" //nolint:gochecknoglobals + type FormatType string const ( @@ -632,7 +634,7 @@ func (app *appContext) validateSecret(secret *secret) error { secret.mode = mode - if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { + if app.ignorePasswd || isDryActivate { secret.owner = 0 secret.group = 0 } else if app.checkMode == Off || app.ignorePasswd { @@ -707,7 +709,7 @@ func (app *appContext) validateTemplate(template *template) error { template.mode = mode - if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { + if app.ignorePasswd || isDryActivate { template.owner = 0 template.group = 0 } else if app.checkMode == Off || app.ignorePasswd { @@ -1378,8 +1380,6 @@ func installSecrets(args []string) error { } } - isDry := os.Getenv("NIXOS_ACTION") == "dry-activate" - if err = MountSecretFs(manifest.SecretsMountPoint, keysGID, manifest.UseTmpfs, manifest.UserMode); err != nil { return fmt.Errorf("failed to mount filesystem for secrets: %w", err) } @@ -1459,12 +1459,12 @@ func installSecrets(args []string) error { } if !manifest.UserMode { - if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets, manifest.Templates); err != nil { + if err := handleModifications(isDryActivate, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets, manifest.Templates); err != nil { return fmt.Errorf("cannot request units to restart: %w", err) } } // No need to perform the actual symlinking - if isDry { + if isDryActivate { return nil } From 17bc7838d8dbbc429fc88622ab63e499422b75cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 09:57:49 +0100 Subject: [PATCH 24/32] use switch case where possible --- pkgs/sops-install-secrets/main.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 04adb403..42a78997 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -218,11 +218,13 @@ func createSymlink(targetFile string, path string, owner int, group int, userMod if stat.Mode()&os.ModeSymlink == os.ModeSymlink { linkTarget, err := os.Readlink(path) - if os.IsNotExist(err) { + + switch { + case os.IsNotExist(err): continue - } else if err != nil { + case err != nil: return fmt.Errorf("cannot read symlink '%s': %w", path, err) - } else if linksAreEqual(linkTarget, targetFile, stat, owner, group) { + case linksAreEqual(linkTarget, targetFile, stat, owner, group): return nil } } @@ -736,23 +738,20 @@ func (app *appContext) validateTemplate(template *template) error { } } - var templateText string - - if template.Content != "" { - templateText = template.Content - } else if template.File != "" { + switch { + case template.Content != "": + template.content = template.Content + case 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 { + template.content = string(templateBytes) + _: return fmt.Errorf("neither content nor file was specified for template %s", template.Name) } - template.content = templateText - return nil } From 035bd53bb7dc14223fda1c06bad610ec10dc218c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 10:08:28 +0100 Subject: [PATCH 25/32] avoid various type conversions i.e. int -> uint32 --- pkgs/sops-install-secrets/darwin.go | 4 +-- pkgs/sops-install-secrets/linux.go | 11 ++++--- pkgs/sops-install-secrets/main.go | 50 ++++++++++++++--------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/pkgs/sops-install-secrets/darwin.go b/pkgs/sops-install-secrets/darwin.go index 05cbf757..96d5baa1 100644 --- a/pkgs/sops-install-secrets/darwin.go +++ b/pkgs/sops-install-secrets/darwin.go @@ -23,7 +23,7 @@ func RuntimeDir() (string, error) { return strings.TrimSuffix(rundir, "/"), nil } -func SecureSymlinkChown(symlinkToCheck string, expectedTarget string, owner, group int) error { +func SecureSymlinkChown(symlinkToCheck string, expectedTarget string, owner, group uint32) error { // not sure what O_PATH is needed for anyways fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_SYMLINK|unix.O_NOFOLLOW, 0) if err != nil { @@ -39,7 +39,7 @@ func SecureSymlinkChown(symlinkToCheck string, expectedTarget string, owner, gro if n > len(expectedTarget) || string(buf[:n]) != expectedTarget { return fmt.Errorf("symlink %s does not point to %s", symlinkToCheck, expectedTarget) } - err = unix.Fchownat(fd, "", owner, group, unix.AT_SYMLINK_NOFOLLOW) + err = unix.Fchownat(fd, "", int(owner), int(group), unix.AT_SYMLINK_NOFOLLOW) if err != nil { return fmt.Errorf("cannot change owner of '%s' to %d/%d: %w", symlinkToCheck, owner, group, err) } diff --git a/pkgs/sops-install-secrets/linux.go b/pkgs/sops-install-secrets/linux.go index f044ff8b..a4beebdb 100644 --- a/pkgs/sops-install-secrets/linux.go +++ b/pkgs/sops-install-secrets/linux.go @@ -20,7 +20,7 @@ func RuntimeDir() (string, error) { return rundir, nil } -func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) error { +func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group uint32) error { // fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_PATH|unix.O_NOFOLLOW, 0) fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_PATH|unix.O_NOFOLLOW, 0) if err != nil { @@ -46,11 +46,11 @@ func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) return fmt.Errorf("cannot stat '%s': %w", symlinkToCheck, err) } - if stat.Uid == uint32(owner) && stat.Gid == uint32(group) { + if stat.Uid == owner && stat.Gid == group { return nil // already correct } - err = unix.Fchownat(fd, "", owner, group, unix.AT_EMPTY_PATH) + err = unix.Fchownat(fd, "", int(owner), int(group), unix.AT_EMPTY_PATH) if err != nil { return fmt.Errorf("cannot change owner of '%s' to %d/%d: %w", symlinkToCheck, owner, group, err) } @@ -80,13 +80,14 @@ func MountSecretFs(mountpoint string, keysGID int, useTmpfs bool, userMode bool) if err := unix.Statfs(mountpoint, &buf); err != nil { return fmt.Errorf("cannot get statfs for directory '%s': %w", mountpoint, err) } - if int32(buf.Type) != fsmagic { + + if int32(buf.Type) != fsmagic { //nolint:gosec if err := unix.Mount("none", mountpoint, fstype, unix.MS_NODEV|unix.MS_NOSUID, "mode=0751"); err != nil { return fmt.Errorf("cannot mount: %w", err) } } - if err := os.Chown(mountpoint, 0, int(keysGID)); err != nil { + if err := os.Chown(mountpoint, 0, keysGID); err != nil { return fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", mountpoint, keysGID, err) } diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 42a78997..7296aab4 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -30,9 +30,9 @@ type secret struct { Key string `json:"key"` Path string `json:"path"` Owner *string `json:"owner,omitempty"` - UID int `json:"uid"` + UID uint32 `json:"uid"` Group *string `json:"group,omitempty"` - GID int `json:"gid"` + GID uint32 `json:"gid"` SopsFile string `json:"sopsFile"` Format FormatType `json:"format"` Mode string `json:"mode"` @@ -40,8 +40,8 @@ type secret struct { ReloadUnits []string `json:"reloadUnits"` value []byte mode os.FileMode - owner int - group int + owner uint32 + group uint32 } type loggingConfig struct { @@ -55,17 +55,17 @@ type template struct { Path string `json:"path"` Mode string `json:"mode"` Owner *string `json:"owner,omitempty"` - UID int `json:"uid"` + UID uint32 `json:"uid"` Group *string `json:"group,omitempty"` - GID int `json:"gid"` + GID uint32 `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 + owner uint32 + group uint32 } type manifest struct { @@ -185,11 +185,11 @@ func readManifest(path string) (*manifest, error) { return &m, nil } -func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, group int) bool { +func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner uint32, group uint32) bool { validUG := true if stat, ok := info.Sys().(*syscall.Stat_t); ok { - validUG = validUG && int(stat.Uid) == owner - validUG = validUG && int(stat.Gid) == group + validUG = validUG && stat.Uid == owner + validUG = validUG && stat.Gid == group } else { panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.") } @@ -197,7 +197,7 @@ func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, g return linkTarget == targetFile && validUG } -func createSymlink(targetFile string, path string, owner int, group int, userMode bool) error { +func createSymlink(targetFile string, path string, owner uint32, group uint32, userMode bool) error { for { stat, err := os.Lstat(path) if os.IsNotExist(err) { @@ -430,7 +430,7 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us } generation++ - dir := filepath.Join(secretMountpoint, strconv.Itoa(int(generation))) + dir := filepath.Join(secretMountpoint, strconv.FormatUint(generation, 10)) if _, err := os.Stat(dir); !os.IsNotExist(err) { if err := os.RemoveAll(dir); err != nil { @@ -443,7 +443,7 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us } if !userMode { - if err := os.Chown(dir, 0, int(keysGID)); err != nil { + if err := os.Chown(dir, 0, keysGID); err != nil { return nil, fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", dir, keysGID, err) } } @@ -462,7 +462,7 @@ func createParentDirs(parent string, target string, keysGID int, userMode bool) } if !userMode { - if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil { + if err := os.Chown(pathSoFar, 0, keysGID); err != nil { return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) } } @@ -479,12 +479,12 @@ func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool return err } - if err := os.WriteFile(fp, []byte(secret.value), secret.mode); err != nil { + if err := os.WriteFile(fp, secret.value, secret.mode); err != nil { return fmt.Errorf("cannot write %s: %w", fp, err) } if !userMode { - if err := os.Chown(fp, secret.owner, secret.group); err != nil { + if err := os.Chown(fp, int(secret.owner), int(secret.group)); err != nil { return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err) } } @@ -600,32 +600,32 @@ func validateMode(mode string) (os.FileMode, error) { return os.FileMode(parsed), nil } -func validateOwner(owner string) (int, error) { +func validateOwner(owner string) (uint32, 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) + ownerNr, err := strconv.ParseUint(lookedUp.Uid, 10, 32) if err != nil { return 0, fmt.Errorf("cannot parse uid %s: %w", lookedUp.Uid, err) } - return int(ownerNr), nil + return uint32(ownerNr), nil } -func validateGroup(group string) (int, error) { +func validateGroup(group string) (uint32, 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) + groupNr, err := strconv.ParseUint(lookedUp.Gid, 10, 32) if err != nil { return 0, fmt.Errorf("cannot parse gid %s: %w", lookedUp.Gid, err) } - return int(groupNr), nil + return uint32(groupNr), nil } func (app *appContext) validateSecret(secret *secret) error { @@ -1307,12 +1307,12 @@ func writeTemplates(targetDir string, templates []template, keysGID int, userMod return err } - if err := os.WriteFile(fp, []byte(template.value), template.mode); err != nil { + if err := os.WriteFile(fp, 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 { + if err := os.Chown(fp, int(template.owner), int(template.group)); err != nil { return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, template.owner, template.group, err) } } From c4a672fdec9d9d758a85d9d91ce0f81402e17990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 10:11:00 +0100 Subject: [PATCH 26/32] importAgeSSHKeys never returnss an error --- pkgs/sops-install-secrets/main.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 7296aab4..d75c8ade 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -932,7 +932,7 @@ func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) erro return nil } -func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File) error { +func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File) { for _, p := range keyPaths { // Read the key sshKey, err := os.ReadFile(p) @@ -962,8 +962,6 @@ func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File) continue } } - - return nil } // Like filepath.Walk but symlink-aware. @@ -1414,10 +1412,7 @@ func installSecrets(args []string) error { // Import SSH keys if len(manifest.AgeSSHKeyPaths) != 0 { - err = importAgeSSHKeys(manifest.Logging, manifest.AgeSSHKeyPaths, *ageFile) - if err != nil { - return err - } + importAgeSSHKeys(manifest.Logging, manifest.AgeSSHKeyPaths, *ageFile) } // Import the keyfile if manifest.AgeKeyFile != "" { From a2b11a4b86d07534057581a2c68d6655e0e5249f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 10:16:47 +0100 Subject: [PATCH 27/32] ingore recvcheck lint for FormatType --- pkgs/sops-install-secrets/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index d75c8ade..77654373 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -93,7 +93,7 @@ type secretFile struct { var isDryActivate = os.Getenv("NIXOS_ACTION") == "dry-activate" //nolint:gochecknoglobals -type FormatType string +type FormatType string //nolint:recvcheck const ( Yaml FormatType = "yaml" From 1674c94dc09e868c255e3d268ed3cafbacdf2228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 20 Nov 2024 10:17:27 +0100 Subject: [PATCH 28/32] enable more golangci-lint checks --- .golangci.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2b94076c..433b33de 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,20 @@ linters: - presets: - - bugs - - unused - enable: - - gofmt - - misspell - - revive - - stylecheck + enable-all: true disable: - # direnv is not a web server, context is not strictly necessary. - - noctx + - cyclop + - depguard + - dogsled + - err113 + - exhaustruct + - exportloopref + - forbidigo + - funlen + - gocognit + - gocyclo + - godot + - godox + - lll + - maintidx + - mnd + - nestif + - varnamelen From 563411a342b624a481bcb6c8b1561319852337c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 24 Nov 2024 15:28:29 +0100 Subject: [PATCH 29/32] unit-test: pass in sops-install-secrets via callPackage --- default.nix | 2 -- flake.nix | 6 ++++-- pkgs/unit-tests.nix | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/default.nix b/default.nix index 3546d4fc..efc7b598 100644 --- a/default.nix +++ b/default.nix @@ -16,8 +16,6 @@ rec { # backwards compatibility inherit (pkgs) ssh-to-pgp; - - unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; } // (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { lint = pkgs.callPackage ./pkgs/lint.nix { diff --git a/flake.nix b/flake.nix index 57a5056d..724eda1e 100644 --- a/flake.nix +++ b/flake.nix @@ -156,9 +156,11 @@ ); devShells = eachSystem ( - { pkgs, ... }: + { system, pkgs, ... }: { - unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; + unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { + sops-install-secrets = self.packages.${system}.sops-install-secrets; + }; default = pkgs.callPackage ./shell.nix { }; } ); diff --git a/pkgs/unit-tests.nix b/pkgs/unit-tests.nix index 9fc14fcd..5126da9b 100644 --- a/pkgs/unit-tests.nix +++ b/pkgs/unit-tests.nix @@ -1,17 +1,17 @@ { - pkgs ? import { }, + stdenv, + gnupg, + util-linux, + nix, + sops-install-secrets, }: -let - sopsPkgs = import ../. { inherit pkgs; }; -in -pkgs.stdenv.mkDerivation { - name = "unit-tests"; - nativeBuildInputs = with pkgs; [ - bashInteractive +stdenv.mkDerivation { + name = "unittests"; + nativeBuildInputs = [ gnupg util-linux nix - sopsPkgs.sops-install-secrets.unittest + sops-install-secrets.unittest ]; # allow to prefetch shell dependencies in build phase dontUnpack = true; From 915b7c3c0ebc55319d04740ce64bd6e386081182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 24 Nov 2024 15:34:28 +0100 Subject: [PATCH 30/32] move lint and cross-build to flake.nix --- default.nix | 9 --------- flake.nix | 8 ++++++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index efc7b598..1e17c35b 100644 --- a/default.nix +++ b/default.nix @@ -17,12 +17,3 @@ rec { # backwards compatibility inherit (pkgs) ssh-to-pgp; } -// (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { - lint = pkgs.callPackage ./pkgs/lint.nix { - inherit sops-install-secrets; - }; - - cross-build = pkgs.callPackage ./pkgs/cross-build.nix { - inherit sops-install-secrets; - }; -}) diff --git a/flake.nix b/flake.nix index 724eda1e..1b95647c 100644 --- a/flake.nix +++ b/flake.nix @@ -99,6 +99,14 @@ (pkgs.callPackage ./formatter.nix { inputs = privateInputs; }).config.build.check; + + cross-build = pkgs.callPackage ./pkgs/cross-build.nix { + sops-install-secrets = self.packages.${system}.sops-install-secrets; + }; + + lint = pkgs.callPackage ./pkgs/lint.nix { + sops-install-secrets = self.packages.${system}.sops-install-secrets; + }; } // (suffix-stable packages-stable) // nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux tests From b93e7c42ee8c6019cca96d09bfb71830d6279c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 24 Nov 2024 15:36:01 +0100 Subject: [PATCH 31/32] default.nix: don't use rec --- default.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 1e17c35b..37f54ce1 100644 --- a/default.nix +++ b/default.nix @@ -3,13 +3,13 @@ vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=", }: let + sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { }; +in +{ sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; }; -in -rec { - inherit sops-install-secrets; - sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { }; + inherit sops-init-gpg-key; default = sops-init-gpg-key; sops-import-keys-hook = pkgs.callPackage ./pkgs/sops-import-keys-hook { }; From a43bb04209b4a68d4c662b6c7c788475c98c153a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 24 Nov 2024 16:53:46 +0100 Subject: [PATCH 32/32] fix vendor hash --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 37f54ce1..7e15b832 100644 --- a/default.nix +++ b/default.nix @@ -1,6 +1,6 @@ { pkgs ? import { }, - vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=", + vendorHash ? "sha256-dWo8SAEUVyBhKyKoIj2u1VHiWPMod9veYGbivXkUI2Y=", }: let sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { };