mirror of
https://github.com/Mic92/sops-nix.git
synced 2024-12-14 11:57:52 +00:00
705 lines
19 KiB
Go
705 lines
19 KiB
Go
// +build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
|
|
agessh "github.com/Mic92/ssh-to-age"
|
|
|
|
"github.com/mozilla-services/yaml"
|
|
"go.mozilla.org/sops/v3/decrypt"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type secret struct {
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
Path string `json:"path"`
|
|
Owner string `json:"owner"`
|
|
Group string `json:"group"`
|
|
SopsFile string `json:"sopsFile"`
|
|
Format FormatType `json:"format"`
|
|
Mode string `json:"mode"`
|
|
RestartServices []string `json:"restartServices"`
|
|
ReloadServices []string `json:"reloadServices"`
|
|
value []byte
|
|
mode os.FileMode
|
|
owner int
|
|
group int
|
|
}
|
|
|
|
type manifest struct {
|
|
Secrets []secret `json:"secrets"`
|
|
SecretsMountPoint string `json:"secretsMountpoint"`
|
|
SymlinkPath string `json:"symlinkPath"`
|
|
SSHKeyPaths []string `json:"sshKeyPaths"`
|
|
GnupgHome string `json:"gnupgHome"`
|
|
AgeKeyFile string `json:"ageKeyFile"`
|
|
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
|
|
}
|
|
|
|
type secretFile struct {
|
|
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"
|
|
Binary FormatType = "binary"
|
|
)
|
|
|
|
func (f *FormatType) UnmarshalJSON(b []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(b, &s); err != nil {
|
|
return err
|
|
}
|
|
var t = FormatType(s)
|
|
switch t {
|
|
case "":
|
|
*f = Yaml
|
|
case Yaml, Json, Binary:
|
|
*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 {
|
|
checkMode CheckMode
|
|
manifest string
|
|
}
|
|
|
|
type appContext struct {
|
|
manifest manifest
|
|
secretFiles map[string]secretFile
|
|
checkMode CheckMode
|
|
}
|
|
|
|
func secureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) error {
|
|
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)
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
buf := make([]byte, len(expectedTarget)+1) // oversize by one to detect trunc
|
|
n, err := unix.Readlinkat(fd, "", buf)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't readlinkat %s", symlinkToCheck)
|
|
}
|
|
if n > len(expectedTarget) || string(buf[:n]) != expectedTarget {
|
|
return fmt.Errorf("symlink %s does not point to %s", symlinkToCheck, expectedTarget)
|
|
}
|
|
err = unix.Fchownat(fd, "", owner, group, unix.AT_EMPTY_PATH)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot change owner of '%s' to %d/%d: %w", symlinkToCheck, owner, group, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readManifest(path string) (*manifest, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
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 &m, nil
|
|
}
|
|
|
|
func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, secret *secret) bool {
|
|
validUG := true
|
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
|
validUG = validUG && int(stat.Uid) == secret.owner
|
|
validUG = validUG && int(stat.Gid) == secret.group
|
|
} else {
|
|
panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.")
|
|
}
|
|
return linkTarget == targetFile && validUG
|
|
}
|
|
|
|
func symlinkSecret(targetFile string, secret *secret) 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 := 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)
|
|
}
|
|
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)
|
|
} 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func symlinkSecrets(targetDir string, secrets []secret) error {
|
|
for _, 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)
|
|
}
|
|
if err := symlinkSecret(targetFile, &secret); err != nil {
|
|
return fmt.Errorf("Failed to symlink secret '%s': %w", secret.Path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type plainData struct {
|
|
data map[string]interface{}
|
|
binary []byte
|
|
}
|
|
|
|
func decryptSecret(s *secret, sourceFiles map[string]plainData) error {
|
|
sourceFile := sourceFiles[s.SopsFile]
|
|
if sourceFile.data == nil || sourceFile.binary == nil {
|
|
plain, err := decrypt.File(s.SopsFile, string(s.Format))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFile, err)
|
|
}
|
|
if s.Format == Binary {
|
|
sourceFile.binary = plain
|
|
} else {
|
|
if s.Format == Yaml {
|
|
if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil {
|
|
return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
|
|
}
|
|
} else {
|
|
if err := json.Unmarshal(plain, &sourceFile.data); err != nil {
|
|
return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if s.Format == Binary {
|
|
s.value = sourceFile.binary
|
|
} else {
|
|
val, ok := sourceFile.data[s.Key]
|
|
|
|
if !ok {
|
|
return fmt.Errorf("The key '%s' cannot be found in '%s'", s.Key, s.SopsFile)
|
|
}
|
|
strVal, ok := val.(string)
|
|
if !ok {
|
|
return fmt.Errorf("The value of key '%s' in '%s' is not a string", s.Key, s.SopsFile)
|
|
}
|
|
s.value = []byte(strVal)
|
|
}
|
|
sourceFiles[s.SopsFile] = sourceFile
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const RAMFS_MAGIC int32 = -2054924042
|
|
|
|
func mountSecretFs(mountpoint string, keysGid int) error {
|
|
if err := os.MkdirAll(mountpoint, 0751); err != nil {
|
|
return fmt.Errorf("Cannot create directory '%s': %w", mountpoint, err)
|
|
}
|
|
|
|
buf := unix.Statfs_t{}
|
|
if err := unix.Statfs(mountpoint, &buf); err != nil {
|
|
return fmt.Errorf("Cannot get statfs for directory '%s': %w", mountpoint, err)
|
|
}
|
|
if int32(buf.Type) != RAMFS_MAGIC {
|
|
if err := unix.Mount("none", mountpoint, "ramfs", unix.MS_NODEV|unix.MS_NOSUID, "mode=0751"); err != nil {
|
|
return fmt.Errorf("Cannot mount: %s", 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
|
|
}
|
|
|
|
func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int) (*string, error) {
|
|
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)
|
|
}
|
|
}
|
|
} else if !os.IsNotExist(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)
|
|
}
|
|
}
|
|
if err := os.Mkdir(dir, os.FileMode(0751)); err != nil {
|
|
return nil, fmt.Errorf("mkdir(): %w", 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) error {
|
|
for _, secret := range secrets {
|
|
filepath := filepath.Join(secretDir, secret.Name)
|
|
if err := ioutil.WriteFile(filepath, []byte(secret.value), secret.mode); err != nil {
|
|
return fmt.Errorf("Cannot write %s: %w", filepath, err)
|
|
}
|
|
if err := os.Chown(filepath, secret.owner, secret.group); err != nil {
|
|
return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", filepath, secret.owner, secret.group, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lookupKeysGroup() (int, error) {
|
|
group, err := user.LookupGroup("keys")
|
|
if err != nil {
|
|
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 int(gid), nil
|
|
}
|
|
|
|
func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) {
|
|
if app.checkMode == Manifest {
|
|
return &secretFile{firstSecret: s}, nil
|
|
}
|
|
|
|
cipherText, err := ioutil.ReadFile(s.SopsFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed reading %s: %w", s.SopsFile, err)
|
|
}
|
|
|
|
var keys map[string]interface{}
|
|
if s.Format == Binary {
|
|
if err := json.Unmarshal(cipherText, &keys); err != nil {
|
|
return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
|
|
}
|
|
return &secretFile{cipherText: cipherText, firstSecret: s}, nil
|
|
}
|
|
|
|
if s.Format == Yaml {
|
|
if err := yaml.Unmarshal(cipherText, &keys); err != nil {
|
|
return nil, fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err)
|
|
}
|
|
} else if err := json.Unmarshal(cipherText, &keys); err != nil {
|
|
return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err)
|
|
}
|
|
|
|
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 {
|
|
if _, ok := file.keys[s.Key]; !ok {
|
|
return fmt.Errorf("secret %s with the key %s not found in %s", s.Name, s.Key, s.SopsFile)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
secret.mode = os.FileMode(mode)
|
|
|
|
if app.checkMode == Off {
|
|
// 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)
|
|
}
|
|
ownerNr, err := strconv.ParseUint(owner.Uid, 10, 64)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
groupNr, err := strconv.ParseUint(group.Gid, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot parse gid %s: %w", group.Gid, err)
|
|
}
|
|
secret.group = int(groupNr)
|
|
}
|
|
|
|
if secret.Format == "" {
|
|
secret.Format = "yaml"
|
|
}
|
|
|
|
if secret.Format != "yaml" && secret.Format != "json" && secret.Format != "binary" {
|
|
return fmt.Errorf("Unsupported format %s for secret %s", secret.Format, secret.Name)
|
|
}
|
|
|
|
file, ok := app.secretFiles[secret.SopsFile]
|
|
if !ok {
|
|
maybeFile, err := app.loadSopsFile(secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
app.secretFiles[secret.SopsFile] = *maybeFile
|
|
file = *maybeFile
|
|
}
|
|
|
|
return app.validateSopsFile(secret, &file)
|
|
}
|
|
|
|
func (app *appContext) validateManifest() error {
|
|
m := &app.manifest
|
|
if m.SecretsMountPoint == "" {
|
|
m.SecretsMountPoint = "/run/secrets.d"
|
|
}
|
|
if m.SymlinkPath == "" {
|
|
m.SymlinkPath = "/run/secrets"
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
|
|
for i := range m.Secrets {
|
|
if err := app.validateSecret(&m.Secrets[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func atomicSymlink(oldname, newname string) error {
|
|
// 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.
|
|
d, err := ioutil.TempDir(filepath.Dir(newname), "."+filepath.Base(newname))
|
|
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 importSSHKeys(keyPaths []string, gpgHome string) error {
|
|
secringPath := filepath.Join(gpgHome, "secring.gpg")
|
|
|
|
secring, err := os.OpenFile(secringPath, os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot create %s: %w", secringPath, err)
|
|
}
|
|
for _, p := range keyPaths {
|
|
sshKey, err := ioutil.ReadFile(p)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
|
|
}
|
|
gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := gpgKey.SerializePrivate(secring, nil); err != nil {
|
|
return fmt.Errorf("Cannot write secring: %w", err)
|
|
}
|
|
|
|
fmt.Printf("%s: Imported %s with fingerprint %s\n", path.Base(os.Args[0]), p, hex.EncodeToString(gpgKey.PrimaryKey.Fingerprint[:]))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func importAgeSSHKeys(keyPaths []string, ageFile os.File) error {
|
|
for _, p := range keyPaths {
|
|
// Read the key
|
|
sshKey, err := ioutil.ReadFile(p)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
|
|
}
|
|
// Convert the key to age
|
|
bech32, err := agessh.SSHPrivateKeyToAge(sshKey)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot convert ssh key '%s': %w", p, err)
|
|
}
|
|
// Append it to the file
|
|
_, err = ageFile.WriteString(*bech32 + "\n")
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot write key to age file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type keyring struct {
|
|
path string
|
|
}
|
|
|
|
func (k *keyring) Remove() {
|
|
os.RemoveAll(k.path)
|
|
os.Unsetenv("GNUPGHOME")
|
|
}
|
|
|
|
func setupGPGKeyring(sshKeys []string, parentDir string) (*keyring, error) {
|
|
dir, err := ioutil.TempDir(parentDir, "gpg")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot create gpg home in '%s': %s", parentDir, err)
|
|
}
|
|
k := keyring{dir}
|
|
|
|
if err := importSSHKeys(sshKeys, dir); err != nil {
|
|
os.RemoveAll(dir)
|
|
return nil, err
|
|
}
|
|
os.Setenv("GNUPGHOME", dir)
|
|
|
|
return &k, nil
|
|
}
|
|
|
|
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()
|
|
}
|
|
var checkMode string
|
|
fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`)
|
|
if err := fs.Parse(args[1:]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if fs.NArg() != 1 {
|
|
flag.Usage()
|
|
return nil, flag.ErrHelp
|
|
}
|
|
opts.manifest = fs.Arg(0)
|
|
return &opts, nil
|
|
}
|
|
|
|
func installSecrets(args []string) error {
|
|
opts, err := parseFlags(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manifest, err := readManifest(opts.manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
app := appContext{
|
|
manifest: *manifest,
|
|
checkMode: opts.checkMode,
|
|
secretFiles: make(map[string]secretFile),
|
|
}
|
|
|
|
if err := app.validateManifest(); err != nil {
|
|
return fmt.Errorf("Manifest is not valid: %w", err)
|
|
}
|
|
|
|
if app.checkMode != Off {
|
|
return nil
|
|
}
|
|
|
|
keysGid, err := lookupKeysGroup()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := mountSecretFs(manifest.SecretsMountPoint, keysGid); err != nil {
|
|
return fmt.Errorf("Failed to mount filesystem for secrets: %w", err)
|
|
}
|
|
|
|
if len(manifest.SSHKeyPaths) != 0 {
|
|
keyring, err := setupGPGKeyring(manifest.SSHKeyPaths, manifest.SecretsMountPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("Error setting up gpg keyring: %w", err)
|
|
}
|
|
defer keyring.Remove()
|
|
} else if manifest.GnupgHome != "" {
|
|
os.Setenv("GNUPGHOME", manifest.GnupgHome)
|
|
}
|
|
|
|
// 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
|
|
ageFile, err := os.OpenFile(keyfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
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.AgeSshKeyPaths, *ageFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Import the keyfile
|
|
if manifest.AgeKeyFile != "" {
|
|
// Read the keyfile
|
|
contents, err := ioutil.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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := decryptSecrets(manifest.Secrets); err != nil {
|
|
return err
|
|
}
|
|
|
|
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to prepare new secrets directory: %w", err)
|
|
}
|
|
if err := writeSecrets(*secretDir, manifest.Secrets); err != nil {
|
|
return fmt.Errorf("Cannot write secrets: %w", err)
|
|
}
|
|
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets); err != nil {
|
|
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 nil
|
|
|
|
}
|
|
|
|
func main() {
|
|
if err := installSecrets(os.Args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
|
|
os.Exit(1)
|
|
}
|
|
}
|