Add age support

This commit is contained in:
Janne Heß 2021-08-27 00:49:58 +02:00
parent 9d47d2e3e4
commit f5a2ba217b
No known key found for this signature in database
GPG key ID: 69165158F05265DF
6 changed files with 145 additions and 39 deletions

View file

@ -85,7 +85,7 @@ let
# Does this need to be configurable?
secretsMountPoint = "/run/secrets.d";
symlinkPath = "/run/secrets";
inherit (cfg) gnupgHome sshKeyPaths;
inherit (cfg) gnupgHome sshKeyPaths ageKeyFile;
});
checkedManifest = let
@ -130,6 +130,25 @@ in {
'';
};
ageKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/var/lib/sops-nix/key.txt";
description = ''
Path to age key file used for sops decryption.
'';
};
generateAgeKey = mkOption {
type = types.bool;
default = false;
description = ''
Whether or not to generate the age key. If this
option is set to false, the key must already be
present at the specified location.
'';
};
gnupgHome = mkOption {
type = types.nullOr types.str;
default = null;
@ -152,8 +171,11 @@ in {
};
config = mkIf (cfg.secrets != {}) {
assertions = [{
assertion = (cfg.gnupgHome == null) != (cfg.sshKeyPaths == []);
assertion = cfg.ageKeyFile == null -> (cfg.gnupgHome == null) != (cfg.sshKeyPaths == []);
message = "Exactly one of sops.gnupgHome and sops.sshKeyPaths must be set";
} {
assertion = (cfg.sshKeyPaths != [] || cfg.gnupgHome != null) != (cfg.ageKeyFile != null);
message = "sops.ageKeyFile is mutually exclusive with sops.gnupgHome and sops.sshKeyPaths";
}] ++ optionals cfg.validateSopsFiles (
concatLists (mapAttrsToList (name: secret: [{
assertion = builtins.pathExists secret.sopsFile;
@ -168,9 +190,18 @@ in {
system.activationScripts.setup-secrets = let
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets;
in stringAfter [ "specialfs" "users" "groups" ] ''
in stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.generateAgeKey "generate-age-key") ''
echo setting up secrets...
${optionalString (cfg.gnupgHome != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${checkedManifest}
'';
system.activationScripts.generate-age-key = (mkIf cfg.generateAgeKey) (stringAfter [] ''
if [[ ! -f "${cfg.ageKeyFile}" ]]; then;
echo generating machine-specific age key...
mkdir -p $(dirname ${cfg.ageKeyFile})
# age-keygen sets 0600 by default, no need to chmod.
${pkgs.age}/bin/age-keygen -o ${cfg.ageKeyFile}
fi
'');
};
}

View file

@ -5,7 +5,6 @@ package main
import (
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
@ -47,6 +46,7 @@ type manifest struct {
SymlinkPath string `json:"symlinkPath"`
SSHKeyPaths []string `json:"sshKeyPaths"`
GnupgHome string `json:"gnupgHome"`
AgeKeyFile string `json:"ageKeyFile"`
}
type secretFile struct {
@ -437,10 +437,17 @@ func (app *appContext) validateManifest() error {
if m.SymlinkPath == "" {
m.SymlinkPath = "/run/secrets"
}
if len(m.SSHKeyPaths) > 0 && m.GnupgHome != "" {
return errors.New("gnupgHome and sshKeyPaths were specified in the manifest. " +
"Both options are mutual exclusive.")
if m.GnupgHome != "" {
errorFmt := "gnupgHome and %s were specified in the manifest. " +
"Both options are mutually exclusive."
if len(m.SSHKeyPaths) > 0 {
return fmt.Errorf(errorFmt, "sshKeyPaths")
}
if m.AgeKeyFile != "" {
return fmt.Errorf(errorFmt, "ageKeyFile")
}
}
for i := range m.Secrets {
if err := app.validateSecret(&m.Secrets[i]); err != nil {
return err
@ -604,6 +611,8 @@ func installSecrets(args []string) error {
defer keyring.Remove()
} else if manifest.GnupgHome != "" {
os.Setenv("GNUPGHOME", manifest.GnupgHome)
} else if manifest.AgeKeyFile != "" {
os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile)
}
if err := decryptSecrets(manifest.Secrets); err != nil {

View file

@ -219,10 +219,44 @@ func testSSHKey(t *testing.T) {
testInstallSecret(t, testdir, &m)
}
func testAge(t *testing.T) {
assets := testAssetPath()
testdir := newTestDir(t)
defer testdir.Remove()
target := path.Join(testdir.path, "existing-target")
file, err := os.Create(target)
ok(t, err)
file.Close()
s := secret{
Name: "test",
Key: "test_key",
Owner: "nobody",
Group: "nogroup",
SopsFile: path.Join(assets, "secrets.yaml"),
Path: target,
Mode: "0400",
RestartServices: []string{"affected-service"},
ReloadServices: make([]string, 0),
}
m := manifest{
Secrets: []secret{s},
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
AgeKeyFile: path.Join(assets, "age-keys.txt"),
}
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)
testAge(t)
}
func TestValidateManifest(t *testing.T) {

View file

@ -23,6 +23,26 @@
inherit (pkgs) system;
};
age-keys = makeTest {
name = "sops-age-keys";
machine = {
imports = [ ../../modules/sops ];
sops = {
ageKeyFile = ./test-assets/age-keys.txt;
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = {};
};
};
testScript = ''
start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
} {
inherit pkgs;
inherit (pkgs) system;
};
pgp-keys = makeTest {
name = "sops-pgp-keys";
nodes.server = { pkgs, lib, config, ... }: {

View file

@ -0,0 +1,3 @@
# created: 2020-07-18T03:16:47-07:00
# public key: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
AGE-SECRET-KEY-1NJT5YCS2LWU4V4QAJQ6R4JNU7LXPDX602DZ9NUFANVU5GDTGUWCQ5T59M6

View file

@ -1,47 +1,56 @@
test_key: ENC[AES256_GCM,data:4cC2PTi7xVPZPA==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:ZaKi9m6ziFKNV+gx7XedTw==,type:str]
test_key: ENC[AES256_GCM,data:6aaSGYcvIY1+lQ==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:/FSMgXuX8TnbxRxpcuwGEA==,type:str]
a_list:
- ENC[AES256_GCM,data:5K0=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:Op0+iEYzV+gfYGveN3VKKg==,type:str]
- ENC[AES256_GCM,data:9dM=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:HvbERoLZcUOjEd4AwLVNEg==,type:str]
- ENC[AES256_GCM,data:IBI=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:7A4l/SgzgxK9sqi+/15A6w==,type:str]
- ENC[AES256_GCM,data:tLE=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:cmhMaddcY2bhydWrPDWNlQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
lastmodified: '2021-01-27T06:12:22Z'
mac: ENC[AES256_GCM,data:/lwT78drEKdCoWW9TPU2H/IWlq/9uEmJocrTvftKTD1Au9e/7AMCUWGWMPGJKMg9R0FWV2pn3tgwli5YXRrIe4L9tIkeM5vJvz85IeQIc+vviby7PM8VtbO1ArisHh95cVwZuASR3KSbumnxURjayZ61J9Jiz0viBeuEmCP50u4=,iv:FX6XDUqetDaRTtLLfMaJAkPZmiZx59wnuDRm0SvmTJM=,tag:HnQ4dHsCCNim2v8WPXyLdw==,type:str]
age:
- recipient: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuY2RtdnhrMEtOeEUvRmFr
SU5uRm1xaU12R3F5TnJvbzI5YlNYMDFycGxvCnRBQVNwY2cxMDVicUNZWlpFdStp
ZTYxcUJkNEFHVkxTbGdrdkIxN3ZMNkUKLS0tIHd2K2ZhaFVoTmlhNDBKeXZyVHBh
ZXFnRy9hUTFNQm9rWVA3YnJXaWV0S2cKBpGxzhth36fab8KDKFBoweQO9L0juys4
cMjz2X/hXVMLvDeCLVBTZTj3K/lXAo4v2qMUGZsR2Idpw3FOPxfSGg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2021-08-26T22:38:49Z"
mac: ENC[AES256_GCM,data:1TNMF0HIfIOetCF4F268N5k2DFQ28JBYOdjPxfOuw03udJ1eKcTZrlBAGGFEgMdu1FuW+ZC+gLHW/b0GpfZWAtkiLuWP9SUof01rrFPYse6LvqWvlY1mLK6unbMveGeD9My3QTkZmt962I9tEHQW+ph35j80egGnN1f6stiJdlY=,iv:KEa/Q6q+B0F2Dv+/Km+2WeztYij0Of1pCSygGXM2fG4=,tag:TzuiTyYwTor36eGPKvTcwQ==,type:str]
pgp:
- created_at: '2020-07-12T08:03:51Z'
enc: |
- created_at: "2020-07-12T08:03:51Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA/m6nevQP1fAAQgAnGLEGUnWgYTLGGZN7sEETu14MJjr4TY7JDhs8VOrA9ng
x9ivSF1dPw6arewal70OgxMLnS55HY6L81vRCbu8d0rdfXkdzO6oeMAo79/udKc8
F5CDSkdrBImK5xpEQ0stpt1zmmKMAPGuPSbSc48YEt4OYUdpoIO+PesYUUgGerL7
4UvXtbhYjIhZL7Sgst/coOUNUWjpQgVRSeza6qcuVfS3gVBvxNCed51n+Cr3/rnD
5PXjeWQ8aayLDDV32CjGf+s3+LG0gvJ7A6eq0THjsvgkY4qGHNSMobkN7npFI4lX
KZG68Scu5d11IY2N3y0ijRQQxYlPPPpukWvw3CK8htJeAZltLDHzh0bpISpc7gqU
xzATRG/LmJk7yS9Vej/B0NGmkVlbB+lFnLZG3lVmDiaQUsgePp2Ho1/j6ysit3/C
i87IM8B71y1brldk2mcr1oAoNSVE4dg+/C2HrDRKxA==
=Qyuq
hQEMA/m6nevQP1fAAQf/Zih3JJl/L/ApqkwdhIv7iUQsbebXV+WRwlslwOz6by/t
Sf6CH3b5FHLaBQtPEUh6QNLpQSqm52Rs4MoJxML0Tan8eu6Q313lS09XP5mQP1yc
d1FN4Tsg8V/xaGMGAtJHM/bj+6vfimGtnnQvNZ7N7PW2U+fxpq51NmqIXMmG+jOw
2Tw+04oNoK6lrG89y0dzJrTvP9ph0wM+hwOgBthfhr0X6/UF7vS9fgEod+HFEqGn
MfAW4CiijXDvFYI5unwseeUE8IosOaq2VTW57eBSteLWIyJqmLUxvVSoMP0emitN
4T0DkzTxywq5KPUMIEISEZrxYEskatqlTdQBmcjEAdJcAQl/u1OGCn7YRIJbXjMP
8/UvlFVfaT3L19sgEg8rSVpo3GiwlwbP0KTcb4jVMu0mHhd70VciNKojtDVlZg9n
0V8Z2LwPHY0+IzcTJu5IS7sO/fuyYJohWQ4ZV7Y=
=1olg
-----END PGP MESSAGE-----
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
- created_at: '2020-07-12T08:03:51Z'
enc: |
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
- created_at: "2020-07-12T08:03:51Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQGMA3ulPRkZxd/UAQv/d1ExCl7gVIe0GeH8bc0xl8gt4hvR2ujH0BmmHpQ7SDLa
tS7lH3ennlLlN+owN3DkCKqm/BNEQ7Wy5y0oTvmQSo2BdSMYOvHrLKoPK3OsDLPH
vtXUKZ+NLKSV2xCSiqrcu4GRki8klLgDNEgna8T21erCXZM8W/ehcOd7gLSUiatd
wQTGEhNAA0RO04kf0NoPcNrPak7e0bGbwEc88RdM2QyxSXi6SVrKVHvYVPE3pfFB
VkUcWZU2edLUEKIAFRAtmb9s3DSjECMMuoVXR6lrPx4c6OMRKvxtlZh0uOOcRpoG
M5KL8O4ZXNxUVNRzPisgqe0hkh2AsnMLIQeWrVLfOLXUdDjr9x42vga/P8WU7AzT
/Lxmlu9cY0KKsXpv7yXibZfFJ/K/4Mo9WsNk6tGAmiSUg1hr/NJPha3PDHyX0ER7
6dPFeMrvs4VngC0I27v6rlEjU+HiRv3/uLnn04z9DRQEk6a+YpLZTpHvAbzfb1Dg
EBpKCQePm2rIzywLE8d60lABXSQKG4/LSH6WgxfRFibORME2z5DD2G7ibiDbVg8j
YbrH0E0KeRxYJMJ28FVDlLgoUL+JbNr70uHkdvz8S0hqCYwo+K4KqUOIsiPc6iIL
tA==
=XoKf
hQGMA3ulPRkZxd/UAQv+LSFG7a6XqqW7GYdYLb1H0bv3NkxJeBYGohsMrEC4AXnA
RdhJ2B55WqiNyDmVmylalUKAPFpD7RJ4c6HDZcRLx6doy5wgA+oFvKCe8FcYy5YS
abSUhNoT0TwWlDQ4Bc4Q/QSu9kMCica56Bc10tz5Pvb5JEYApoLfL0kTO7mBI+w4
hKovAxlp9aoT9cky/7WFhmFn1+mYz//ejDKPuyVSYhM/2eIosDE9maPootLQMrIX
cqnsCDCOgMDwIOXt5/W4Cab799Pop4SQcFDSDa4FU4kN12vmNwJu7HPxpR+W80bL
GOUu35gVyykig9XCaTA/UCc85URtOMYmXPBsdTVoMVAgfxkji3ovJCEFBcI7wYgd
XudLQw5tJ9Pwz+5EeNw771ISaTBO7bWazSq+q3HrKW9kKLFJV4kVt7zLr+yZ1kNu
jEjTFmaKYl3p9AYh6yxb6n9PuTosk1ZJE8gv8ME+b4z21BVxfZsDfYWVhT9thZz5
q2LHw+E/fmf0EXz9tviS0k4BimrDWSnYNyuDqTImB+v7baY0rEoYhQY2ZLAbUsew
wg+KC2RjkapaZ7CFbCMs7xaWzZ5p/nfWOtsb1GvmvCUZcHK5MYeVM5S+Xf0GnYg=
=7cg6
-----END PGP MESSAGE-----
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
unencrypted_suffix: _unencrypted
version: 3.6.1