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/.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/.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
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
];
diff --git a/default.nix b/default.nix
index 63abb9b1..7e15b832 100644
--- a/default.nix
+++ b/default.nix
@@ -1,34 +1,19 @@
{
pkgs ? import { },
- vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=",
+ vendorHash ? "sha256-dWo8SAEUVyBhKyKoIj2u1VHiWPMod9veYGbivXkUI2Y=",
}:
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 { };
# 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 {
- 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 b389c09f..1b95647c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -95,6 +95,18 @@
in
{
home-manager = self.legacyPackages.${system}.homeConfigurations.sops.activation-script;
+ treefmt =
+ (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
@@ -131,6 +143,13 @@
}
);
+ formatter = eachSystem (
+ { pkgs, ... }:
+ (pkgs.callPackage ./formatter.nix {
+ inputs = privateInputs;
+ }).config.build.wrapper
+ );
+
apps = eachSystem (
{ pkgs, ... }:
{
@@ -145,9 +164,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/formatter.nix b/formatter.nix
new file mode 100644
index 00000000..4e6f13a0
--- /dev/null
+++ b/formatter.nix
@@ -0,0 +1,30 @@
+{ pkgs, inputs, ... }:
+inputs.treefmt-nix.lib.evalModule pkgs {
+ projectRootFile = ".git/config";
+
+ programs = {
+ gofumpt.enable = true;
+
+ nixfmt.enable = true;
+
+ deadnix.enable = true;
+ deno.enable = true;
+ shellcheck.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;
+ };
+ };
+ };
+}
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
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()
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/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/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 b551e4b0..a4beebdb 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,12 +14,13 @@ 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
}
-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 {
@@ -27,26 +29,32 @@ 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) {
+
+ 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)
}
+
return nil
}
@@ -60,8 +68,9 @@ 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
@@ -71,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 280df4c9..77654373 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"
@@ -31,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"`
@@ -41,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 {
@@ -56,33 +55,33 @@ 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 {
- 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 {
@@ -92,7 +91,9 @@ type secretFile struct {
firstSecret *secret
}
-type FormatType string
+var isDryActivate = os.Getenv("NIXOS_ACTION") == "dry-activate" //nolint:gochecknoglobals
+
+type FormatType string //nolint:recvcheck
const (
Yaml FormatType = "yaml"
@@ -118,9 +119,11 @@ 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)
+
switch t {
case "":
*f = Yaml
@@ -132,7 +135,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
@@ -165,52 +173,62 @@ 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
}
-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.")
}
+
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) {
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)
}
}
+
return nil
} 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) {
+
+ 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
}
}
+
if err := os.Remove(path); err != nil {
return fmt.Errorf("cannot override %s: %w", path, err)
}
@@ -223,10 +241,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 +257,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 +278,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 := ""
@@ -270,29 +294,44 @@ 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
}
+
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
+ keyStr, ok := key.(string)
+ if !ok {
+ return "", fmt.Errorf("the key '%s' is not a string", key)
+ }
+
+ currentData[keyStr] = value
}
}
@@ -300,6 +339,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
}
@@ -319,7 +359,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:
@@ -327,13 +367,14 @@ 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:
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,10 +386,13 @@ 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
}
@@ -359,6 +403,7 @@ func decryptSecrets(secrets []secret) error {
return err
}
}
+
return nil
}
@@ -369,10 +414,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,38 +428,46 @@ 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)))
+ dir := filepath.Join(secretMountpoint, strconv.FormatUint(generation, 10))
+
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 {
+ 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)
}
}
+
return &dir, nil
}
func createParentDirs(parent string, target string, keysGID int, userMode bool) error {
dirs := strings.Split(filepath.Dir(target), "/")
pathSoFar := parent
+
for _, dir := range dirs {
pathSoFar = filepath.Join(pathSoFar, dir)
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
}
+
if !userMode {
- if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil {
+ 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)
}
}
}
+
return nil
}
@@ -423,15 +478,18 @@ 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 {
+
+ 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)
}
}
}
+
return nil
}
@@ -440,10 +498,12 @@ 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)
}
+
return int(gid), nil
}
@@ -452,10 +512,12 @@ func lookupKeysGroup() (int, error) {
if err1 == nil {
return gid, nil
}
+
gid, err2 := lookupGroup("nogroup")
if err2 == nil {
return gid, nil
}
+
return 0, fmt.Errorf("can't find group 'keys' nor 'nogroup' (%w)", err2)
}
@@ -476,6 +538,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 {
@@ -486,7 +549,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 +560,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,12 +580,14 @@ 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 {
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
}
}
+
return nil
}
@@ -529,31 +596,36 @@ 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
}
-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 {
@@ -561,9 +633,10 @@ func (app *appContext) validateSecret(secret *secret) error {
if err != nil {
return err
}
+
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 {
@@ -574,6 +647,7 @@ func (app *appContext) validateSecret(secret *secret) error {
if err != nil {
return err
}
+
secret.owner = owner
}
@@ -584,6 +658,7 @@ func (app *appContext) validateSecret(secret *secret) error {
if err != nil {
return err
}
+
secret.group = group
}
}
@@ -602,6 +677,7 @@ func (app *appContext) validateSecret(secret *secret) error {
if err != nil {
return err
}
+
app.secretFiles[secret.SopsFile] = *maybeFile
file = *maybeFile
@@ -623,6 +699,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
}
@@ -631,9 +708,10 @@ func (app *appContext) validateTemplate(template *template) error {
if err != nil {
return err
}
+
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 {
@@ -644,6 +722,7 @@ func (app *appContext) validateTemplate(template *template) error {
if err != nil {
return err
}
+
template.owner = owner
}
@@ -654,25 +733,25 @@ 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 != "" {
+ 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
}
@@ -684,6 +763,7 @@ func (app *appContext) validateManifest() error {
if len(m.SSHKeyPaths) > 0 {
return fmt.Errorf(errorFmt, "sshKeyPaths")
}
+
if m.AgeKeyFile != "" {
return fmt.Errorf(errorFmt, "ageKeyFile")
}
@@ -709,27 +789,30 @@ func (app *appContext) validateManifest() error {
return err
}
}
+
return nil
}
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
+
defer func() {
if cleanup {
os.RemoveAll(d)
@@ -738,15 +821,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 {
@@ -771,6 +860,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 +872,7 @@ func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int)
if generationNum == currentGeneration {
continue
}
+
if currentGeneration-keepGenerations >= generationNum {
os.RemoveAll(path.Join(secretsMountPoint, generationName))
}
@@ -810,60 +901,67 @@ 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
}
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))
}
}
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)
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
}
}
-
- return nil
}
// Like filepath.Walk but symlink-aware.
@@ -873,30 +971,39 @@ 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 {
var restart []string
+
var reload []string
newSecrets := make(map[string]bool)
@@ -927,15 +1034,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) {
@@ -958,15 +1067,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) {
@@ -981,32 +1092,39 @@ 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)
}
}
}
+
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 +1136,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
}
@@ -1030,8 +1150,9 @@ 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, "..")
if isSecret {
@@ -1041,6 +1162,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,8 +1171,10 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s
return nil
}
}
+
removedTemplates[path] = true
}
+
return nil
})
if err != nil {
@@ -1064,6 +1188,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 +1200,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 +1230,15 @@ 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,16 +1246,20 @@ 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
+ return nil, fmt.Errorf("error parsing flags: %w", err)
}
switch CheckMode(checkMode) {
@@ -1138,23 +1271,30 @@ func parseFlags(args []string) (*options, error) {
if fs.NArg() != 1 {
flag.Usage()
+
return nil, flag.ErrHelp
}
+
opts.manifest = fs.Arg(0)
+
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 {
ret += "%"
}
+
first = false
ret += strings.ReplaceAll(part, "%r", rundir)
}
- return
+
+ return ret
}
func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error {
@@ -1165,15 +1305,17 @@ 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)
}
}
}
+
return nil
}
@@ -1191,16 +1333,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
}
@@ -1230,18 +1377,18 @@ 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)
}
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,28 +1400,30 @@ 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
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 != "" {
// 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 +1443,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)
}
@@ -1303,20 +1453,23 @@ 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
}
+
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 +1482,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 8c09e68c..595d6b15 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"
@@ -19,30 +18,41 @@ 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()
+
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)
+
encoder := json.NewEncoder(f)
ok(t, encoder.Encode(m))
f.Close()
+
return filename
}
@@ -51,7 +61,9 @@ func testAssetPath() string {
if assets != "" {
return assets
}
+
_, filename, _, _ := runtime.Caller(0)
+
return path.Join(path.Dir(filename), "test-assets")
}
@@ -64,43 +76,52 @@ 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)
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)))
- 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
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)
}
}()
- nobody := "nobody"
- nogroup := "nogroup"
// should create a symlink
+ nobody := NOBODY
+ nogroup := NOGROUP
yamlSecret := secret{
Name: "test",
Key: "test_key",
@@ -114,6 +135,7 @@ func testGPG(t *testing.T) {
}
var jsonSecret, binarySecret, dotenvSecret, iniSecret secret
+
root := "root"
// should not create a symlink
jsonSecret = yamlSecret
@@ -147,19 +169,19 @@ func testGPG(t *testing.T) {
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)
@@ -174,22 +196,24 @@ func testGPG(t *testing.T) {
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))
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)
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))
@@ -199,14 +223,16 @@ func testGPG(t *testing.T) {
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)
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)
@@ -217,8 +243,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",
@@ -242,6 +268,8 @@ func testSSHKey(t *testing.T) {
}
func TestAge(t *testing.T) {
+ t.Parallel()
+
assets := testAssetPath()
testdir := newTestDir(t)
@@ -252,8 +280,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",
@@ -277,6 +305,8 @@ func TestAge(t *testing.T) {
}
func TestAgeWithSSH(t *testing.T) {
+ t.Parallel()
+
assets := testAssetPath()
testdir := newTestDir(t)
@@ -287,8 +317,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",
@@ -311,20 +341,16 @@ 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)
defer testdir.Remove()
- nobody := "nobody"
- nogroup := "nogroup"
+ nobody := NOBODY
+ nogroup := NOGROUP
s := secret{
Name: "test",
Key: "test_key",
@@ -351,6 +377,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 {
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
- ];
-}
diff --git a/pkgs/sops-install-secrets/sshkeys/convert.go b/pkgs/sops-install-secrets/sshkeys/convert.go
index 070cf1e4..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)
@@ -60,9 +60,10 @@ 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
+ return nil, fmt.Errorf("failed to sign user id: %w", err)
}
return gpgKey, nil
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 acf6b1d7..00000000
--- a/pkgs/sops-pgp-hook/hook_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package main
-
-import (
- "bytes"
- "fmt"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "runtime"
- "strings"
- "testing"
-)
-
-// ok fails the test if an err is not nil.
-func ok(tb testing.TB, err error) {
- 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())
- tb.FailNow()
- }
-}
-
-func TestShellHook(t *testing.T) {
- 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 eba37387..00000000
Binary files a/pkgs/sops-pgp-hook/test-assets/existing-key.gpg and /dev/null differ
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 c168d740..00000000
Binary files a/pkgs/sops-pgp-hook/test-assets/keys/key.gpg and /dev/null differ
diff --git a/pkgs/sops-pgp-hook/test-assets/shell.nix b/pkgs/sops-pgp-hook/test-assets/shell.nix
deleted file mode 100644
index 71173fde..00000000
--- a/pkgs/sops-pgp-hook/test-assets/shell.nix
+++ /dev/null
@@ -1,14 +0,0 @@
-# shell.nix
-with import { };
-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..5126da9b 100644
--- a/pkgs/unit-tests.nix
+++ b/pkgs/unit-tests.nix
@@ -1,21 +1,18 @@
{
- pkgs ? import { },
+ stdenv,
+ gnupg,
+ util-linux,
+ nix,
+ sops-install-secrets,
}:
-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;
+stdenv.mkDerivation {
+ name = "unittests";
+ nativeBuildInputs = [
+ gnupg
+ util-linux
+ nix
+ 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
'';
}
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