From 2b9a0815caa1fe70b47575f8382211b331bed3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janne=20He=C3=9F?= Date: Fri, 10 Sep 2021 12:02:38 +0200 Subject: [PATCH] Implement nested secrets --- README.md | 10 +- pkgs/sops-install-secrets/main.go | 91 +++++++++++++++---- pkgs/sops-install-secrets/nixos-test.nix | 3 + .../test-assets/secrets.yaml | 9 +- 4 files changed, 91 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index aa8f0df..2522273 100644 --- a/README.md +++ b/README.md @@ -352,8 +352,13 @@ In our example we put the following content in it: example-key: example-value ``` -NOTE: At the moment we do not support nested data structures that -sops support. This might change in the future. See also [Different file formats](#different-file-formats) +Nesting the key results in the creation of directories. +These directories will be owned by root:keys and have permissions 0751. +```yaml +myservice: + my_subdir: + my_secret: example value +``` As a result when saving the file the following content will be in it: @@ -443,6 +448,7 @@ If you derived your server public key from ssh, all you need in your configurati sops.age.keyFile = "/var/lib/sops-nix/key.txt"; # This will generate a new key if the key specified above does not exist sops.age.generateKey = true; + sops.secrets."myservice/my_subdir/my_secret" = {}; } ``` diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index b098734..82cd82d 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -205,6 +205,54 @@ type plainData struct { binary []byte } +func recurseSecretKey(keys map[string]interface{}, wantedKey string) (string, error) { + var val interface{} + var ok bool + currentKey := wantedKey + currentData := keys + keyUntilNow := "" + + for { + slashIndex := strings.IndexByte(currentKey, '/') + if slashIndex == -1 { + // We got to the end + val, ok = currentData[currentKey] + if !ok { + if keyUntilNow != "" { + keyUntilNow += "." + } + return "", fmt.Errorf("b: 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) + } + 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 + } + } + + strVal, ok := val.(string) + if !ok { + return "", fmt.Errorf("The value of key '%s' is not a string", keyUntilNow) + } + return strVal, nil +} + func decryptSecret(s *secret, sourceFiles map[string]plainData) error { sourceFile := sourceFiles[s.SopsFile] if sourceFile.data == nil || sourceFile.binary == nil { @@ -229,14 +277,9 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { if s.Format == Binary { s.value = sourceFile.binary } else { - val, ok := sourceFile.data[s.Key] - - if !ok { - return fmt.Errorf("The key '%s' cannot be found in '%s'", s.Key, s.SopsFile) - } - strVal, ok := val.(string) - if !ok { - return fmt.Errorf("The value of key '%s' in '%s' is not a string", s.Key, s.SopsFile) + strVal, err := recurseSecretKey(sourceFile.data, s.Key) + if err != nil { + return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) } s.value = []byte(strVal) } @@ -308,14 +351,27 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int) (* return &dir, nil } -func writeSecrets(secretDir string, secrets []secret) error { +func writeSecrets(secretDir string, secrets []secret, keysGid int) error { for _, secret := range secrets { - filepath := filepath.Join(secretDir, secret.Name) - if err := ioutil.WriteFile(filepath, []byte(secret.value), secret.mode); err != nil { - return fmt.Errorf("Cannot write %s: %w", filepath, err) + fp := filepath.Join(secretDir, secret.Name) + + dirs := strings.Split(filepath.Dir(secret.Name), "/") + pathSoFar := secretDir + for _, dir := range dirs { + pathSoFar = filepath.Join(pathSoFar, dir) + if err := os.MkdirAll(pathSoFar, 0751); err != nil { + return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, fp, err) + } + if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil { + return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, fp, err) + } } - if err := os.Chown(filepath, secret.owner, secret.group); err != nil { - return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", filepath, secret.owner, secret.group, err) + + if err := ioutil.WriteFile(fp, []byte(secret.value), secret.mode); err != nil { + return fmt.Errorf("Cannot write %s: %w", fp, err) + } + if err := os.Chown(fp, secret.owner, secret.group); err != nil { + return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err) } } return nil @@ -374,8 +430,9 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { file.firstSecret.Format, file.firstSecret.Name) } if app.checkMode != Manifest && s.Format != Binary { - if _, ok := file.keys[s.Key]; !ok { - return fmt.Errorf("secret %s with the key %s not found in %s", s.Name, s.Key, s.SopsFile) + _, 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 @@ -680,7 +737,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); err != nil { + if err := writeSecrets(*secretDir, manifest.Secrets, keysGid); err != nil { return fmt.Errorf("Cannot write secrets: %w", err) } if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets); err != nil { diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 2e9a95d..d9f6571 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -87,6 +87,7 @@ sops.gnupg.home = "/run/gpghome"; sops.defaultSopsFile = ./test-assets/secrets.yaml; sops.secrets.test_key.owner = config.users.users.someuser.name; + sops.secrets."nested/test/file".owner = config.users.users.someuser.name; sops.secrets.existing-file = { key = "test_key"; path = "/run/existing-file"; @@ -118,6 +119,8 @@ assertEqual("test_value", value) server.succeed("runuser -u someuser -- cat /run/secrets/test_key >&2") + value = server.succeed("cat /run/secrets/nested/test/file") + assertEqual(value, "another value") target = server.succeed("readlink -f /run/existing-file") assertEqual("/run/secrets.d/1/existing-file", target.strip()) diff --git a/pkgs/sops-install-secrets/test-assets/secrets.yaml b/pkgs/sops-install-secrets/test-assets/secrets.yaml index ff111db..c552333 100644 --- a/pkgs/sops-install-secrets/test-assets/secrets.yaml +++ b/pkgs/sops-install-secrets/test-assets/secrets.yaml @@ -2,6 +2,9 @@ test_key: ENC[AES256_GCM,data:2mP+IAdczoEr0g==,iv:voX4IQemcgt0O97oLExy5r2V85nn68 a_list: - ENC[AES256_GCM,data:oOQ=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:ox4rgjbb8c0vYZ2XmwRgpg==,type:str] - ENC[AES256_GCM,data:mYU=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:CFrhnZv6lYGJOVso+2YBFg==,type:str] +nested: + test: + file: ENC[AES256_GCM,data:9YhsaXoxxdbLUzeWqA==,iv:xBYQQpsC/Xq0wHXNcqmLxNs5yvG+yjBzcpdRpC+UJxs=,tag:OnonR98dn9CXh/LwlRwXIw==,type:str] sops: kms: [] gcp_kms: [] @@ -26,8 +29,8 @@ sops: WHJmNkhFVStxRG5PZTZUWnRFTmtzemsKLXKJN3GSJKDI4MYPxDU5HbTzoSAt0jK9 T9sJbd++By2OC9rl+GJoJcy4aM0uTYy83EDfqBV02Y1CfepRjHLRWQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2021-08-27T17:58:39Z" - mac: ENC[AES256_GCM,data:V9QGBTrofAza2LK1hA5cQmuT37BsfRJZZtSmvc5CDnIeWYTLUOkCFRzY+wk3uZNj0aM3BUAjvIM6LPNJ+rR8p0vXwKN37UH/oRmGADuLcu5Ec7hmArCGjKMo0gyaarlvuGmFhjb1gcW2PAbo84lDykxbTyf7Pp7APJhIvGbwBu4=,iv:ufG8sG5NAtVO25kZXrWnSQ/kkbDlwmOWhO4T9UpRvOg=,tag:YidkKTBqH48N7bjBabrAgQ==,type:str] + lastmodified: "2021-09-30T19:49:41Z" + mac: ENC[AES256_GCM,data:WQjpNaji5Jg/0m0brFXernN+n+zwroiyFRGmhXE2T13THjnxlPyTxkjTIf3oJlAwpeIsfaCCxRdBSJC1B9s3XaqCWHTY4wEOdZ5f+oNBlMxbwKyN5t+GG2CRdpoMsIQ/bBJ+v8LM5BEa3qNXQ6TwWrnhAXWWPRjRa1LsguvI+TM=,iv:fwWjzvLA2cYJNnanm/vw5yE1tXtGU2amZfhw3Ha5zbo=,tag:6FT1M6fqJfUX8E10pvEehw==,type:str] pgp: - created_at: "2020-07-12T08:03:51Z" enc: | @@ -63,4 +66,4 @@ sops: -----END PGP MESSAGE----- fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4 unencrypted_suffix: _unencrypted - version: 3.6.1 + version: 3.7.1