mirror of
https://github.com/Mic92/sops-nix.git
synced 2024-12-14 11:57:52 +00:00
9083e64fb9
It makes more sense to import the key when we have one and ignore the SSH keys instead of only importing the key when we have no SSH keys. This is because we import all SSH keys by default in the module and using a key file means the use has to explicitly unset the SSH keys.
689 lines
19 KiB
Go
689 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, ageFilePath string) error {
|
|
ageFile, err := os.OpenFile(ageFilePath, os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot create '%s': %w", ageFilePath, err)
|
|
}
|
|
defer ageFile.Close()
|
|
fmt.Fprintf(ageFile, "# generated by sops-nix at %s\n", time.Now().Format(time.RFC3339))
|
|
|
|
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)
|
|
}
|
|
|
|
if manifest.AgeKeyFile != "" {
|
|
os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile)
|
|
} else if len(manifest.AgeSshKeyPaths) != 0 {
|
|
keyfile := filepath.Join(manifest.SecretsMountPoint, "age-keys.txt")
|
|
err = importAgeSSHKeys(manifest.AgeSshKeyPaths, keyfile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
os.Setenv("SOPS_AGE_KEY_FILE", keyfile)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|