Merge branch 'master' of github.com:Mic92/sops-nix into nested-secrets

Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
This commit is contained in:
iosmanthus 2024-06-06 15:29:59 +08:00
commit 57783f3084
No known key found for this signature in database
GPG key ID: DEE5BAABFE092169
26 changed files with 885 additions and 708 deletions

View file

@ -29,7 +29,7 @@ func TestShellHook(t *testing.T) {
}
tempdir, err := os.MkdirTemp("", "testdir")
ok(t, err)
cmd := exec.Command("cp", "-vra", assets+"/.", tempdir)
cmd := exec.Command("cp", "-vra", assets+"/.", tempdir) // nolint:gosec
fmt.Printf("$ %s\n", strings.Join(cmd.Args, " "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -37,7 +37,7 @@ func TestShellHook(t *testing.T) {
defer os.RemoveAll(tempdir)
cmd = exec.Command("nix-shell", path.Join(assets, "shell.nix"), "--run", "gpg --list-keys")
cmd = exec.Command("nix-shell", path.Join(assets, "shell.nix"), "--run", "gpg --list-keys") // nolint:gosec
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf

View file

@ -19,7 +19,7 @@ func RuntimeDir() (string, error) {
out, err := exec.Command("getconf", "DARWIN_USER_TEMP_DIR").Output()
rundir := strings.TrimRight(string(out[:]), " \t\n")
if err != nil {
return "", fmt.Errorf("Cannot get DARWIN_USER_TEMP_DIR: %v", err)
return "", fmt.Errorf("cannot get DARWIN_USER_TEMP_DIR: %v", err)
}
return strings.TrimSuffix(rundir, "/"), nil
}
@ -28,7 +28,7 @@ func SecureSymlinkChown(symlinkToCheck string, expectedTarget string, owner, gro
// not sure what O_PATH is needed for anyways
fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_SYMLINK|unix.O_NOFOLLOW, 0)
if err != nil {
return fmt.Errorf("Failed to open %s: %w", symlinkToCheck, err)
return fmt.Errorf("failed to open %s: %w", symlinkToCheck, err)
}
defer unix.Close(fd)
@ -53,9 +53,9 @@ func SecureSymlinkChown(symlinkToCheck string, expectedTarget string, owner, gro
// mydev=`hdiutil attach -nomount ram://$NUMSECTORS`
// newfs_hfs $mydev
// mount -t hfs $mydev /tmp/mymount
func MountSecretFs(mountpoint string, keysGid int, _useTmpfs bool, userMode bool) error {
func MountSecretFs(mountpoint string, keysGID int, _useTmpfs bool, userMode bool) error {
if err := os.MkdirAll(mountpoint, 0o751); err != nil {
return fmt.Errorf("Cannot create directory '%s': %w", mountpoint, err)
return fmt.Errorf("cannot create directory '%s': %w", mountpoint, err)
}
if _, err := os.Stat(mountpoint + "/sops-nix-secretfs"); !errors.Is(err, os.ErrNotExist) {
return nil // secret fs already exists
@ -77,18 +77,22 @@ func MountSecretFs(mountpoint string, keysGid int, _useTmpfs bool, userMode bool
log.Printf("hdiutil attach ret %v. out: %s", err, diskpath)
// format as hfs
out, err = exec.Command("newfs_hfs", diskpath).Output()
out, err = exec.Command("newfs_hfs", "-s", diskpath).Output()
log.Printf("newfs_hfs ret %v. out: %s", err, out)
// "posix" mount takes `struct hfs_mount_args` which we dont have bindings for at hand.
// See https://stackoverflow.com/a/49048846/4108673
// err = unix.Mount("hfs", mountpoint, unix.MNT_NOEXEC|unix.MNT_NODEV, mount_args)
// Instead we call:
out, err = exec.Command("mount", "-t", "hfs", diskpath, mountpoint).Output()
out, err = exec.Command("mount", "-t", "hfs", "-o", "nobrowse,nodev,nosuid,-m=0751", diskpath, mountpoint).Output()
log.Printf("mount ret %v. out: %s", err, out)
// There is no documented way to check for memfs mountpoint. Thus we place a file.
_, err = os.Create(mountpoint + "/sops-nix-secretfs")
path := mountpoint + "/sops-nix-secretfs"
_, err = os.Create(path)
if err != nil {
return fmt.Errorf("cannot create file '%s': %w", path, err)
}
// This would be the way to check on unix.
//buf := unix.Statfs_t{}
@ -103,8 +107,8 @@ func MountSecretFs(mountpoint string, keysGid int, _useTmpfs bool, userMode bool
//}
if !userMode {
if err := os.Chown(mountpoint, 0, int(keysGid)); err != nil {
return fmt.Errorf("Cannot change owner/group of '%s' to 0/%d: %w", mountpoint, keysGid, err)
if err := os.Chown(mountpoint, 0, int(keysGID)); err != nil {
return fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", mountpoint, keysGID, err)
}
}

View file

@ -13,7 +13,7 @@ import (
func RuntimeDir() (string, error) {
rundir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
if !ok {
return "", fmt.Errorf("$XDG_RUNTIME_DIR is not set!")
return "", fmt.Errorf("$XDG_RUNTIME_DIR is not set")
}
return rundir, nil
}
@ -22,7 +22,7 @@ func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int)
// fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_PATH|unix.O_NOFOLLOW, 0)
fd, err := unix.Open(symlinkToCheck, unix.O_CLOEXEC|unix.O_PATH|unix.O_NOFOLLOW, 0)
if err != nil {
return fmt.Errorf("Failed to open %s: %w", symlinkToCheck, err)
return fmt.Errorf("failed to open %s: %w", symlinkToCheck, err)
}
defer unix.Close(fd)
@ -50,9 +50,9 @@ func SecureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int)
return nil
}
func MountSecretFs(mountpoint string, keysGid int, useTmpfs bool, userMode bool) error {
func MountSecretFs(mountpoint string, keysGID int, useTmpfs bool, userMode bool) error {
if err := os.MkdirAll(mountpoint, 0o751); err != nil {
return fmt.Errorf("Cannot create directory '%s': %w", mountpoint, err)
return fmt.Errorf("cannot create directory '%s': %w", mountpoint, err)
}
// We can't create a ramfs as user
@ -60,25 +60,25 @@ func MountSecretFs(mountpoint string, keysGid int, useTmpfs bool, userMode bool)
return nil
}
var fstype string = "ramfs"
var fsmagic int32 = RAMFS_MAGIC
var fstype = "ramfs"
var fsmagic = RamfsMagic
if useTmpfs {
fstype = "tmpfs"
fsmagic = TMPFS_MAGIC
fsmagic = TmpfsMagic
}
buf := unix.Statfs_t{}
if err := unix.Statfs(mountpoint, &buf); err != nil {
return fmt.Errorf("Cannot get statfs for directory '%s': %w", mountpoint, err)
return fmt.Errorf("cannot get statfs for directory '%s': %w", mountpoint, err)
}
if int32(buf.Type) != fsmagic {
if err := unix.Mount("none", mountpoint, fstype, unix.MS_NODEV|unix.MS_NOSUID, "mode=0751"); err != nil {
return fmt.Errorf("Cannot mount: %s", err)
return fmt.Errorf("cannot mount: %w", err)
}
}
if err := os.Chown(mountpoint, 0, int(keysGid)); err != nil {
return fmt.Errorf("Cannot change owner/group of '%s' to 0/%d: %w", mountpoint, keysGid, err)
if err := os.Chown(mountpoint, 0, int(keysGID)); err != nil {
return fmt.Errorf("cannot change owner/group of '%s' to 0/%d: %w", mountpoint, keysGID, err)
}
return nil

View file

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
@ -18,9 +19,10 @@ import (
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
agessh "github.com/Mic92/ssh-to-age"
"github.com/getsops/sops/v3/decrypt"
"github.com/joho/godotenv"
"github.com/mozilla-services/yaml"
"go.mozilla.org/sops/v3/decrypt"
"gopkg.in/ini.v1"
)
type secret struct {
@ -53,7 +55,7 @@ type manifest struct {
SSHKeyPaths []string `json:"sshKeyPaths"`
GnupgHome string `json:"gnupgHome"`
AgeKeyFile string `json:"ageKeyFile"`
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
AgeSSHKeyPaths []string `json:"ageSshKeyPaths"`
UseTmpfs bool `json:"useTmpfs"`
UserMode bool `json:"userMode"`
Logging loggingConfig `json:"logging"`
@ -70,7 +72,7 @@ type FormatType string
const (
Yaml FormatType = "yaml"
Json FormatType = "json"
JSON FormatType = "json"
Binary FormatType = "binary"
Dotenv FormatType = "dotenv"
Ini FormatType = "ini"
@ -79,7 +81,7 @@ const (
func IsValidFormat(format string) bool {
switch format {
case string(Yaml),
string(Json),
string(JSON),
string(Binary),
string(Dotenv),
string(Ini):
@ -98,7 +100,7 @@ func (f *FormatType) UnmarshalJSON(b []byte) error {
switch t {
case "":
*f = Yaml
case Yaml, Json, Binary, Dotenv, Ini:
case Yaml, JSON, Binary, Dotenv, Ini:
*f = t
}
@ -133,13 +135,13 @@ type appContext struct {
func readManifest(path string) (*manifest, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Failed to open manifest: %w", err)
return nil, fmt.Errorf("failed to open manifest: %w", err)
}
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)
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
return &m, nil
}
@ -159,46 +161,46 @@ func symlinkSecret(targetFile string, secret *secret, userMode bool) error {
for {
stat, err := os.Lstat(secret.Path)
if os.IsNotExist(err) {
if err := os.Symlink(targetFile, secret.Path); err != nil {
return fmt.Errorf("Cannot create symlink '%s': %w", secret.Path, err)
if err = os.Symlink(targetFile, secret.Path); err != nil {
return fmt.Errorf("cannot create symlink '%s': %w", secret.Path, err)
}
if !userMode {
if err := SecureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil {
return fmt.Errorf("Cannot chown symlink '%s': %w", secret.Path, err)
if err = SecureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil {
return fmt.Errorf("cannot chown symlink '%s': %w", secret.Path, err)
}
}
return nil
} else if err != nil {
return fmt.Errorf("Cannot stat '%s': %w", secret.Path, err)
return fmt.Errorf("cannot stat '%s': %w", secret.Path, err)
}
if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
linkTarget, err := os.Readlink(secret.Path)
if os.IsNotExist(err) {
continue
} else if err != nil {
return fmt.Errorf("Cannot read symlink '%s': %w", secret.Path, err)
return fmt.Errorf("cannot read symlink '%s': %w", secret.Path, err)
} else if linksAreEqual(linkTarget, targetFile, stat, secret) {
return nil
}
}
if err := os.Remove(secret.Path); err != nil {
return fmt.Errorf("Cannot override %s: %w", secret.Path, err)
return fmt.Errorf("cannot override %s: %w", secret.Path, err)
}
}
}
func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error {
for _, secret := range secrets {
for i, secret := range secrets {
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)
return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err)
}
if err := symlinkSecret(targetFile, &secret, userMode); err != nil {
return fmt.Errorf("Failed to symlink secret '%s': %w", secret.Path, err)
if err := symlinkSecret(targetFile, &secrets[i], userMode); err != nil {
return fmt.Errorf("failed to symlink secret '%s': %w", secret.Path, err)
}
}
return nil
@ -225,7 +227,7 @@ func recurseSecretKey(format FormatType, keys map[string]interface{}, wantedKey
if keyUntilNow != "" {
keyUntilNow += "/"
}
return "", fmt.Errorf("The key '%s%s' cannot be found", keyUntilNow, currentKey)
return "", fmt.Errorf("the key '%s%s' cannot be found", keyUntilNow, currentKey)
}
break
}
@ -238,11 +240,12 @@ func recurseSecretKey(format FormatType, keys map[string]interface{}, wantedKey
currentKey = currentKey[(slashIndex + 1):]
val, ok = currentData[thisKey]
if !ok {
return "", fmt.Errorf("The key '%s' cannot be found", keyUntilNow)
return "", fmt.Errorf("the key '%s' cannot be found", keyUntilNow)
}
valWithWrongType, ok := val.(map[interface{}]interface{})
var valWithWrongType map[interface{}]interface{}
valWithWrongType, ok = val.(map[interface{}]interface{})
if !ok {
return "", fmt.Errorf("Key '%s' does not refer to a dictionary", keyUntilNow)
return "", fmt.Errorf("key '%s' does not refer to a dictionary", keyUntilNow)
}
currentData = make(map[string]interface{})
for key, value := range valWithWrongType {
@ -252,17 +255,22 @@ func recurseSecretKey(format FormatType, keys map[string]interface{}, wantedKey
var marshaller func(interface{}) ([]byte, error)
switch format {
case Json:
case JSON:
marshaller = json.Marshal
case Yaml:
marshaller = yaml.Marshal
default:
return "", fmt.Errorf("Secret of type %s is not supported", format)
return "", fmt.Errorf("secret of type %s is not supported", format)
}
// If the value is a string, do not marshal it.
if strVal, ok := val.(string); ok {
return strVal, nil
}
strVal, err := marshaller(val)
if err != nil {
return "", fmt.Errorf("Cannot marshal the value of key '%s': %w", keyUntilNow, err)
return "", fmt.Errorf("cannot marshal the value of key '%s': %w", keyUntilNow, err)
}
strVal = bytes.TrimSpace(strVal)
@ -274,7 +282,7 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
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)
return fmt.Errorf("failed to decrypt '%s': %w", s.SopsFile, err)
}
switch s.Format {
@ -282,20 +290,20 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
sourceFile.binary = plain
case Yaml:
if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
return fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err)
}
case Json:
case JSON:
if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
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)
return fmt.Errorf("secret of type %s in %s is not supported", s.Format, s.SopsFile)
}
}
switch s.Format {
case Binary, Dotenv, Ini:
s.value = sourceFile.binary
case Yaml, Json:
case Yaml, JSON:
strVal, err := recurseSecretKey(s.Format, sourceFile.data, s.Key)
if err != nil {
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
@ -317,11 +325,11 @@ func decryptSecrets(secrets []secret) error {
}
const (
RAMFS_MAGIC int32 = -2054924042
TMPFS_MAGIC int32 = 16914836
RamfsMagic int32 = -2054924042
TmpfsMagic int32 = 16914836
)
func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int, userMode bool) (*string, error) {
func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, userMode bool) (*string, error) {
var generation uint64
linkTarget, err := os.Readlink(linkName)
if err == nil {
@ -329,31 +337,31 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int, us
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)
return nil, fmt.Errorf("cannot parse %s of %s as a number: %w", targetBasename, linkTarget, err)
}
}
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("Cannot access %s: %w", linkName, err)
return nil, fmt.Errorf("cannot access %s: %w", linkName, err)
}
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)
return nil, fmt.Errorf("cannot remove existing %s: %w", dir, err)
}
}
if err := os.Mkdir(dir, os.FileMode(0o751)); err != nil {
return nil, fmt.Errorf("mkdir(): %w", err)
}
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)
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)
}
}
return &dir, nil
}
func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool) error {
func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error {
for _, secret := range secrets {
fp := filepath.Join(secretDir, secret.Name)
@ -362,21 +370,21 @@ func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool
for _, dir := range dirs {
pathSoFar = filepath.Join(pathSoFar, dir)
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, fp, err)
}
if !userMode {
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(pathSoFar, 0, int(keysGID)); err != nil {
return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, fp, err)
}
}
}
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)
}
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)
return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err)
}
}
}
@ -386,11 +394,11 @@ func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool
func lookupGroup(groupname string) (int, error) {
group, err := user.LookupGroup(groupname)
if err != nil {
return 0, fmt.Errorf("Failed to lookup 'keys' group: %w", err)
return 0, fmt.Errorf("failed to lookup 'keys' group: %w", err)
}
gid, err := strconv.ParseInt(group.Gid, 10, 64)
if err != nil {
return 0, fmt.Errorf("Cannot parse keys gid %s: %w", group.Gid, err)
return 0, fmt.Errorf("cannot parse keys gid %s: %w", group.Gid, err)
}
return int(gid), nil
}
@ -404,7 +412,7 @@ func lookupKeysGroup() (int, error) {
if err2 == nil {
return gid, nil
}
return 0, fmt.Errorf("Can't find group 'keys' nor 'nogroup' (%w).", err2)
return 0, fmt.Errorf("can't find group 'keys' nor 'nogroup' (%w)", err2)
}
func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
@ -414,7 +422,7 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
cipherText, err := os.ReadFile(s.SopsFile)
if err != nil {
return nil, fmt.Errorf("Failed reading %s: %w", s.SopsFile, err)
return nil, fmt.Errorf("failed reading %s: %w", s.SopsFile, err)
}
var keys map[string]interface{}
@ -422,26 +430,32 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
switch s.Format {
case Binary:
if err := json.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
return nil, fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err)
}
return &secretFile{cipherText: cipherText, firstSecret: s}, nil
case Yaml:
if err := yaml.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
return nil, fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err)
}
case Dotenv:
env, err := godotenv.Unmarshal(string(cipherText))
if err != nil {
return nil, fmt.Errorf("Cannot parse dotenv of '%s': %w", s.SopsFile, err)
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:
case JSON:
if err := json.Unmarshal(cipherText, &keys); err != nil {
return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
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)
}
// TODO: we do not acctually check the contents of the ini here...
}
return &secretFile{
@ -457,7 +471,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
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)) {
if app.checkMode != Manifest && (s.Format != Binary && s.Format != Dotenv && s.Format != Ini) {
_, err := recurseSecretKey(s.Format, file.keys, s.Key)
if err != nil {
return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err)
@ -469,7 +483,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error {
func (app *appContext) validateSecret(secret *secret) error {
mode, err := strconv.ParseUint(secret.Mode, 8, 16)
if err != nil {
return fmt.Errorf("Invalid number in mode: %d: %w", mode, err)
return fmt.Errorf("invalid number in mode: %d: %w", mode, err)
}
secret.mode = os.FileMode(mode)
@ -480,21 +494,21 @@ func (app *appContext) validateSecret(secret *secret) error {
// we only access to the user/group during deployment
owner, err := user.Lookup(secret.Owner)
if err != nil {
return fmt.Errorf("Failed to lookup user '%s': %w", secret.Owner, err)
return fmt.Errorf("failed to lookup user '%s': %w", secret.Owner, err)
}
ownerNr, err := strconv.ParseUint(owner.Uid, 10, 64)
if err != nil {
return fmt.Errorf("Cannot parse uid %s: %w", owner.Uid, err)
return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err)
}
secret.owner = int(ownerNr)
group, err := user.LookupGroup(secret.Group)
if err != nil {
return fmt.Errorf("Failed to lookup group '%s': %w", secret.Group, err)
return fmt.Errorf("failed to lookup group '%s': %w", secret.Group, err)
}
groupNr, err := strconv.ParseUint(group.Gid, 10, 64)
if err != nil {
return fmt.Errorf("Cannot parse gid %s: %w", group.Gid, err)
return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err)
}
secret.group = int(groupNr)
}
@ -504,7 +518,7 @@ func (app *appContext) validateSecret(secret *secret) error {
}
if !IsValidFormat(string(secret.Format)) {
return fmt.Errorf("Unsupported format %s for secret %s", secret.Format, secret.Name)
return fmt.Errorf("unsupported format %s for secret %s", secret.Format, secret.Name)
}
file, ok := app.secretFiles[secret.SopsFile]
@ -542,6 +556,10 @@ func (app *appContext) validateManifest() error {
}
func atomicSymlink(oldname, newname string) error {
if err := os.MkdirAll(filepath.Dir(newname), 0o755); err != nil {
return err
}
// 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) {
@ -582,19 +600,19 @@ func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int)
// 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)
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)
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)
return fmt.Errorf("cannot read %s: %w", secretsMountPoint, err)
}
for _, generationName := range generations {
generationNum, err := strconv.Atoi(generationName)
@ -617,23 +635,40 @@ func pruneGenerations(secretsMountPoint, secretsDir string, keepGenerations int)
func importSSHKeys(logcfg loggingConfig, keyPaths []string, gpgHome string) error {
secringPath := filepath.Join(gpgHome, "secring.gpg")
pubringPath := filepath.Join(gpgHome, "pubring.gpg")
secring, err := os.OpenFile(secringPath, os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return fmt.Errorf("Cannot create %s: %w", secringPath, err)
return fmt.Errorf("cannot create %s: %w", secringPath, err)
}
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 {
sshKey, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err)
continue
}
gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey)
if err != nil {
return err
fmt.Fprintf(os.Stderr, "%s\n", err)
continue
}
if err := gpgKey.SerializePrivate(secring, nil); err != nil {
return fmt.Errorf("Cannot write secring: %w", err)
fmt.Fprintf(os.Stderr, "Cannot write secring: %s\n", err)
continue
}
if err := gpgKey.Serialize(pubring); err != nil {
fmt.Fprintf(os.Stderr, "Cannot write pubring: %s\n", err)
continue
}
if logcfg.KeyImport {
@ -649,21 +684,25 @@ func importAgeSSHKeys(logcfg loggingConfig, keyPaths []string, ageFile os.File)
// Read the key
sshKey, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
fmt.Fprintf(os.Stderr, "Cannot read ssh key '%s': %s\n", p, err)
continue
}
// Convert the key to age
privKey, pubKey, err := agessh.SSHPrivateKeyToAge(sshKey)
privKey, pubKey, err := agessh.SSHPrivateKeyToAge(sshKey, []byte{})
if err != nil {
return fmt.Errorf("Cannot convert ssh key '%s': %w", p, err)
fmt.Fprintf(os.Stderr, "Cannot convert ssh key '%s': %s\n", p, err)
continue
}
// Append it to the file
_, err = ageFile.WriteString(*privKey + "\n")
if err != nil {
return fmt.Errorf("Cannot write key to age file: %w", err)
fmt.Fprintf(os.Stderr, "Cannot write key to age file: %s\n", err)
continue
}
if logcfg.KeyImport {
fmt.Printf("%s: Imported %s as age key with fingerprint %s\n", path.Base(os.Args[0]), p, *pubKey)
fmt.Fprintf(os.Stderr, "%s: Imported %s as age key with fingerprint %s\n", path.Base(os.Args[0]), p, *pubKey)
continue
}
}
@ -838,7 +877,7 @@ func (k *keyring) Remove() {
func setupGPGKeyring(logcfg loggingConfig, sshKeys []string, parentDir string) (*keyring, error) {
dir, err := os.MkdirTemp(parentDir, "gpg")
if err != nil {
return nil, fmt.Errorf("Cannot create gpg home in '%s': %s", parentDir, err)
return nil, fmt.Errorf("cannot create gpg home in '%s': %w", parentDir, err)
}
k := keyring{dir}
@ -869,7 +908,7 @@ func parseFlags(args []string) (*options, error) {
case Manifest, SopsFile, Off:
opts.checkMode = CheckMode(checkMode)
default:
return nil, fmt.Errorf("Invalid value provided for -check-mode flag: %s", opts.checkMode)
return nil, fmt.Errorf("invalid value provided for -check-mode flag: %s", opts.checkMode)
}
if fs.NArg() != 1 {
@ -905,9 +944,10 @@ func installSecrets(args []string) error {
}
if manifest.UserMode {
rundir, err := RuntimeDir()
var rundir string
rundir, err = RuntimeDir()
if opts.checkMode == Off && err != nil {
return fmt.Errorf("Error: %v", err)
return fmt.Errorf("cannot figure out runtime directory: %w", err)
}
manifest.SecretsMountPoint = replaceRuntimeDir(manifest.SecretsMountPoint, rundir)
manifest.SymlinkPath = replaceRuntimeDir(manifest.SymlinkPath, rundir)
@ -926,19 +966,19 @@ func installSecrets(args []string) error {
secretFiles: make(map[string]secretFile),
}
if err := app.validateManifest(); err != nil {
return fmt.Errorf("Manifest is not valid: %w", err)
if err = app.validateManifest(); err != nil {
return fmt.Errorf("manifest is not valid: %w", err)
}
if app.checkMode != Off {
return nil
}
var keysGid int
var keysGID int
if opts.ignorePasswd {
keysGid = 0
keysGID = 0
} else {
keysGid, err = lookupKeysGroup()
keysGID, err = lookupKeysGroup()
if err != nil {
return err
}
@ -946,14 +986,15 @@ func installSecrets(args []string) error {
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)
if err = MountSecretFs(manifest.SecretsMountPoint, keysGID, manifest.UseTmpfs, manifest.UserMode); err != nil {
return fmt.Errorf("failed to mount filesystem for secrets: %w", err)
}
if len(manifest.SSHKeyPaths) != 0 {
keyring, err := setupGPGKeyring(manifest.Logging, manifest.SSHKeyPaths, manifest.SecretsMountPoint)
var keyring *keyring
keyring, err = setupGPGKeyring(manifest.Logging, manifest.SSHKeyPaths, manifest.SecretsMountPoint)
if err != nil {
return fmt.Errorf("Error setting up gpg keyring: %w", err)
return fmt.Errorf("error setting up gpg keyring: %w", err)
}
defer keyring.Remove()
} else if manifest.GnupgHome != "" {
@ -961,20 +1002,21 @@ func installSecrets(args []string) error {
}
// Import age keys
if len(manifest.AgeSshKeyPaths) != 0 || manifest.AgeKeyFile != "" {
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
ageFile, err := os.OpenFile(keyfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
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)
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 len(manifest.AgeSSHKeyPaths) != 0 {
err = importAgeSSHKeys(manifest.Logging, manifest.AgeSSHKeyPaths, *ageFile)
if err != nil {
return err
}
@ -982,32 +1024,33 @@ func installSecrets(args []string) error {
// Import the keyfile
if manifest.AgeKeyFile != "" {
// Read the keyfile
contents, err := os.ReadFile(manifest.AgeKeyFile)
var contents []byte
contents, err = os.ReadFile(manifest.AgeKeyFile)
if err != nil {
return fmt.Errorf("Cannot read keyfile '%s': %w", manifest.AgeKeyFile, err)
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)
return fmt.Errorf("cannot write key to age file: %w", err)
}
}
}
if err := decryptSecrets(manifest.Secrets); err != nil {
if err = decryptSecrets(manifest.Secrets); err != nil {
return err
}
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid, manifest.UserMode)
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGID, manifest.UserMode)
if err != nil {
return fmt.Errorf("Failed to prepare new secrets directory: %w", err)
return fmt.Errorf("failed to prepare new secrets directory: %w", err)
}
if err := writeSecrets(*secretDir, manifest.Secrets, keysGid, manifest.UserMode); err != nil {
return fmt.Errorf("Cannot write secrets: %w", err)
if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil {
return fmt.Errorf("cannot write secrets: %w", err)
}
if !manifest.UserMode {
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)
}
}
// No need to perform the actual symlinking
@ -1015,13 +1058,13 @@ func installSecrets(args []string) error {
return nil
}
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.UserMode); err != nil {
return fmt.Errorf("Failed to prepare symlinks to secret store: %w", err)
return fmt.Errorf("failed to prepare symlinks to secret store: %w", err)
}
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
return fmt.Errorf("Cannot update secrets symlink: %w", err)
return fmt.Errorf("cannot update secrets symlink: %w", err)
}
if err := pruneGenerations(manifest.SecretsMountPoint, *secretDir, manifest.KeepGenerations); err != nil {
return fmt.Errorf("Cannot prune old secrets generations: %w", err)
return fmt.Errorf("cannot prune old secrets generations: %w", err)
}
return nil
@ -1029,7 +1072,7 @@ func installSecrets(args []string) error {
func main() {
if err := installSecrets(os.Args); err != nil {
if err == flag.ErrHelp {
if errors.Is(err, flag.ErrHelp) {
return
}
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)

View file

@ -83,7 +83,7 @@ func testGPG(t *testing.T) {
gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome))
ok(t, os.Mkdir(gpgHome, os.FileMode(0o700)))
cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc"))
cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc")) // nolint:gosec
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = gpgEnv
@ -296,7 +296,7 @@ func TestAgeWithSSH(t *testing.T) {
Secrets: []secret{s},
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
AgeSshKeyPaths: []string{path.Join(assets, "ssh-ed25519-key")},
AgeSSHKeyPaths: []string{path.Join(assets, "ssh-ed25519-key")},
}
testInstallSecret(t, testdir, &m)
@ -346,7 +346,7 @@ func TestIsValidFormat(t *testing.T) {
t.Errorf("input %s must return %v but returned %v", input, mustBe, result)
}
}
for _, format := range []string{string(Yaml), string(Json), string(Binary), string(Dotenv)} {
for _, format := range []string{string(Yaml), string(JSON), string(Binary), string(Dotenv)} {
generateCase(format, true)
generateCase(strings.ToUpper(format), false)
}

View file

@ -1,5 +1,47 @@
{ makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>
, pkgs ? (import <nixpkgs> { }) }: {
, pkgs ? (import <nixpkgs> { }) }:
let
userPasswordTest = name: extraConfig: makeTest {
inherit name;
nodes.machine = { config, lib, ... }: {
imports = [
../../modules/sops
extraConfig
];
sops = {
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key.neededForUsers = true;
secrets."nested/test/file".owner = "example-user";
};
users.users.example-user = {
isNormalUser = true;
hashedPasswordFile = config.sops.secrets.test_key.path;
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned...
assert user == "example-user", f"Expected 'example-user', got '{user}'"
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
# BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test
'' + pkgs.lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) ''
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
'';
} {
inherit pkgs;
inherit (pkgs) system;
};
in {
ssh-keys = makeTest {
name = "sops-ssh-keys";
nodes.server = { ... }: {
@ -23,57 +65,23 @@
inherit (pkgs) system;
};
user-passwords = makeTest {
name = "sops-user-passwords";
nodes.machine = { config, lib, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key.neededForUsers = true;
secrets."nested/test/file".owner = "example-user";
};
users.users.example-user = let
passwordFileKey =
if (lib.versionAtLeast (lib.versions.majorMinor lib.version)
"23.11") then
"hashedPasswordFile"
else
"passwordFile";
in {
isNormalUser = true;
${passwordFileKey} = config.sops.secrets.test_key.path;
};
};
testScript = ''
start_all()
machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work...
machine.succeed("[ $(stat -c%U /run/secrets/nested/test/file) = example-user ]") # ...and are owned...
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists
machine.succeed("/run/current-system/bin/switch-to-configuration test")
machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch
machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch
'';
} {
inherit pkgs;
inherit (pkgs) system;
};
pruning = makeTest {
name = "sops-pruning";
nodes.machine = { lib, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
keepGenerations = lib.mkDefault 0;
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
specialisation.pruning.configuration.sops.keepGenerations = 10;
};
@ -106,13 +114,19 @@
age-keys = makeTest {
name = "sops-age-keys";
nodes.machine = {
nodes.machine = { lib, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
};
testScript = ''
@ -211,14 +225,20 @@
templates = makeTest {
name = "sops-templates";
nodes.machine = { config, ... }: {
nodes.machine = { config, lib, ... }: {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
sops.templates.test_template = {
content = ''
This line is not modified.
@ -273,7 +293,7 @@
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = {
restartUnits = [ "restart-unit.service" "reload-unit.service" ];
@ -281,6 +301,12 @@
};
};
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
systemd.services."restart-unit" = {
description = "Restart unit";
# not started on boot
@ -377,4 +403,26 @@
inherit pkgs;
inherit (pkgs) system;
};
user-passwords = userPasswordTest "sops-user-passwords" {
# must run before sops sets up keys
boot.initrd.postDeviceCommands = ''
cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
};
} // pkgs.lib.optionalAttrs (pkgs.lib.versionAtLeast (pkgs.lib.versions.majorMinor pkgs.lib.version) "24.05") {
user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" {
systemd.sysusers.enable = true;
users.mutableUsers = true;
system.etc.overlay.enable = true;
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
# must run before sops sets up keys
systemd.services."sops-install-secrets-for-users".preStart = ''
printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt
chmod -R 700 /run/age-keys.txt
'';
};
}

View file

@ -21,7 +21,7 @@ func parsePrivateKey(sshPrivateKey []byte) (*rsa.PrivateKey, error) {
rsaKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("Only RSA keys are supported right now, got: %s", reflect.TypeOf(privateKey))
return nil, fmt.Errorf("only RSA keys are supported right now, got: %s", reflect.TypeOf(privateKey))
}
return rsaKey, nil