sops-nix icon indicating copy to clipboard operation
sops-nix copied to clipboard

Support mounting yaml and json files as is?

Open ljani opened this issue 2 years ago • 8 comments

My use case: I'd like to pass a sops encrypted .yaml configuration file to my app after decryption as is, instead of picking a single key from the file. So, sops-nix is a little too clever for my use case and it should focus only on decrypting the file, not parsing it.

In other words, I use this file ./secrets/my-config.yaml in my flake:

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: []
...

It should be mounted as /run/secrets/my-config.yaml:

my-secret1: hello
my-secret2: hello

Then I could pass the /run/secrets/my-config.yaml to my app to read any secrets from.

I could do this using a format=binary file, but then I have to remember to use format=binary everywhere, including when invoking sops. Alternatively, I could use a different file extension, but then I'd lose syntax highlight. Yet another alternative would be to use a literal multi line yaml string, but then again I'd lose highlight.

I wonder if this is already supported, because the README.md mentions home-assistant-secrets.yaml, which is similar to what I'm looking for? But I think that was defined as something like this:

home-assistant-secrets.yaml: |
    my-secret1: hello
    my-secret2: hello

(Notice how the renderer fails to highlight the yaml keys my-secret1 and my-secret2).

Does this make any sense to you?

ljani avatar Sep 29 '21 10:09 ljani

Currently I don't see any other way but using either the multi-line variant or encrypting the yaml file as a binary, in which case you would still not be able to use sops as an editor of the file. Personally I use multline yaml values to encode yaml secrets. Notice that this a limitation in sops itself, where sops-nix has little control over.

Mic92 avatar Sep 29 '21 16:09 Mic92

Notice that this a limitation in sops itself, where sops-nix has little control over.

Does it? To me it seems it's sops-nix doing the unmarshalling.

A hack would be to write decryptSecret to something like this (search for SkipUnmarshal), but I have not tested this:

func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
	sourceFile := sourceFiles[s.SopsFile]
	if sourceFile.data == nil || sourceFile.binary == nil {
		plain, err := decrypt.File(s.SopsFile, string(s.Format))
		if err != nil {
			return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFile, err)
		}
		if s.Format == Binary || s.SkipUnmarshal {
			sourceFile.binary = plain
		} else {
			if s.Format == Yaml {
				if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
					return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
				}
			} else {
				if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
					return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
				}
			}
		}
	}
	if s.Format == Binary || s.SkipUnmarshal {
		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)
		}
		s.value = []byte(strVal)
	}
	sourceFiles[s.SopsFile] = sourceFile
	return nil
}

ljani avatar Sep 29 '21 18:09 ljani

Yet another way would be to reserialize nested keys back to yaml/json and set that string into s.value, but I'm not sure if I like that

ljani avatar Sep 29 '21 20:09 ljani

I think I would prefer format=binary over adding another option which does the same thing. At the end of the day you would need an option to treat it differently.

Mic92 avatar Sep 30 '21 07:09 Mic92

Yeah, I understand and thus I'm trying to come up with a good idea. What about supporting setting s.Key to nil? In other words, if the secret key is not set, use the whole file:

func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
	sourceFile := sourceFiles[s.SopsFile]
	if sourceFile.data == nil || sourceFile.binary == nil {
		plain, err := decrypt.File(s.SopsFile, string(s.Format))
		if err != nil {
			return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFile, err)
		}
		if s.Format == Binary || s.Key == nil {
			sourceFile.binary = plain
		} else {
			if s.Format == Yaml {
				if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
					return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
				}
			} else {
				if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
					return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
				}
			}
		}
	}
	if s.Format == Binary || s.Key == nil {
		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)
		}
		s.value = []byte(strVal)
	}
	sourceFiles[s.SopsFile] = sourceFile
	return nil
}

EDIT: Err, the current way of defining secrets does not really support that. To do this, secrets should be indexed by their file paths, not their keys. EDIT2: I think I probably should investigate systemd's LoadCredential= and LoadCredentialEncrypted= a bit more as their functionality is more closer how I imagine secrets should be handled. As far as I understand, those could be plugged to use sops as well.

ljani avatar Sep 30 '21 09:09 ljani

I think I would prefer format=binary over adding another option which does the same thing. At the end of the day you would need an option to treat it differently.

I ran into wanting the same request personally. I do agree that functionally, you get some of the same things with a binary encoding. But the biggest thing you don't get with a binary encoding is one of SOPS's selling points, which is a nice textual diffs in source control. So I don't think it's completely fair to say it's the same thing, though it's close.

So that said, is this feature something we think we can get upstreamed? That would inform the effort and approach I take personally in trying to get it to work.

shajra avatar May 06 '23 12:05 shajra

Hi, I came across with similar use cases. Any updates on this feature request?

piyoki avatar Feb 08 '24 09:02 piyoki

Would love to have this too. It's really useful for passing a complex configuration file as-is to a program, while still retaining the benefits of SOPS (diff-able plain text).

musjj avatar Feb 19 '24 19:02 musjj