1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2025-03-16 21:48:15 +00:00
sops-nix/pkgs/sops-install-secrets/main.go

1329 lines
36 KiB
Go
Raw Normal View History

2020-07-06 07:30:09 +01:00
package main
import (
"bytes"
"encoding/hex"
2020-07-06 07:30:09 +01:00
"encoding/json"
"errors"
2020-07-19 11:32:59 +01:00
"flag"
2020-07-06 07:30:09 +01:00
"fmt"
"os"
"os/user"
"path"
2020-07-06 07:30:09 +01:00
"path/filepath"
"sort"
2020-07-06 07:30:09 +01:00
"strconv"
"strings"
"syscall"
2021-08-27 20:09:28 +02:00
"time"
2020-07-06 07:30:09 +01:00
2021-02-01 12:12:20 +01:00
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
2021-09-01 13:32:03 +02:00
agessh "github.com/Mic92/ssh-to-age"
2020-07-12 13:50:55 +01:00
"github.com/getsops/sops/v3/decrypt"
2023-11-03 14:31:26 +01:00
"github.com/joho/godotenv"
2020-07-06 07:30:09 +01:00
"github.com/mozilla-services/yaml"
"gopkg.in/ini.v1"
2020-07-06 07:30:09 +01:00
)
type secret struct {
Name string `json:"name"`
Key string `json:"key"`
Path string `json:"path"`
Owner *string `json:"owner,omitempty"`
UID int `json:"uid"`
Group *string `json:"group,omitempty"`
GID int `json:"gid"`
SopsFile string `json:"sopsFile"`
Format FormatType `json:"format"`
Mode string `json:"mode"`
RestartUnits []string `json:"restartUnits"`
ReloadUnits []string `json:"reloadUnits"`
value []byte
mode os.FileMode
owner int
group int
}
type loggingConfig struct {
KeyImport bool `json:"keyImport"`
SecretChanges bool `json:"secretChanges"`
2020-07-06 07:30:09 +01:00
}
2024-03-18 13:42:31 +01:00
type template struct {
Name string `json:"name"`
Content string `json:"content"`
Path string `json:"path"`
Mode string `json:"mode"`
Owner *string `json:"owner,omitempty"`
UID int `json:"uid"`
Group *string `json:"group,omitempty"`
GID int `json:"gid"`
File string `json:"file"`
RestartUnits []string `json:"restartUnits"`
ReloadUnits []string `json:"reloadUnits"`
value []byte
mode os.FileMode
content string
owner int
group int
2024-03-18 13:42:31 +01:00
}
2020-07-06 07:30:09 +01:00
type manifest struct {
2024-11-05 11:46:26 -06:00
Secrets []secret `json:"secrets"`
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).
2024-11-11 00:18:56 -06:00
Templates []template `json:"templates"`
2024-11-05 11:46:26 -06:00
PlaceholderBySecretName map[string]string `json:"placeholderBySecretName"`
SecretsMountPoint string `json:"secretsMountPoint"`
SymlinkPath string `json:"symlinkPath"`
KeepGenerations int `json:"keepGenerations"`
SSHKeyPaths []string `json:"sshKeyPaths"`
GnupgHome string `json:"gnupgHome"`
AgeKeyFile string `json:"ageKeyFile"`
AgeSSHKeyPaths []string `json:"ageSshKeyPaths"`
UseTmpfs bool `json:"useTmpfs"`
UserMode bool `json:"userMode"`
Logging loggingConfig `json:"logging"`
2020-07-06 07:30:09 +01:00
}
type secretFile struct {
2020-07-19 17:09:27 +01:00
cipherText []byte
keys map[string]interface{}
/// First secret that defined this secretFile, used for error messages
firstSecret *secret
}
type FormatType string
const (
Yaml FormatType = "yaml"
JSON FormatType = "json"
2020-07-19 17:09:27 +01:00
Binary FormatType = "binary"
Dotenv FormatType = "dotenv"
Ini FormatType = "ini"
2020-07-19 17:09:27 +01:00
)
func IsValidFormat(format string) bool {
switch format {
case string(Yaml),
string(JSON),
2023-11-03 14:31:26 +01:00
string(Binary),
string(Dotenv),
string(Ini):
return true
default:
return false
}
}
2020-07-19 17:09:27 +01:00
func (f *FormatType) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
2023-11-03 14:31:26 +01:00
t := FormatType(s)
2020-07-19 17:09:27 +01:00
switch t {
case "":
*f = Yaml
case Yaml, JSON, Binary, Dotenv, Ini:
2020-07-19 17:09:27 +01:00
*f = t
}
return nil
}
func (f FormatType) MarshalJSON() ([]byte, error) {
return json.Marshal(string(f))
}
type CheckMode string
const (
Manifest CheckMode = "manifest"
SopsFile CheckMode = "sopsfile"
Off CheckMode = "off"
)
type options struct {
2021-10-19 18:26:43 +02:00
checkMode CheckMode
manifest string
ignorePasswd bool
2020-07-19 17:09:27 +01:00
}
type appContext struct {
2024-11-05 11:46:26 -06:00
manifest manifest
secretFiles map[string]secretFile
secretByPlaceholder map[string]*secret
checkMode CheckMode
ignorePasswd bool
2020-07-19 17:09:27 +01:00
}
// Keep this in sync with `modules/sops/templates/default.nix`
const RenderedSubdir string = "rendered"
2020-07-06 07:30:09 +01:00
func readManifest(path string) (*manifest, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open manifest: %w", err)
2020-07-06 07:30:09 +01:00
}
defer file.Close()
dec := json.NewDecoder(file)
var m manifest
if err := dec.Decode(&m); err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
2020-07-06 07:30:09 +01:00
}
return &m, nil
}
func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, group int) bool {
2021-02-01 12:12:20 +01:00
validUG := true
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
validUG = validUG && int(stat.Uid) == owner
validUG = validUG && int(stat.Gid) == group
} else {
panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.")
}
return linkTarget == targetFile && validUG
}
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).
2024-11-11 00:18:56 -06:00
func createSymlink(targetFile string, path string, owner int, group int, userMode bool) error {
2020-07-12 13:50:55 +01:00
for {
stat, err := os.Lstat(path)
2020-07-12 13:50:55 +01:00
if os.IsNotExist(err) {
if err = os.Symlink(targetFile, path); err != nil {
return fmt.Errorf("cannot create symlink '%s': %w", path, err)
2020-07-12 13:50:55 +01:00
}
if !userMode {
if err = SecureSymlinkChown(path, targetFile, owner, group); err != nil {
return fmt.Errorf("cannot chown symlink '%s': %w", path, err)
}
}
2020-07-12 13:50:55 +01:00
return nil
} else if err != nil {
return fmt.Errorf("cannot stat '%s': %w", path, err)
2020-07-19 23:23:38 +01:00
}
if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
linkTarget, err := os.Readlink(path)
2020-07-19 23:23:38 +01:00
if os.IsNotExist(err) {
continue
} else if err != nil {
return fmt.Errorf("cannot read symlink '%s': %w", path, err)
} else if linksAreEqual(linkTarget, targetFile, stat, owner, group) {
2020-07-19 23:23:38 +01:00
return nil
}
2020-07-12 13:50:55 +01:00
}
if err := os.Remove(path); err != nil {
return fmt.Errorf("cannot override %s: %w", path, err)
2020-07-12 13:50:55 +01:00
}
}
}
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).
2024-11-11 00:18:56 -06:00
func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates []template, userMode bool) error {
for _, secret := range secrets {
2020-07-06 07:30:09 +01:00
targetFile := filepath.Join(targetDir, secret.Name)
if targetFile == secret.Path {
continue
}
parent := filepath.Dir(secret.Path)
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err)
2020-07-06 07:30:09 +01:00
}
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).
2024-11-11 00:18:56 -06:00
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)
2020-07-06 07:30:09 +01:00
}
}
for _, template := range templates {
targetFile := filepath.Join(targetDir, RenderedSubdir, template.Name)
if targetFile == template.Path {
continue
}
parent := filepath.Dir(template.Path)
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
return fmt.Errorf("cannot create parent directory of '%s': %w", template.Path, err)
}
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).
2024-11-11 00:18:56 -06:00
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)
}
}
2020-07-06 07:30:09 +01:00
return nil
}
type plainData struct {
data map[string]interface{}
2020-07-06 07:30:09 +01:00
binary []byte
}
2021-09-10 12:02:38 +02:00
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 += "/"
2021-09-10 12:02:38 +02:00
}
return "", fmt.Errorf("the key '%s%s' cannot be found", keyUntilNow, currentKey)
2021-09-10 12:02:38 +02:00
}
break
}
thisKey := currentKey[:slashIndex]
if keyUntilNow == "" {
keyUntilNow = thisKey
} else {
keyUntilNow += "/" + thisKey
2021-09-10 12:02:38 +02:00
}
currentKey = currentKey[(slashIndex + 1):]
val, ok = currentData[thisKey]
if !ok {
return "", fmt.Errorf("the key '%s' cannot be found", keyUntilNow)
2021-09-10 12:02:38 +02:00
}
var valWithWrongType map[interface{}]interface{}
valWithWrongType, ok = val.(map[interface{}]interface{})
2021-09-10 12:02:38 +02:00
if !ok {
return "", fmt.Errorf("key '%s' does not refer to a dictionary", keyUntilNow)
2021-09-10 12:02:38 +02:00
}
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)
2021-09-10 12:02:38 +02:00
}
return strVal, nil
}
2020-07-06 07:30:09 +01:00
func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
sourceFile := sourceFiles[s.SopsFile]
if sourceFile.data == nil || sourceFile.binary == nil {
2020-07-19 17:09:27 +01:00
plain, err := decrypt.File(s.SopsFile, string(s.Format))
2020-07-06 07:30:09 +01:00
if err != nil {
return fmt.Errorf("failed to decrypt '%s': %w", s.SopsFile, err)
2020-07-06 07:30:09 +01:00
}
switch s.Format {
case Binary, Dotenv, Ini:
2020-07-06 07:30:09 +01:00
sourceFile.binary = plain
case Yaml:
if s.Key == "" {
sourceFile.binary = plain
} else {
if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
}
2020-07-06 07:30:09 +01:00
}
case JSON:
if s.Key == "" {
sourceFile.binary = plain
} else {
if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
}
}
default:
return fmt.Errorf("secret of type %s in %s is not supported", s.Format, s.SopsFile)
2020-07-06 07:30:09 +01:00
}
}
switch s.Format {
case Binary, Dotenv, Ini:
2020-07-06 07:30:09 +01:00
s.value = sourceFile.binary
case Yaml, JSON:
if s.Key == "" {
s.value = sourceFile.binary
} else {
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)
2021-02-01 12:12:20 +01:00
}
2020-07-06 07:30:09 +01:00
}
sourceFiles[s.SopsFile] = sourceFile
return nil
}
func decryptSecrets(secrets []secret) error {
sourceFiles := make(map[string]plainData)
for i := range secrets {
if err := decryptSecret(&secrets[i], sourceFiles); err != nil {
return err
}
}
return nil
}
2023-11-03 14:31:26 +01:00
const (
RamfsMagic int32 = -2054924042
TmpfsMagic int32 = 16914836
2023-11-03 14:31:26 +01:00
)
2021-01-27 08:36:33 +01:00
func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, userMode bool) (*string, error) {
2020-07-06 07:30:09 +01:00
var generation uint64
linkTarget, err := os.Readlink(linkName)
if err == nil {
if strings.HasPrefix(linkTarget, secretMountpoint) {
targetBasename := filepath.Base(linkTarget)
generation, err = strconv.ParseUint(targetBasename, 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse %s of %s as a number: %w", targetBasename, linkTarget, err)
2020-07-06 07:30:09 +01:00
}
}
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("cannot access %s: %w", linkName, err)
2020-07-06 07:30:09 +01:00
}
generation++
dir := filepath.Join(secretMountpoint, strconv.Itoa(int(generation)))
if _, err := os.Stat(dir); !os.IsNotExist(err) {
if err := os.RemoveAll(dir); err != nil {
return nil, fmt.Errorf("cannot remove existing %s: %w", dir, err)
2020-07-06 07:30:09 +01:00
}
}
2023-11-03 14:31:26 +01:00
if err := os.Mkdir(dir, os.FileMode(0o751)); err != nil {
return nil, fmt.Errorf("mkdir(): %w", err)
2020-07-06 07:30:09 +01:00
}
if !userMode {
if err := os.Chown(dir, 0, int(keysGID)); err != nil {
return nil, fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", dir, keysGID, err)
}
2020-07-06 07:30:09 +01:00
}
return &dir, nil
}
2024-11-05 11:46:26 -06:00
func createParentDirs(parent string, target string, keysGID int, userMode bool) error {
2024-03-18 13:42:31 +01:00
dirs := strings.Split(filepath.Dir(target), "/")
pathSoFar := parent
for _, dir := range dirs {
pathSoFar = filepath.Join(pathSoFar, dir)
if err := os.MkdirAll(pathSoFar, 0o751); err != nil {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
2024-03-18 13:42:31 +01:00
}
if !userMode {
2024-11-05 11:46:26 -06:00
if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil {
return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err)
2024-03-18 13:42:31 +01:00
}
}
}
return nil
}
2024-11-05 11:46:26 -06:00
func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error {
2020-07-06 07:30:09 +01:00
for _, secret := range secrets {
2021-09-10 12:02:38 +02:00
fp := filepath.Join(secretDir, secret.Name)
2024-11-05 11:46:26 -06:00
if err := createParentDirs(secretDir, secret.Name, keysGID, userMode); err != nil {
2024-03-18 13:42:31 +01:00
return err
2020-07-06 07:30:09 +01:00
}
2023-02-28 09:44:31 +01:00
if err := os.WriteFile(fp, []byte(secret.value), secret.mode); err != nil {
return fmt.Errorf("cannot write %s: %w", fp, err)
2021-09-10 12:02:38 +02:00
}
if !userMode {
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)
}
2020-07-06 07:30:09 +01:00
}
}
return nil
}
2022-07-10 18:10:30 +02:00
func lookupGroup(groupname string) (int, error) {
group, err := user.LookupGroup(groupname)
2020-07-06 07:30:09 +01:00
if err != nil {
return 0, fmt.Errorf("failed to lookup 'keys' group: %w", err)
2020-07-06 07:30:09 +01:00
}
gid, err := strconv.ParseInt(group.Gid, 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot parse keys gid %s: %w", group.Gid, err)
2020-07-06 07:30:09 +01:00
}
return int(gid), nil
}
2022-07-10 18:10:30 +02:00
func lookupKeysGroup() (int, error) {
2023-11-03 14:31:26 +01:00
gid, err1 := lookupGroup("keys")
if err1 == nil {
return gid, nil
}
gid, err2 := lookupGroup("nogroup")
if err2 == nil {
return gid, nil
}
return 0, fmt.Errorf("can't find group 'keys' nor 'nogroup' (%w)", err2)
2022-07-10 18:10:30 +02:00
}
2020-07-19 17:09:27 +01:00
func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
if app.checkMode == Manifest {
return &secretFile{firstSecret: s}, nil
}
2023-02-28 09:44:31 +01:00
cipherText, err := os.ReadFile(s.SopsFile)
2020-07-19 17:09:27 +01:00
if err != nil {
return nil, fmt.Errorf("failed reading %s: %w", s.SopsFile, err)
2020-07-19 17:09:27 +01:00
}
var keys map[string]interface{}
switch s.Format {
case Binary:
2020-07-19 17:09:27 +01:00
if err := json.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err)
2020-07-19 17:09:27 +01:00
}
return &secretFile{cipherText: cipherText, firstSecret: s}, nil
case Yaml:
2020-07-19 17:09:27 +01:00
if err := yaml.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err)
2020-07-19 17:09:27 +01:00
}
case Dotenv:
env, err := godotenv.Unmarshal(string(cipherText))
if err != nil {
return nil, fmt.Errorf("cannot parse dotenv of '%s': %w", s.SopsFile, err)
}
keys = map[string]interface{}{}
for k, v := range env {
keys[k] = v
}
case JSON:
if err := json.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err)
}
case Ini:
_, err := ini.Load(bytes.NewReader(cipherText))
if err != nil {
return nil, fmt.Errorf("cannot parse ini of '%s': %w", s.SopsFile, err)
}
2024-11-05 11:46:26 -06:00
// TODO: we do not actually check the contents of the ini here...
2020-07-19 17:09:27 +01:00
}
return &secretFile{
cipherText: cipherText,
keys: keys,
firstSecret: s,
}, nil
}
func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
if file.firstSecret.Format != s.Format {
return fmt.Errorf("secret %s defined the format of %s as %s, but it was specified as %s in %s before",
s.Name, s.SopsFile, s.Format,
file.firstSecret.Format, file.firstSecret.Name)
}
if app.checkMode != Manifest && !(s.Format == Binary || s.Format == Dotenv || s.Format == Ini) && s.Key != "" {
2021-09-10 12:02:38 +02:00
_, 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)
2020-07-19 17:09:27 +01:00
}
}
return nil
}
2024-03-18 13:42:31 +01:00
func validateMode(mode string) (os.FileMode, error) {
parsed, err := strconv.ParseUint(mode, 8, 16)
if err != nil {
2024-11-05 11:46:26 -06:00
return 0, fmt.Errorf("invalid number in mode: %s: %w", mode, err)
2024-03-18 13:42:31 +01:00
}
return os.FileMode(parsed), nil
}
func validateOwner(owner string) (int, error) {
lookedUp, err := user.Lookup(owner)
if err != nil {
2024-11-05 11:46:26 -06:00
return 0, fmt.Errorf("failed to lookup user '%s': %w", owner, err)
2024-03-18 13:42:31 +01:00
}
ownerNr, err := strconv.ParseUint(lookedUp.Uid, 10, 64)
if err != nil {
2024-11-05 11:46:26 -06:00
return 0, fmt.Errorf("cannot parse uid %s: %w", lookedUp.Uid, err)
2024-03-18 13:42:31 +01:00
}
return int(ownerNr), nil
}
func validateGroup(group string) (int, error) {
lookedUp, err := user.LookupGroup(group)
if err != nil {
2024-11-05 11:46:26 -06:00
return 0, fmt.Errorf("failed to lookup group '%s': %w", group, err)
2024-03-18 13:42:31 +01:00
}
groupNr, err := strconv.ParseUint(lookedUp.Gid, 10, 64)
if err != nil {
2024-11-05 11:46:26 -06:00
return 0, fmt.Errorf("cannot parse gid %s: %w", lookedUp.Gid, err)
2024-03-18 13:42:31 +01:00
}
return int(groupNr), nil
}
2020-07-19 17:09:27 +01:00
func (app *appContext) validateSecret(secret *secret) error {
2024-03-18 13:42:31 +01:00
mode, err := validateMode(secret.Mode)
2020-07-06 07:30:09 +01:00
if err != nil {
2024-03-18 13:42:31 +01:00
return err
2020-07-06 07:30:09 +01:00
}
2024-03-18 13:42:31 +01:00
secret.mode = mode
2020-07-06 07:30:09 +01:00
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
2021-10-19 18:26:43 +02:00
secret.owner = 0
secret.group = 0
} else if app.checkMode == Off || app.ignorePasswd {
if secret.Owner == nil {
secret.owner = secret.UID
} else {
2024-11-05 11:46:26 -06:00
owner, err := validateOwner(*secret.Owner)
if err != nil {
2024-11-05 11:46:26 -06:00
return err
}
2024-11-05 11:46:26 -06:00
secret.owner = owner
}
if secret.Group == nil {
secret.group = secret.GID
} else {
2024-11-05 11:46:26 -06:00
group, err := validateGroup(*secret.Group)
if err != nil {
2024-11-05 11:46:26 -06:00
return err
}
2024-11-05 11:46:26 -06:00
secret.group = group
}
2020-07-06 07:30:09 +01:00
}
if secret.Format == "" {
secret.Format = "yaml"
}
if !IsValidFormat(string(secret.Format)) {
return fmt.Errorf("unsupported format %s for secret %s", secret.Format, secret.Name)
2020-07-06 07:30:09 +01:00
}
2020-07-19 17:09:27 +01:00
file, ok := app.secretFiles[secret.SopsFile]
if !ok {
maybeFile, err := app.loadSopsFile(secret)
if err != nil {
return err
}
app.secretFiles[secret.SopsFile] = *maybeFile
2024-11-05 11:46:26 -06:00
2020-07-19 17:09:27 +01:00
file = *maybeFile
}
return app.validateSopsFile(secret, &file)
2020-07-06 07:30:09 +01:00
}
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).
2024-11-11 00:18:56 -06:00
func renderTemplates(templates []template, secretByPlaceholder map[string]*secret) {
for i := range templates {
template := &templates[i]
2024-11-05 11:46:26 -06:00
rendered := renderTemplate(&template.content, secretByPlaceholder)
template.value = []byte(rendered)
}
}
2024-03-18 13:42:31 +01:00
2024-11-05 11:46:26 -06:00
func renderTemplate(content *string, secretByPlaceholder map[string]*secret) string {
rendered := *content
for placeholder, secret := range secretByPlaceholder {
rendered = strings.ReplaceAll(rendered, placeholder, string(secret.value))
2024-03-18 13:42:31 +01:00
}
2024-11-05 11:46:26 -06:00
return rendered
2024-03-18 13:42:31 +01:00
}
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 {
2024-11-05 11:46:26 -06:00
if template.Owner == nil {
template.owner = template.UID
} else {
owner, err := validateOwner(*template.Owner)
if err != nil {
return err
}
template.owner = owner
2024-03-18 13:42:31 +01:00
}
2024-11-05 11:46:26 -06:00
if template.Group == nil {
template.group = template.GID
} else {
group, err := validateGroup(*template.Group)
if err != nil {
return err
}
template.group = group
2024-03-18 13:42:31 +01:00
}
}
var templateText string
if template.Content != "" {
templateText = template.Content
} else if template.File != "" {
templateBytes, err := os.ReadFile(template.File)
if err != nil {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("cannot read %s: %w", template.File, err)
2024-03-18 13:42:31 +01:00
}
templateText = string(templateBytes)
} else {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("neither content nor file was specified for template %s", template.Name)
2024-03-18 13:42:31 +01:00
}
2024-11-05 11:46:26 -06:00
template.content = templateText
2024-03-18 13:42:31 +01:00
return nil
}
2020-07-19 17:09:27 +01:00
func (app *appContext) validateManifest() error {
m := &app.manifest
2021-08-27 00:49:58 +02:00
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")
}
2020-07-12 13:50:55 +01:00
}
2021-08-27 00:49:58 +02:00
2024-11-05 11:46:26 -06:00
for i := range m.Secrets {
secret := &m.Secrets[i]
if err := app.validateSecret(secret); err != nil {
2024-03-18 13:42:31 +01:00
return err
}
2024-11-05 11:46:26 -06:00
// The Nix module only defines placeholders for secrets if there are
// templates.
if len(m.Templates) > 0 {
placeholder := m.PlaceholderBySecretName[secret.Name]
app.secretByPlaceholder[placeholder] = secret
}
2024-03-18 13:42:31 +01:00
}
2024-11-05 11:46:26 -06:00
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).
2024-11-11 00:18:56 -06:00
for i := range m.Templates {
template := &m.Templates[i]
2024-11-05 11:46:26 -06:00
if err := app.validateTemplate(template); err != nil {
2020-07-06 07:30:09 +01:00
return err
}
}
return nil
}
func atomicSymlink(oldname, newname string) error {
2024-04-18 14:30:16 +02:00
if err := os.MkdirAll(filepath.Dir(newname), 0o755); err != nil {
return err
}
2020-07-06 07:30:09 +01:00
// Fast path: if newname does not exist yet, we can skip the whole dance
// below.
if err := os.Symlink(oldname, newname); err == nil || !os.IsExist(err) {
return err
}
// We need to use ioutil.TempDir, as we cannot overwrite a ioutil.TempFile,
// and removing+symlinking creates a TOCTOU race.
2023-02-28 09:44:31 +01:00
d, err := os.MkdirTemp(filepath.Dir(newname), "."+filepath.Base(newname))
2020-07-06 07:30:09 +01:00
if err != nil {
return err
}
cleanup := true
defer func() {
if cleanup {
os.RemoveAll(d)
}
}()
symlink := filepath.Join(d, "tmp.symlink")
if err := os.Symlink(oldname, symlink); err != nil {
return err
}
if err := os.Rename(symlink, newname); err != nil {
return err
}
cleanup = false
return os.RemoveAll(d)
}
func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int) error {
if keepGenerations == 0 {
return nil // Nothing to prune
}
// Prepare our failsafe
currentGeneration, err := strconv.Atoi(path.Base(secretsDir))
if err != nil {
return fmt.Errorf("logic error, current generation is not numeric: %w", err)
}
// Read files in the mount directory
file, err := os.Open(secretsMountPoint)
if err != nil {
return fmt.Errorf("cannot open %s: %w", secretsMountPoint, err)
}
defer file.Close()
generations, err := file.Readdirnames(0)
if err != nil {
return fmt.Errorf("cannot read %s: %w", secretsMountPoint, err)
}
for _, generationName := range generations {
generationNum, err := strconv.Atoi(generationName)
// Not a number? Not relevant
if err != nil {
continue
}
// Not strictly necessary but a good failsafe to
// make sure we don't prune the current generation
if generationNum == currentGeneration {
continue
}
if currentGeneration-keepGenerations >= generationNum {
os.RemoveAll(path.Join(secretsMountPoint, generationName))
}
}
return nil
}
func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) error {
2020-07-12 13:50:55 +01:00
secringPath := filepath.Join(gpgHome, "secring.gpg")
pubringPath := filepath.Join(gpgHome, "pubring.gpg")
2020-07-14 13:41:03 +01:00
2023-11-03 14:31:26 +01:00
secring, err := os.OpenFile(secringPath, os.O_WRONLY|os.O_CREATE, 0o600)
2020-07-12 13:50:55 +01:00
if err != nil {
return fmt.Errorf("cannot create %s: %w", secringPath, err)
2020-07-12 13:50:55 +01:00
}
defer secring.Close()
pubring, err := os.OpenFile(pubringPath, os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return fmt.Errorf("cannot create %s: %w", pubringPath, err)
}
defer pubring.Close()
for _, p := range keyPaths {
2023-02-28 09:44:31 +01:00
sshKey, err := os.ReadFile(p)
2020-07-12 13:50:55 +01:00
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err)
continue
2020-07-12 13:50:55 +01:00
}
gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
continue
2020-07-12 13:50:55 +01:00
}
2020-07-12 13:50:55 +01:00
if err := gpgKey.SerializePrivate(secring, nil); err != nil {
fmt.Fprintf(os.Stderr, "Cannot write secring: %s\n", err)
continue
2020-07-12 13:50:55 +01:00
}
if err := gpgKey.Serialize(pubring); err != nil {
fmt.Fprintf(os.Stderr, "Cannot write pubring: %s\n", err)
continue
}
if logcfg.KeyImport {
2022-07-09 00:04:54 +02:00
fmt.Printf("%s: Imported %s as GPG key with fingerprint %s\n", path.Base(os.Args[0]), p, hex.EncodeToString(gpgKey.PrimaryKey.Fingerprint[:]))
}
2020-07-12 13:50:55 +01:00
}
return nil
}
2022-07-09 00:04:54 +02:00
func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File) error {
2021-08-27 20:09:28 +02:00
for _, p := range keyPaths {
// Read the key
2023-02-28 09:44:31 +01:00
sshKey, err := os.ReadFile(p)
2021-08-27 20:09:28 +02:00
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err)
continue
2021-08-27 20:09:28 +02:00
}
2021-09-01 13:32:03 +02:00
// Convert the key to age
2024-03-14 13:03:02 +01:00
privKey, pubKey, err := agessh.SSHPrivateKeyToAge(sshKey, []byte{})
2021-08-27 20:09:28 +02:00
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot convert ssh key '%s': %s\n", p, err)
continue
2021-08-27 20:09:28 +02:00
}
// Append it to the file
2022-07-09 00:04:54 +02:00
_, err = ageFile.WriteString(*privKey + "\n")
2021-08-27 20:09:28 +02:00
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot write key to age file: %s\n", err)
continue
2021-08-27 20:09:28 +02:00
}
2022-07-09 00:04:54 +02:00
if logcfg.KeyImport {
fmt.Fprintf(os.Stderr, "%s: Imported %s as age key with fingerprint %s\n", path.Base(os.Args[0]), p, *pubKey)
continue
2022-07-09 00:04:54 +02:00
}
2021-08-27 20:09:28 +02:00
}
return nil
}
// Like filepath.Walk but symlink-aware.
// Inspired by https://github.com/facebookarchive/symwalk
func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) error {
symWalkFunc := func(path string, info os.FileInfo, err error) error {
if fname, err := filepath.Rel(filename, path); err == nil {
path = filepath.Join(linkDirname, fname)
} else {
return err
}
if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink {
finalPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
info, err := os.Lstat(finalPath)
if err != nil {
return walkFn(path, info, err)
}
if info.IsDir() {
return symlinkWalk(finalPath, path, walkFn)
}
}
return walkFn(path, info, err)
}
return filepath.Walk(filename, symWalkFunc)
}
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).
2024-11-11 00:18:56 -06:00
func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error {
var restart []string
var reload []string
newSecrets := make(map[string]bool)
modifiedSecrets := make(map[string]bool)
removedSecrets := make(map[string]bool)
newTemplates := make(map[string]bool)
modifiedTemplates := make(map[string]bool)
removedTemplates := make(map[string]bool)
// When the symlink path does not exist yet, we are being run in stage-2-init.sh
// where switch-to-configuration is not run so the services would only be restarted
// the next time switch-to-configuration is run.
if _, err := os.Stat(symlinkPath); os.IsNotExist(err) {
return nil
}
// Find modified/new secrets
for _, secret := range secrets {
oldPath := filepath.Join(symlinkPath, secret.Name)
newPath := filepath.Join(secretDir, secret.Name)
// Read the old file
2023-02-28 09:44:31 +01:00
oldData, err := os.ReadFile(oldPath)
if err != nil {
if os.IsNotExist(err) {
// File did not exist before
restart = append(restart, secret.RestartUnits...)
reload = append(reload, secret.ReloadUnits...)
newSecrets[secret.Name] = true
continue
}
return err
}
// Read the new file
2023-02-28 09:44:31 +01:00
newData, err := os.ReadFile(newPath)
if err != nil {
return err
}
if !bytes.Equal(oldData, newData) {
restart = append(restart, secret.RestartUnits...)
reload = append(reload, secret.ReloadUnits...)
modifiedSecrets[secret.Name] = true
}
}
// Find modified/new templates
for _, template := range templates {
oldPath := filepath.Join(symlinkPath, RenderedSubdir, template.Name)
newPath := filepath.Join(secretDir, RenderedSubdir, template.Name)
// Read the old file
oldData, err := os.ReadFile(oldPath)
if err != nil {
if os.IsNotExist(err) {
// File did not exist before
restart = append(restart, template.RestartUnits...)
reload = append(reload, template.ReloadUnits...)
newTemplates[template.Name] = true
continue
}
return err
}
// Read the new file
newData, err := os.ReadFile(newPath)
if err != nil {
return err
}
if !bytes.Equal(oldData, newData) {
restart = append(restart, template.RestartUnits...)
reload = append(reload, template.ReloadUnits...)
modifiedTemplates[template.Name] = true
}
}
writeLines := func(list []string, file string) error {
if len(list) != 0 {
2023-11-03 14:31:26 +01:00
f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer f.Close()
for _, unit := range list {
if _, err = f.WriteString(unit + "\n"); err != nil {
return err
}
}
}
return nil
}
var dryPrefix string
if isDry {
dryPrefix = "/run/nixos/dry-activation"
} else {
dryPrefix = "/run/nixos/activation"
}
if err := writeLines(restart, dryPrefix+"-restart-list"); err != nil {
return err
}
if err := writeLines(reload, dryPrefix+"-reload-list"); err != nil {
return err
}
// Do not output changes if not requested
if !logcfg.SecretChanges {
return nil
}
// Find removed secrets/templates.
symlinkRenderedPath := filepath.Join(symlinkPath, RenderedSubdir)
err := symlinkWalk(symlinkPath, symlinkPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// If the path we're looking at isn't in `symlinkRenderedPath`, then
// it's a secret.
rel, err := filepath.Rel(symlinkRenderedPath, path)
if err != nil {
return err
}
isSecret := strings.HasPrefix(rel, "..")
if isSecret {
path = strings.TrimPrefix(path, symlinkPath+string(os.PathSeparator))
for _, secret := range secrets {
if secret.Name == path {
return nil
}
}
removedSecrets[path] = true
} else {
path = strings.TrimPrefix(path, symlinkRenderedPath+string(os.PathSeparator))
for _, template := range templates {
if template.Name == path {
return nil
}
}
removedTemplates[path] = true
}
return nil
})
if err != nil {
return err
}
// Output new/modified/removed secrets/templates
outputChanged := func(noun string, changed map[string]bool, regularPrefix, dryPrefix string) {
if len(changed) > 0 {
s := ""
if len(changed) != 1 {
s = "s"
}
if isDry {
fmt.Printf("%s %s%s: ", dryPrefix, noun, s)
} else {
fmt.Printf("%s %s%s: ", regularPrefix, noun, s)
}
// Sort the output for deterministic behavior.
keys := make([]string, 0, len(changed))
for key := range changed {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Println(strings.Join(keys, ", "))
}
}
outputChanged("secret", newSecrets, "adding", "would add")
outputChanged("secret", modifiedSecrets, "modifying", "would modify")
outputChanged("secret", removedSecrets, "removing", "would remove")
outputChanged("rendered secret", newTemplates, "adding", "would add")
outputChanged("rendered secret", modifiedTemplates, "modifying", "would modify")
outputChanged("rendered secret", removedTemplates, "removing", "would remove")
return nil
}
2020-07-12 13:50:55 +01:00
type keyring struct {
path string
}
func (k *keyring) Remove() {
os.RemoveAll(k.path)
os.Unsetenv("GNUPGHOME")
}
func setupGPGKeyring(logcfg loggingConfig, sshKeys []string, parentDir string) (*keyring, error) {
2023-02-28 09:44:31 +01:00
dir, err := os.MkdirTemp(parentDir, "gpg")
2020-07-12 13:50:55 +01:00
if err != nil {
return nil, fmt.Errorf("cannot create gpg home in '%s': %w", parentDir, err)
2020-07-12 13:50:55 +01:00
}
k := keyring{dir}
if err := importSSHKeys(logcfg, sshKeys, dir); err != nil {
2020-07-12 13:50:55 +01:00
os.RemoveAll(dir)
return nil, err
}
os.Setenv("GNUPGHOME", dir)
return &k, nil
}
2020-07-19 11:32:59 +01:00
func parseFlags(args []string) (*options, error) {
var opts options
fs := flag.NewFlagSet(args[0], flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [OPTION] manifest.json\n", args[0])
fs.PrintDefaults()
}
2020-07-19 17:09:27 +01:00
var checkMode string
fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`)
fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root or the user executing the tool in user mode`)
2020-07-19 11:32:59 +01:00
if err := fs.Parse(args[1:]); err != nil {
return nil, err
}
2020-07-19 17:09:27 +01:00
switch CheckMode(checkMode) {
case Manifest, SopsFile, Off:
opts.checkMode = CheckMode(checkMode)
default:
return nil, fmt.Errorf("invalid value provided for -check-mode flag: %s", opts.checkMode)
2020-07-19 17:09:27 +01:00
}
2020-07-19 11:32:59 +01:00
if fs.NArg() != 1 {
flag.Usage()
return nil, flag.ErrHelp
}
opts.manifest = fs.Arg(0)
return &opts, nil
}
2022-07-04 20:34:57 +02:00
func replaceRuntimeDir(path, rundir string) (ret string) {
parts := strings.Split(path, "%%")
first := true
for _, part := range parts {
if !first {
ret += "%"
}
first = false
ret += strings.ReplaceAll(part, "%r", rundir)
}
return
}
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).
2024-11-11 00:18:56 -06:00
func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error {
2024-03-18 13:42:31 +01:00
for _, template := range templates {
fp := filepath.Join(targetDir, template.Name)
2024-11-05 11:46:26 -06:00
if err := createParentDirs(targetDir, template.Name, keysGID, userMode); err != nil {
return err
}
2024-03-18 13:42:31 +01:00
if err := os.WriteFile(fp, []byte(template.value), template.mode); err != nil {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("cannot write %s: %w", fp, err)
2024-03-18 13:42:31 +01:00
}
if !userMode {
if err := os.Chown(fp, template.owner, template.group); err != nil {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, template.owner, template.group, err)
}
}
2024-03-18 13:42:31 +01:00
}
return nil
}
2020-07-06 07:30:09 +01:00
func installSecrets(args []string) error {
2020-07-19 11:32:59 +01:00
opts, err := parseFlags(args)
if err != nil {
return err
2020-07-06 07:30:09 +01:00
}
2020-07-19 11:32:59 +01:00
manifest, err := readManifest(opts.manifest)
2020-07-06 07:30:09 +01:00
if err != nil {
2020-07-12 13:50:55 +01:00
return err
2020-07-06 07:30:09 +01:00
}
if manifest.UserMode {
var rundir string
rundir, err = RuntimeDir()
2022-07-10 22:17:40 +02:00
if opts.checkMode == Off && err != nil {
return fmt.Errorf("cannot figure out runtime directory: %w", err)
}
2022-07-04 20:34:57 +02:00
manifest.SecretsMountPoint = replaceRuntimeDir(manifest.SecretsMountPoint, rundir)
manifest.SymlinkPath = replaceRuntimeDir(manifest.SymlinkPath, rundir)
var newSecrets []secret
for _, secret := range manifest.Secrets {
2022-07-04 20:34:57 +02:00
secret.Path = replaceRuntimeDir(secret.Path, rundir)
newSecrets = append(newSecrets, secret)
}
manifest.Secrets = newSecrets
}
2020-07-19 17:09:27 +01:00
app := appContext{
2024-11-05 11:46:26 -06:00
manifest: *manifest,
checkMode: opts.checkMode,
ignorePasswd: opts.ignorePasswd,
secretFiles: make(map[string]secretFile),
secretByPlaceholder: make(map[string]*secret),
2020-07-19 17:09:27 +01:00
}
if err = app.validateManifest(); err != nil {
return fmt.Errorf("manifest is not valid: %w", err)
2020-07-06 07:30:09 +01:00
}
2020-07-19 17:09:27 +01:00
if app.checkMode != Off {
2020-07-19 11:32:59 +01:00
return nil
}
var keysGID int
2021-10-19 18:26:43 +02:00
if opts.ignorePasswd {
keysGID = 0
2021-10-19 18:26:43 +02:00
} else {
keysGID, err = lookupKeysGroup()
2021-10-19 18:26:43 +02:00
if err != nil {
return err
}
2020-07-06 07:30:09 +01:00
}
isDry := os.Getenv("NIXOS_ACTION") == "dry-activate"
if err = MountSecretFs(manifest.SecretsMountPoint, keysGID, manifest.UseTmpfs, manifest.UserMode); err != nil {
return fmt.Errorf("failed to mount filesystem for secrets: %w", err)
2020-07-06 07:30:09 +01:00
}
2020-07-12 13:50:55 +01:00
if len(manifest.SSHKeyPaths) != 0 {
var keyring *keyring
keyring, err = setupGPGKeyring(manifest.Logging, manifest.SSHKeyPaths, manifest.SecretsMountPoint)
2020-07-12 13:50:55 +01:00
if err != nil {
return fmt.Errorf("error setting up gpg keyring: %w", err)
2020-07-12 13:50:55 +01:00
}
defer keyring.Remove()
} else if manifest.GnupgHome != "" {
os.Setenv("GNUPGHOME", manifest.GnupgHome)
2021-09-28 14:07:26 +02:00
}
// Import age keys
if len(manifest.AgeSSHKeyPaths) != 0 || manifest.AgeKeyFile != "" {
keyfile := filepath.Join(manifest.SecretsMountPoint, "age-keys.txt")
os.Setenv("SOPS_AGE_KEY_FILE", keyfile)
// Create the keyfile
var ageFile *os.File
ageFile, err = os.OpenFile(keyfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("cannot create '%s': %w", keyfile, err)
}
defer ageFile.Close()
fmt.Fprintf(ageFile, "# generated by sops-nix at %s\n", time.Now().Format(time.RFC3339))
// Import SSH keys
if len(manifest.AgeSSHKeyPaths) != 0 {
err = importAgeSSHKeys(manifest.Logging, manifest.AgeSSHKeyPaths, *ageFile)
if err != nil {
return err
}
}
// Import the keyfile
if manifest.AgeKeyFile != "" {
// Read the keyfile
var contents []byte
contents, err = os.ReadFile(manifest.AgeKeyFile)
if err != nil {
return fmt.Errorf("cannot read keyfile '%s': %w", manifest.AgeKeyFile, err)
}
// Append it to the file
_, err = ageFile.WriteString(string(contents) + "\n")
if err != nil {
return fmt.Errorf("cannot write key to age file: %w", err)
}
2021-08-27 20:09:28 +02:00
}
2020-07-06 07:30:09 +01:00
}
2020-07-12 13:50:55 +01:00
2024-11-05 11:46:26 -06:00
if err := decryptSecrets(manifest.Secrets); err != nil {
2020-07-12 13:50:55 +01:00
return err
2020-07-06 07:30:09 +01:00
}
2020-07-12 13:50:55 +01:00
2024-11-05 11:46:26 -06:00
// Now that the secrets are decrypted, we can render the templates.
renderTemplates(manifest.Templates, app.secretByPlaceholder)
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGID, manifest.UserMode)
2020-07-06 07:30:09 +01:00
if err != nil {
return fmt.Errorf("failed to prepare new secrets directory: %w", err)
2020-07-06 07:30:09 +01:00
}
if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil {
return fmt.Errorf("cannot write secrets: %w", err)
2020-07-06 07:30:09 +01:00
}
2024-03-18 13:42:31 +01:00
if err := writeTemplates(path.Join(*secretDir, RenderedSubdir), manifest.Templates, keysGID, manifest.UserMode); err != nil {
2024-11-05 11:46:26 -06:00
return fmt.Errorf("cannot render templates: %w", err)
2024-03-18 13:42:31 +01:00
}
if !manifest.UserMode {
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets, manifest.Templates); err != nil {
return fmt.Errorf("cannot request units to restart: %w", err)
}
}
// No need to perform the actual symlinking
if isDry {
return nil
}
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).
2024-11-11 00:18:56 -06:00
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)
2020-07-12 13:50:55 +01:00
}
2020-07-06 07:30:09 +01:00
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
return fmt.Errorf("cannot update secrets symlink: %w", err)
2020-07-06 07:30:09 +01:00
}
if err := pruneGenerations(manifest.SecretsMountPoint, *secretDir, manifest.KeepGenerations); err != nil {
return fmt.Errorf("cannot prune old secrets generations: %w", err)
}
2020-07-06 07:30:09 +01:00
return nil
}
func main() {
if err := installSecrets(os.Args); err != nil {
if errors.Is(err, flag.ErrHelp) {
2020-07-19 11:32:59 +01:00
return
}
2020-07-06 07:30:09 +01:00
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
os.Exit(1)
}
}