Do not render templates when decrypting neededForUsers secrets

This fixes https://github.com/Mic92/sops-nix/issues/659

In https://github.com/Mic92/sops-nix/pull/649, we started rendering
templates twice:

1. When rendering `neededForUsers` secrets (if there are any
   `neededForUsers` secrets).
2. When decrypting "regular" secrets.

This alone was weird and wrong, but didn't cause issues
for people until https://github.com/Mic92/sops-nix/pull/655, which
triggered https://github.com/Mic92/sops-nix/issues/659. The cause is not
super obvious:

1. When rendering `neededForUsers` secrets, we'd generate templates in
   `/run/secrets-for-users/rendered`.
2. However, the `path` for these templates is in
   `/run/secrets/rendered`, which is not inside of the
   `/run/secrets-for-users` directory we're dealing with, so we'd
   generate a symlink from `/run/secrets/rendered/<foo>` to
   `/run/secrets-for-users/rendered/<foo>`, which required making
   the parent directory of the symlink (`/run/secrets/rendered/`).
3. This breaks sops-nix's assumption that `/run/secrets` either doesn't
   exist, or is a symlink, and you get the symptoms described in
   <https://github.com/Mic92/sops-nix/issues/659>.

Reproducing this in a test was straightforward: just expand our existing
template test to also have a `neededForUsers` secret.

Fixing this was also straightforward: don't render templates during the
`neededForUsers` phase (if we want to add support for `neededForUsers`
templates in the future, that would be straightforward to do, but I
opted not do that here).
This commit is contained in:
Jeremy Fleischman 2024-11-11 00:18:56 -06:00 committed by mergify[bot]
parent 47fc1d8c72
commit eee831aadb
6 changed files with 32 additions and 21 deletions

View file

@ -71,7 +71,7 @@ type template struct {
type manifest struct {
Secrets []secret `json:"secrets"`
Templates map[string]*template `json:"templates"`
Templates []template `json:"templates"`
PlaceholderBySecretName map[string]string `json:"placeholderBySecretName"`
SecretsMountPoint string `json:"secretsMountPoint"`
SymlinkPath string `json:"symlinkPath"`
@ -185,7 +185,7 @@ func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, g
return linkTarget == targetFile && validUG
}
func symlinkSecret(targetFile string, path string, owner int, group int, userMode bool) error {
func createSymlink(targetFile string, path string, owner int, group int, userMode bool) error {
for {
stat, err := os.Lstat(path)
if os.IsNotExist(err) {
@ -217,7 +217,7 @@ func symlinkSecret(targetFile string, path string, owner int, group int, userMod
}
}
func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*template, userMode bool) error {
func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates []template, userMode bool) error {
for _, secret := range secrets {
targetFile := filepath.Join(targetDir, secret.Name)
if targetFile == secret.Path {
@ -227,7 +227,7 @@ func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*te
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err)
}
if err := symlinkSecret(targetFile, secret.Path, secret.owner, secret.group, userMode); err != nil {
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)
}
}
@ -241,7 +241,7 @@ func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*te
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
return fmt.Errorf("cannot create parent directory of '%s': %w", template.Path, err)
}
if err := symlinkSecret(targetFile, template.Path, template.owner, template.group, userMode); err != nil {
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)
}
}
@ -610,8 +610,9 @@ func (app *appContext) validateSecret(secret *secret) error {
return app.validateSopsFile(secret, &file)
}
func renderTemplates(templates map[string]*template, secretByPlaceholder map[string]*secret) {
for _, template := range templates {
func renderTemplates(templates []template, secretByPlaceholder map[string]*secret) {
for i := range templates {
template := &templates[i]
rendered := renderTemplate(&template.content, secretByPlaceholder)
template.value = []byte(rendered)
}
@ -702,7 +703,8 @@ func (app *appContext) validateManifest() error {
}
}
for _, template := range m.Templates {
for i := range m.Templates {
template := &m.Templates[i]
if err := app.validateTemplate(template); err != nil {
return err
}
@ -893,7 +895,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc)
return filepath.Walk(filename, symWalkFunc)
}
func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates map[string]*template) error {
func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error {
var restart []string
var reload []string
@ -1148,7 +1150,7 @@ func replaceRuntimeDir(path, rundir string) (ret string) {
return
}
func writeTemplates(targetDir string, templates map[string]*template, keysGID int, userMode bool) error {
func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error {
for _, template := range templates {
fp := filepath.Join(targetDir, template.Name)
@ -1302,7 +1304,7 @@ func installSecrets(args []string) error {
if isDry {
return nil
}
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.Templates, manifest.UserMode); err != 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 {

View file

@ -266,6 +266,10 @@ in {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
# Verify that things work even with `neededForUsers` secrets. See
# <https://github.com/Mic92/sops-nix/issues/659>.
secrets."nested/test/file".neededForUsers = true;
};
# must run before sops sets up keys