mirror of
https://github.com/Mic92/sops-nix.git
synced 2024-12-14 11:57:52 +00:00
template refactoring
This commit is contained in:
parent
59d6988329
commit
bb7d636211
3 changed files with 180 additions and 22 deletions
|
@ -15,6 +15,7 @@ writeTextFile {
|
||||||
ageKeyFile = cfg.age.keyFile;
|
ageKeyFile = cfg.age.keyFile;
|
||||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||||
useTmpfs = cfg.useTmpfs;
|
useTmpfs = cfg.useTmpfs;
|
||||||
|
templates = cfg.templates;
|
||||||
userMode = false;
|
userMode = false;
|
||||||
logging = {
|
logging = {
|
||||||
keyImport = builtins.elem "keyImport" cfg.log;
|
keyImport = builtins.elem "keyImport" cfg.log;
|
||||||
|
|
|
@ -37,13 +37,13 @@ in {
|
||||||
type = singleLineStr;
|
type = singleLineStr;
|
||||||
default = config._module.args.name;
|
default = config._module.args.name;
|
||||||
description = ''
|
description = ''
|
||||||
Name of the file used in /run/secrets-rendered
|
Name of the file used in /run/secrets/rendered
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
path = mkOption {
|
path = mkOption {
|
||||||
description = "Path where the rendered file will be placed";
|
description = "Path where the rendered file will be placed";
|
||||||
type = singleLineStr;
|
type = singleLineStr;
|
||||||
default = "/run/secrets-rendered/${config.name}";
|
default = "/run/secrets/rendered/${config.name}";
|
||||||
};
|
};
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
type = lines;
|
type = lines;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"os/user"
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -49,8 +50,23 @@ type loggingConfig struct {
|
||||||
SecretChanges bool `json:"secretChanges"`
|
SecretChanges bool `json:"secretChanges"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type template struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Group string `json:"group"`
|
||||||
|
File string `json:"file"`
|
||||||
|
value []byte
|
||||||
|
mode os.FileMode
|
||||||
|
owner int
|
||||||
|
group int
|
||||||
|
}
|
||||||
|
|
||||||
type manifest struct {
|
type manifest struct {
|
||||||
Secrets []secret `json:"secrets"`
|
Secrets []secret `json:"secrets"`
|
||||||
|
Templates []template `json:"templates"`
|
||||||
SecretsMountPoint string `json:"secretsMountPoint"`
|
SecretsMountPoint string `json:"secretsMountPoint"`
|
||||||
SymlinkPath string `json:"symlinkPath"`
|
SymlinkPath string `json:"symlinkPath"`
|
||||||
KeepGenerations int `json:"keepGenerations"`
|
KeepGenerations int `json:"keepGenerations"`
|
||||||
|
@ -130,6 +146,7 @@ type options struct {
|
||||||
type appContext struct {
|
type appContext struct {
|
||||||
manifest manifest
|
manifest manifest
|
||||||
secretFiles map[string]secretFile
|
secretFiles map[string]secretFile
|
||||||
|
placeholder map[string]secret
|
||||||
checkMode CheckMode
|
checkMode CheckMode
|
||||||
ignorePasswd bool
|
ignorePasswd bool
|
||||||
}
|
}
|
||||||
|
@ -346,24 +363,30 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us
|
||||||
return &dir, nil
|
return &dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error {
|
func createParentDirs(parent string, target string, keysGid int, userMode bool) error {
|
||||||
for _, secret := range secrets {
|
dirs := strings.Split(filepath.Dir(target), "/")
|
||||||
fp := filepath.Join(secretDir, secret.Name)
|
pathSoFar := parent
|
||||||
|
|
||||||
dirs := strings.Split(filepath.Dir(secret.Name), "/")
|
|
||||||
pathSoFar := secretDir
|
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
pathSoFar = filepath.Join(pathSoFar, dir)
|
pathSoFar = filepath.Join(pathSoFar, dir)
|
||||||
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
|
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
|
||||||
return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, fp, err)
|
return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
|
||||||
}
|
}
|
||||||
if !userMode {
|
if !userMode {
|
||||||
if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil {
|
if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil {
|
||||||
return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, fp, err)
|
return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool) error {
|
||||||
|
for _, secret := range secrets {
|
||||||
|
fp := filepath.Join(secretDir, secret.Name)
|
||||||
|
|
||||||
|
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, []byte(secret.value), secret.mode); err != nil {
|
||||||
return fmt.Errorf("cannot write %s: %w", fp, err)
|
return fmt.Errorf("cannot write %s: %w", fp, err)
|
||||||
}
|
}
|
||||||
|
@ -465,18 +488,55 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) validateSecret(secret *secret) error {
|
func validateMode(mode string) (os.FileMode, error) {
|
||||||
mode, err := strconv.ParseUint(secret.Mode, 8, 16)
|
parsed, err := strconv.ParseUint(mode, 8, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid number in mode: %d: %w", mode, err)
|
return 0, fmt.Errorf("Invalid number in mode: %d: %w", mode, err)
|
||||||
}
|
}
|
||||||
secret.mode = os.FileMode(mode)
|
return os.FileMode(parsed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOwner(owner string) (int, 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)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Cannot parse uid %s: %w", lookedUp.Uid, err)
|
||||||
|
}
|
||||||
|
return int(ownerNr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGroup(group string) (int, 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)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Cannot parse gid %s: %w", lookedUp.Gid, err)
|
||||||
|
}
|
||||||
|
return int(groupNr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) validateSecret(secret *secret) error {
|
||||||
|
mode, err := validateMode(secret.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
secret.mode = mode
|
||||||
|
|
||||||
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
||||||
secret.owner = 0
|
secret.owner = 0
|
||||||
secret.group = 0
|
secret.group = 0
|
||||||
} else if app.checkMode == Off || app.ignorePasswd {
|
} else if app.checkMode == Off || app.ignorePasswd {
|
||||||
// we only access to the user/group during deployment
|
// we only access to the user/group during deployment
|
||||||
|
owner, err := validateOwner(*secret.Owner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
secret.owner = owner
|
||||||
|
|
||||||
if secret.Owner == nil {
|
if secret.Owner == nil {
|
||||||
secret.owner = secret.UID
|
secret.owner = secret.UID
|
||||||
|
@ -528,6 +588,73 @@ func (app *appContext) validateSecret(secret *secret) error {
|
||||||
return app.validateSopsFile(secret, &file)
|
return app.validateSopsFile(secret, &file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var PLACEHOLDER = regexp.MustCompile(`<SOPS:([a-f0-9]{64}):PLACEHOLDER>`)
|
||||||
|
|
||||||
|
func renderTemplate(content *string, secrets []secret) ([]byte, error) {
|
||||||
|
secretMap := make(map[string][]byte)
|
||||||
|
var err error = nil
|
||||||
|
replaced := PLACEHOLDER.ReplaceAllStringFunc(*content, func(match string) string {
|
||||||
|
secretName := PLACEHOLDER.FindStringSubmatch(match)[1]
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if secret.Name == secretName {
|
||||||
|
secretMap[secretName] = secret.value
|
||||||
|
return string(secret.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) validateTemplate(template *template) error {
|
||||||
|
mode, err := validateMode(template.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
template.mode = mode
|
||||||
|
|
||||||
|
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
||||||
|
template.owner = 0
|
||||||
|
template.group = 0
|
||||||
|
} else if app.checkMode == Off || app.ignorePasswd {
|
||||||
|
// we only access to the user/group during deployment
|
||||||
|
owner, err := validateOwner(template.Owner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
template.owner = owner
|
||||||
|
|
||||||
|
group, err := validateGroup(template.Group)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
template.group = group
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateText string
|
||||||
|
if template.Content != "" {
|
||||||
|
templateText = template.Content
|
||||||
|
} else if 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 {
|
||||||
|
return fmt.Errorf("Neither content nor file was specified for template %s", template.Name)
|
||||||
|
}
|
||||||
|
rendered, err := renderTemplate(&templateText, app.manifest.Secrets)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to render template %s: %w", template.Name, err)
|
||||||
|
}
|
||||||
|
template.value = rendered
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (app *appContext) validateManifest() error {
|
func (app *appContext) validateManifest() error {
|
||||||
m := &app.manifest
|
m := &app.manifest
|
||||||
if m.GnupgHome != "" {
|
if m.GnupgHome != "" {
|
||||||
|
@ -541,8 +668,15 @@ func (app *appContext) validateManifest() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range m.Secrets {
|
for _, secret := range m.Secrets {
|
||||||
if err := app.validateSecret(&m.Secrets[i]); err != nil {
|
if err := app.validateSecret(&secret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m.Templates) > 0 {
|
||||||
|
}
|
||||||
|
for _, template := range m.Templates {
|
||||||
|
if err := app.validateTemplate(&template); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -926,6 +1060,24 @@ func replaceRuntimeDir(path, rundir string) (ret string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeTemplates(targetDir string, templates []template, keysGid int, userMode bool) error {
|
||||||
|
for _, template := range templates {
|
||||||
|
fp := filepath.Join(targetDir, template.Name)
|
||||||
|
|
||||||
|
createParentDirs(targetDir, template.Name, keysGid, userMode)
|
||||||
|
|
||||||
|
if err := os.WriteFile(fp, []byte(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 {
|
||||||
|
return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func installSecrets(args []string) error {
|
func installSecrets(args []string) error {
|
||||||
opts, err := parseFlags(args)
|
opts, err := parseFlags(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1042,6 +1194,11 @@ func installSecrets(args []string) error {
|
||||||
if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil {
|
if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil {
|
||||||
return fmt.Errorf("cannot write secrets: %w", err)
|
return fmt.Errorf("cannot write secrets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := writeTemplates(path.Join(*secretDir, "rendered"), manifest.Templates, keysGid, manifest.UserMode); err != nil {
|
||||||
|
return fmt.Errorf("Cannot render templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !manifest.UserMode {
|
if !manifest.UserMode {
|
||||||
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil {
|
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil {
|
||||||
return fmt.Errorf("cannot request units to restart: %w", err)
|
return fmt.Errorf("cannot request units to restart: %w", err)
|
||||||
|
|
Loading…
Reference in a new issue