2020-07-22 22:46:05 +00:00
// +build linux
2020-07-06 06:30:09 +00:00
package main
import (
2021-09-02 09:00:57 +00:00
"bytes"
2020-07-14 12:41:32 +00:00
"encoding/hex"
2020-07-06 06:30:09 +00:00
"encoding/json"
2020-07-19 10:32:59 +00:00
"flag"
2020-07-06 06:30:09 +00:00
"fmt"
"io/ioutil"
"os"
"os/user"
2020-07-30 15:19:51 +00:00
"path"
2020-07-06 06:30:09 +00:00
"path/filepath"
"strconv"
"strings"
2020-08-24 08:24:43 +00:00
"syscall"
2021-08-27 18:09:28 +00:00
"time"
2020-07-06 06:30:09 +00:00
2021-02-01 11:12:20 +00:00
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
2021-09-01 11:32:03 +00:00
agessh "github.com/Mic92/ssh-to-age"
2020-07-12 12:50:55 +00:00
2020-07-06 06:30:09 +00:00
"github.com/mozilla-services/yaml"
"go.mozilla.org/sops/v3/decrypt"
"golang.org/x/sys/unix"
2023-01-16 12:51:41 +00:00
"github.com/joho/godotenv"
2020-07-06 06:30:09 +00:00
)
type secret struct {
2021-09-02 09:00:57 +00:00
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" `
RestartUnits [ ] string ` json:"restartUnits" `
2022-03-14 16:30:56 +00:00
ReloadUnits [ ] string ` json:"reloadUnits" `
2021-09-02 09:00:57 +00:00
value [ ] byte
mode os . FileMode
owner int
group int
}
type loggingConfig struct {
KeyImport bool ` json:"keyImport" `
SecretChanges bool ` json:"secretChanges" `
2020-07-06 06:30:09 +00:00
}
type manifest struct {
2021-09-02 09:00:57 +00:00
Secrets [ ] secret ` json:"secrets" `
2021-10-19 16:04:34 +00:00
SecretsMountPoint string ` json:"secretsMountPoint" `
2021-09-02 09:00:57 +00:00
SymlinkPath string ` json:"symlinkPath" `
2021-11-06 20:29:22 +00:00
KeepGenerations int ` json:"keepGenerations" `
2021-09-02 09:00:57 +00:00
SSHKeyPaths [ ] string ` json:"sshKeyPaths" `
GnupgHome string ` json:"gnupgHome" `
AgeKeyFile string ` json:"ageKeyFile" `
AgeSshKeyPaths [ ] string ` json:"ageSshKeyPaths" `
2022-03-29 17:14:06 +00:00
UserMode bool ` json:"userMode" `
2021-09-02 09:00:57 +00:00
Logging loggingConfig ` json:"logging" `
2020-07-06 06:30:09 +00:00
}
2020-07-19 20:04:58 +00:00
type secretFile struct {
2020-07-19 16:09:27 +00: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"
Binary FormatType = "binary"
2023-01-16 12:51:41 +00:00
Dotenv FormatType = "dotenv"
Ini FormatType = "ini"
2020-07-19 16:09:27 +00:00
)
2023-01-16 12:51:41 +00:00
func IsValidFormat ( format string ) bool {
switch format {
case string ( Yaml ) ,
string ( Json ) ,
string ( Binary ) ,
string ( Dotenv ) ,
string ( Ini ) :
return true
default :
return false
}
}
2020-07-19 16:09:27 +00:00
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
2023-01-16 12:51:41 +00:00
case Yaml , Json , Binary , Dotenv , Ini :
2020-07-19 16:09:27 +00: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 16:26:43 +00:00
checkMode CheckMode
manifest string
ignorePasswd bool
2020-07-19 16:09:27 +00:00
}
type appContext struct {
2021-10-19 16:26:43 +00:00
manifest manifest
secretFiles map [ string ] secretFile
checkMode CheckMode
ignorePasswd bool
2020-07-19 16:09:27 +00:00
}
2020-08-24 08:24:43 +00:00
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 {
2021-02-01 11:12:20 +00:00
return fmt . Errorf ( "Failed to open %s: %w" , symlinkToCheck , err )
2020-08-24 08:24:43 +00:00
}
defer unix . Close ( fd )
2021-02-01 11:12:20 +00:00
buf := make ( [ ] byte , len ( expectedTarget ) + 1 ) // oversize by one to detect trunc
2020-08-24 08:24:43 +00:00
n , err := unix . Readlinkat ( fd , "" , buf )
if err != nil {
return fmt . Errorf ( "couldn't readlinkat %s" , symlinkToCheck )
}
2021-02-01 11:12:20 +00:00
if n > len ( expectedTarget ) || string ( buf [ : n ] ) != expectedTarget {
2020-08-24 08:24:43 +00:00
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
}
2020-07-06 06:30:09 +00:00
func readManifest ( path string ) ( * manifest , error ) {
file , err := os . Open ( path )
if err != nil {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Failed to open manifest: %w" , err )
2020-07-06 06:30:09 +00:00
}
defer file . Close ( )
dec := json . NewDecoder ( file )
var m manifest
if err := dec . Decode ( & m ) ; err != nil {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Failed to parse manifest: %w" , err )
2020-07-06 06:30:09 +00:00
}
return & m , nil
}
2020-08-24 08:24:43 +00:00
func linksAreEqual ( linkTarget , targetFile string , info os . FileInfo , secret * secret ) bool {
2021-02-01 11:12:20 +00:00
validUG := true
2020-08-24 08:24:43 +00:00
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
}
2022-03-29 17:14:06 +00:00
func symlinkSecret ( targetFile string , secret * secret , userMode bool ) error {
2020-07-12 12:50:55 +00:00
for {
2020-07-19 22:23:38 +00:00
stat , err := os . Lstat ( secret . Path )
2020-07-12 12:50:55 +00:00
if os . IsNotExist ( err ) {
if err := os . Symlink ( targetFile , secret . Path ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot create symlink '%s': %w" , secret . Path , err )
2020-07-12 12:50:55 +00:00
}
2022-03-29 17:14:06 +00:00
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 )
}
2020-08-24 08:24:43 +00:00
}
2020-07-12 12:50:55 +00:00
return nil
} else if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot stat '%s': %w" , secret . Path , err )
2020-07-19 22:23:38 +00:00
}
if stat . Mode ( ) & os . ModeSymlink == os . ModeSymlink {
linkTarget , err := os . Readlink ( secret . Path )
if os . IsNotExist ( err ) {
continue
} else if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot read symlink '%s': %w" , secret . Path , err )
2020-08-24 08:24:43 +00:00
} else if linksAreEqual ( linkTarget , targetFile , stat , secret ) {
2020-07-19 22:23:38 +00:00
return nil
}
2020-07-12 12:50:55 +00:00
}
if err := os . Remove ( secret . Path ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot override %s: %w" , secret . Path , err )
2020-07-12 12:50:55 +00:00
}
}
}
2022-03-29 17:14:06 +00:00
func symlinkSecrets ( targetDir string , secrets [ ] secret , userMode bool ) error {
2020-07-06 06:30:09 +00:00
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 {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot create parent directory of '%s': %w" , secret . Path , err )
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00:00
if err := symlinkSecret ( targetFile , & secret , userMode ) ; err != nil {
2020-08-24 08:24:43 +00:00
return fmt . Errorf ( "Failed to symlink secret '%s': %w" , secret . Path , err )
2020-07-06 06:30:09 +00:00
}
}
return nil
}
type plainData struct {
2021-01-27 06:22:19 +00:00
data map [ string ] interface { }
2020-07-06 06:30:09 +00:00
binary [ ] byte
}
2021-09-10 10:02:38 +00: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 != "" {
2021-11-24 16:19:03 +00:00
keyUntilNow += "/"
2021-09-10 10:02:38 +00:00
}
2021-11-23 21:32:41 +00:00
return "" , fmt . Errorf ( "The key '%s%s' cannot be found" , keyUntilNow , currentKey )
2021-09-10 10:02:38 +00:00
}
break
}
thisKey := currentKey [ : slashIndex ]
if keyUntilNow == "" {
keyUntilNow = thisKey
} else {
2021-11-24 16:19:03 +00:00
keyUntilNow += "/" + thisKey
2021-09-10 10:02:38 +00:00
}
currentKey = currentKey [ ( slashIndex + 1 ) : ]
val , ok = currentData [ thisKey ]
if ! ok {
return "" , fmt . Errorf ( "The key '%s' cannot be found" , keyUntilNow )
}
valWithWrongType , ok := val . ( map [ interface { } ] interface { } )
if ! ok {
return "" , fmt . Errorf ( "Key '%s' does not refer to a dictionary" , keyUntilNow )
}
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 )
}
return strVal , nil
}
2020-07-06 06:30:09 +00:00
func decryptSecret ( s * secret , sourceFiles map [ string ] plainData ) error {
sourceFile := sourceFiles [ s . SopsFile ]
if sourceFile . data == nil || sourceFile . binary == nil {
2020-07-19 16:09:27 +00:00
plain , err := decrypt . File ( s . SopsFile , string ( s . Format ) )
2020-07-06 06:30:09 +00:00
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to decrypt '%s': %w" , s . SopsFile , err )
2020-07-06 06:30:09 +00:00
}
2023-01-16 12:51:41 +00:00
switch s . Format {
case Binary , Dotenv , Ini :
2020-07-06 06:30:09 +00:00
sourceFile . binary = plain
2023-01-16 12:51:41 +00:00
case Yaml :
if err := yaml . Unmarshal ( plain , & sourceFile . data ) ; err != nil {
return fmt . Errorf ( "Cannot parse yaml of '%s': %w" , s . SopsFile , err )
2020-07-06 06:30:09 +00:00
}
2023-01-16 12:51:41 +00:00
case Json :
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 06:30:09 +00:00
}
}
2023-01-16 12:51:41 +00:00
switch s . Format {
case Binary , Dotenv , Ini :
2020-07-06 06:30:09 +00:00
s . value = sourceFile . binary
2023-01-16 12:51:41 +00:00
case Yaml , Json :
2021-09-10 10:02:38 +00:00
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 )
2021-02-01 11:12:20 +00:00
}
2021-01-27 06:22:19 +00:00
s . value = [ ] byte ( strVal )
2020-07-06 06:30:09 +00: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
}
2021-02-01 08:25:59 +00:00
const RAMFS_MAGIC int32 = - 2054924042
2021-01-27 07:36:33 +00:00
2022-03-29 17:14:06 +00:00
func mountSecretFs ( mountpoint string , keysGid int , userMode bool ) error {
2021-06-05 15:37:38 +00:00
if err := os . MkdirAll ( mountpoint , 0751 ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot create directory '%s': %w" , mountpoint , err )
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00:00
// We can't create a ramfs as user
if userMode {
return nil
}
2021-02-01 11:12:20 +00:00
buf := unix . Statfs_t { }
2021-01-27 07:36:33 +00:00
if err := unix . Statfs ( mountpoint , & buf ) ; err != nil {
return fmt . Errorf ( "Cannot get statfs for directory '%s': %w" , mountpoint , err )
}
2021-02-01 08:25:59 +00:00
if int32 ( buf . Type ) != RAMFS_MAGIC {
2021-06-05 15:37:38 +00:00
if err := unix . Mount ( "none" , mountpoint , "ramfs" , unix . MS_NODEV | unix . MS_NOSUID , "mode=0751" ) ; err != nil {
2021-01-27 07:36:33 +00:00
return fmt . Errorf ( "Cannot mount: %s" , err )
}
2020-07-06 06:30:09 +00:00
}
2020-07-14 12:21:07 +00:00
if err := os . Chown ( mountpoint , 0 , int ( keysGid ) ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot change owner/group of '%s' to 0/%d: %w" , mountpoint , keysGid , err )
2020-07-14 12:21:07 +00:00
}
2020-07-06 06:30:09 +00:00
return nil
}
2022-03-29 17:14:06 +00:00
func prepareSecretsDir ( secretMountpoint string , linkName string , keysGid int , userMode bool ) ( * string , error ) {
2020-07-06 06:30:09 +00: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 {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Cannot parse %s of %s as a number: %w" , targetBasename , linkTarget , err )
2020-07-06 06:30:09 +00:00
}
}
} else if ! os . IsNotExist ( err ) {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Cannot access %s: %w" , linkName , err )
2020-07-06 06:30:09 +00: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 {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Cannot remove existing %s: %w" , dir , err )
2020-07-06 06:30:09 +00:00
}
}
2021-06-05 15:37:38 +00:00
if err := os . Mkdir ( dir , os . FileMode ( 0751 ) ) ; err != nil {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "mkdir(): %w" , err )
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00: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 06:30:09 +00:00
}
return & dir , nil
}
2022-03-29 17:14:06 +00:00
func writeSecrets ( secretDir string , secrets [ ] secret , keysGid int , userMode bool ) error {
2020-07-06 06:30:09 +00:00
for _ , secret := range secrets {
2021-09-10 10:02:38 +00:00
fp := filepath . Join ( secretDir , secret . Name )
dirs := strings . Split ( filepath . Dir ( secret . Name ) , "/" )
pathSoFar := secretDir
for _ , dir := range dirs {
pathSoFar = filepath . Join ( pathSoFar , dir )
if err := os . MkdirAll ( pathSoFar , 0751 ) ; err != nil {
return fmt . Errorf ( "Cannot create directory '%s' for %s: %w" , pathSoFar , fp , err )
}
2022-03-29 17:14:06 +00:00
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 )
}
2021-09-10 10:02:38 +00:00
}
2020-07-06 06:30:09 +00:00
}
2021-09-10 10:02:38 +00:00
if err := ioutil . WriteFile ( fp , [ ] byte ( secret . value ) , secret . mode ) ; err != nil {
return fmt . Errorf ( "Cannot write %s: %w" , fp , err )
}
2022-03-29 17:14:06 +00: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 06:30:09 +00:00
}
}
return nil
}
func lookupKeysGroup ( ) ( int , error ) {
group , err := user . LookupGroup ( "keys" )
if err != nil {
2020-07-30 15:19:14 +00:00
return 0 , fmt . Errorf ( "Failed to lookup 'keys' group: %w" , err )
2020-07-06 06:30:09 +00:00
}
gid , err := strconv . ParseInt ( group . Gid , 10 , 64 )
if err != nil {
2020-07-30 15:19:14 +00:00
return 0 , fmt . Errorf ( "Cannot parse keys gid %s: %w" , group . Gid , err )
2020-07-06 06:30:09 +00:00
}
return int ( gid ) , nil
}
2020-07-19 16:09:27 +00:00
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 {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Failed reading %s: %w" , s . SopsFile , err )
2020-07-19 16:09:27 +00:00
}
var keys map [ string ] interface { }
2023-01-16 12:51:41 +00:00
switch s . Format {
case Binary :
2020-07-19 16:09:27 +00:00
if err := json . Unmarshal ( cipherText , & keys ) ; err != nil {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Cannot parse json of '%s': %w" , s . SopsFile , err )
2020-07-19 16:09:27 +00:00
}
return & secretFile { cipherText : cipherText , firstSecret : s } , nil
2023-01-16 12:51:41 +00:00
case Yaml :
2020-07-19 16:09:27 +00:00
if err := yaml . Unmarshal ( cipherText , & keys ) ; err != nil {
2020-07-30 15:19:14 +00:00
return nil , fmt . Errorf ( "Cannot parse yaml of '%s': %w" , s . SopsFile , err )
2020-07-19 16:09:27 +00:00
}
2023-01-16 12:51:41 +00: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 )
}
2020-07-19 16:09:27 +00: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 )
}
2023-01-16 12:51:41 +00:00
if app . checkMode != Manifest && ( ! ( s . Format == Binary || s . Format == Dotenv || s . Format == Ini ) ) {
2021-09-10 10:02:38 +00: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 16:09:27 +00:00
}
}
return nil
}
func ( app * appContext ) validateSecret ( secret * secret ) error {
2020-07-06 06:30:09 +00:00
mode , err := strconv . ParseUint ( secret . Mode , 8 , 16 )
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Invalid number in mode: %d: %w" , mode , err )
2020-07-06 06:30:09 +00:00
}
secret . mode = os . FileMode ( mode )
2022-08-25 14:13:36 +00:00
if app . ignorePasswd || os . Getenv ( "NIXOS_ACTION" ) == "dry-activate" {
2021-10-19 16:26:43 +00:00
secret . owner = 0
secret . group = 0
2022-03-29 17:14:06 +00:00
} else if app . checkMode == Off || app . ignorePasswd {
2020-07-19 20:04:58 +00:00
// we only access to the user/group during deployment
owner , err := user . Lookup ( secret . Owner )
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to lookup user '%s': %w" , secret . Owner , err )
2020-07-19 20:04:58 +00:00
}
ownerNr , err := strconv . ParseUint ( owner . Uid , 10 , 64 )
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot parse uid %s: %w" , owner . Uid , err )
2020-07-19 20:04:58 +00:00
}
secret . owner = int ( ownerNr )
2020-07-06 06:30:09 +00:00
2020-07-19 20:04:58 +00:00
group , err := user . LookupGroup ( secret . Group )
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to lookup group '%s': %w" , secret . Group , err )
2020-07-19 20:04:58 +00:00
}
groupNr , err := strconv . ParseUint ( group . Gid , 10 , 64 )
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot parse gid %s: %w" , group . Gid , err )
2020-07-19 20:04:58 +00:00
}
secret . group = int ( groupNr )
2020-07-06 06:30:09 +00:00
}
if secret . Format == "" {
secret . Format = "yaml"
}
2023-01-16 12:51:41 +00:00
if ! IsValidFormat ( string ( secret . Format ) ) {
2020-07-19 16:09:27 +00:00
return fmt . Errorf ( "Unsupported format %s for secret %s" , secret . Format , secret . Name )
2020-07-06 06:30:09 +00:00
}
2020-07-19 16:09:27 +00:00
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 )
2020-07-06 06:30:09 +00:00
}
2020-07-19 16:09:27 +00:00
func ( app * appContext ) validateManifest ( ) error {
m := & app . manifest
2021-08-26 22:49:58 +00: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 12:50:55 +00:00
}
2021-08-26 22:49:58 +00:00
2020-07-06 06:30:09 +00:00
for i := range m . Secrets {
2020-07-19 16:09:27 +00:00
if err := app . validateSecret ( & m . Secrets [ i ] ) ; err != nil {
2020-07-06 06:30:09 +00:00
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 )
}
2021-11-06 20:29:22 +00:00
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
}
2021-09-02 09:00:57 +00:00
func importSSHKeys ( logcfg loggingConfig , keyPaths [ ] string , gpgHome string ) error {
2020-07-12 12:50:55 +00:00
secringPath := filepath . Join ( gpgHome , "secring.gpg" )
2020-07-14 12:41:03 +00:00
secring , err := os . OpenFile ( secringPath , os . O_WRONLY | os . O_CREATE , 0600 )
2020-07-12 12:50:55 +00:00
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot create %s: %w" , secringPath , err )
2020-07-12 12:50:55 +00:00
}
2020-07-30 15:19:14 +00:00
for _ , p := range keyPaths {
sshKey , err := ioutil . ReadFile ( p )
2020-07-12 12:50:55 +00:00
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot read ssh key '%s': %w" , p , err )
2020-07-12 12:50:55 +00:00
}
gpgKey , err := sshkeys . SSHPrivateKeyToPGP ( sshKey )
if err != nil {
return err
}
2020-07-14 12:41:32 +00:00
2020-07-12 12:50:55 +00:00
if err := gpgKey . SerializePrivate ( secring , nil ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot write secring: %w" , err )
2020-07-12 12:50:55 +00:00
}
2020-07-14 12:41:32 +00:00
2021-09-02 09:00:57 +00:00
if logcfg . KeyImport {
2022-07-08 22:04:54 +00:00
fmt . Printf ( "%s: Imported %s as GPG key with fingerprint %s\n" , path . Base ( os . Args [ 0 ] ) , p , hex . EncodeToString ( gpgKey . PrimaryKey . Fingerprint [ : ] ) )
2021-09-02 09:00:57 +00:00
}
2020-07-12 12:50:55 +00:00
}
return nil
}
2022-07-08 22:04:54 +00:00
func importAgeSSHKeys ( logcfg loggingConfig , keyPaths [ ] string , ageFile os . File ) error {
2021-08-27 18:09:28 +00:00
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 )
}
2021-09-01 11:32:03 +00:00
// Convert the key to age
2022-07-08 22:04:54 +00:00
privKey , pubKey , err := agessh . SSHPrivateKeyToAge ( sshKey )
2021-08-27 18:09:28 +00:00
if err != nil {
return fmt . Errorf ( "Cannot convert ssh key '%s': %w" , p , err )
}
// Append it to the file
2022-07-08 22:04:54 +00:00
_ , err = ageFile . WriteString ( * privKey + "\n" )
2021-08-27 18:09:28 +00:00
if err != nil {
return fmt . Errorf ( "Cannot write key to age file: %w" , err )
}
2022-07-08 22:04:54 +00:00
if logcfg . KeyImport {
fmt . Printf ( "%s: Imported %s as age key with fingerprint %s\n" , path . Base ( os . Args [ 0 ] ) , p , * pubKey )
}
2021-08-27 18:09:28 +00:00
}
return nil
}
2021-09-02 09:00:57 +00:00
// 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 )
}
func handleModifications ( isDry bool , logcfg loggingConfig , symlinkPath string , secretDir string , secrets [ ] secret ) error {
var restart [ ] string
2022-03-14 16:30:56 +00:00
var reload [ ] string
2021-09-02 09:00:57 +00:00
newSecrets := make ( map [ string ] bool )
modifiedSecrets := make ( map [ string ] bool )
removedSecrets := 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
oldData , err := ioutil . ReadFile ( oldPath )
if err != nil {
if os . IsNotExist ( err ) {
// File did not exist before
restart = append ( restart , secret . RestartUnits ... )
2022-03-14 16:30:56 +00:00
reload = append ( reload , secret . ReloadUnits ... )
2021-09-02 09:00:57 +00:00
newSecrets [ secret . Name ] = true
continue
}
return err
}
// Read the new file
newData , err := ioutil . ReadFile ( newPath )
if err != nil {
return err
}
if ! bytes . Equal ( oldData , newData ) {
restart = append ( restart , secret . RestartUnits ... )
2022-03-14 16:30:56 +00:00
reload = append ( reload , secret . ReloadUnits ... )
2021-09-02 09:00:57 +00:00
modifiedSecrets [ secret . Name ] = true
}
}
writeLines := func ( list [ ] string , file string ) error {
if len ( list ) != 0 {
f , err := os . OpenFile ( file , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0600 )
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
}
2022-03-14 16:30:56 +00:00
if err := writeLines ( reload , dryPrefix + "-reload-list" ) ; err != nil {
return err
}
2021-09-02 09:00:57 +00:00
// Do not output changes if not requested
if ! logcfg . SecretChanges {
return nil
}
// Find removed secrets
err := symlinkWalk ( symlinkPath , symlinkPath , func ( path string , info os . FileInfo , err error ) error {
if err != nil {
return err
}
if info . IsDir ( ) {
return nil
}
path = strings . TrimPrefix ( path , symlinkPath + string ( os . PathSeparator ) )
for _ , secret := range secrets {
if secret . Name == path {
return nil
}
}
removedSecrets [ path ] = true
return nil
} )
if err != nil {
return err
}
// Output new/modified/removed secrets
outputChanged := func ( changed map [ string ] bool , regularPrefix , dryPrefix string ) {
if len ( changed ) > 0 {
s := ""
if len ( changed ) != 1 {
s = "s"
}
if isDry {
fmt . Printf ( "%s secret%s: " , dryPrefix , s )
} else {
fmt . Printf ( "%s secret%s: " , regularPrefix , s )
}
comma := ""
for name := range changed {
fmt . Printf ( "%s%s" , comma , name )
comma = ", "
}
fmt . Println ( )
}
}
outputChanged ( newSecrets , "adding" , "would add" )
outputChanged ( modifiedSecrets , "modifying" , "would modify" )
outputChanged ( removedSecrets , "removing" , "would remove" )
return nil
}
2020-07-12 12:50:55 +00:00
type keyring struct {
path string
}
func ( k * keyring ) Remove ( ) {
os . RemoveAll ( k . path )
os . Unsetenv ( "GNUPGHOME" )
}
2021-09-02 09:00:57 +00:00
func setupGPGKeyring ( logcfg loggingConfig , sshKeys [ ] string , parentDir string ) ( * keyring , error ) {
2020-07-12 12:50:55 +00:00
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 }
2021-09-02 09:00:57 +00:00
if err := importSSHKeys ( logcfg , sshKeys , dir ) ; err != nil {
2020-07-12 12:50:55 +00:00
os . RemoveAll ( dir )
return nil , err
}
os . Setenv ( "GNUPGHOME" , dir )
return & k , nil
}
2020-07-19 10:32:59 +00: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 16:09:27 +00:00
var checkMode string
fs . StringVar ( & checkMode , "check-mode" , "off" , ` Validate configuration without installing it (possible values: "manifest","sopsfile","off") ` )
2022-03-29 17:14:06 +00:00
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 10:32:59 +00:00
if err := fs . Parse ( args [ 1 : ] ) ; err != nil {
return nil , err
}
2020-07-19 16:09:27 +00: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 10:32:59 +00:00
if fs . NArg ( ) != 1 {
flag . Usage ( )
return nil , flag . ErrHelp
}
opts . manifest = fs . Arg ( 0 )
return & opts , nil
}
2022-07-04 18:34:57 +00: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
}
2020-07-06 06:30:09 +00:00
func installSecrets ( args [ ] string ) error {
2020-07-19 10:32:59 +00:00
opts , err := parseFlags ( args )
if err != nil {
return err
2020-07-06 06:30:09 +00:00
}
2020-07-19 10:32:59 +00:00
manifest , err := readManifest ( opts . manifest )
2020-07-06 06:30:09 +00:00
if err != nil {
2020-07-12 12:50:55 +00:00
return err
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00:00
if manifest . UserMode {
rundir , ok := os . LookupEnv ( "XDG_RUNTIME_DIR" )
2022-07-08 21:45:38 +00:00
if opts . checkMode == Off && ! ok {
2022-07-04 18:34:57 +00:00
return fmt . Errorf ( "$XDG_RUNTIME_DIR is not set!" )
2022-03-29 17:14:06 +00:00
}
2022-07-04 18:34:57 +00:00
manifest . SecretsMountPoint = replaceRuntimeDir ( manifest . SecretsMountPoint , rundir )
manifest . SymlinkPath = replaceRuntimeDir ( manifest . SymlinkPath , rundir )
2022-03-29 17:14:06 +00:00
var newSecrets [ ] secret
for _ , secret := range manifest . Secrets {
2022-07-04 18:34:57 +00:00
secret . Path = replaceRuntimeDir ( secret . Path , rundir )
2022-03-29 17:14:06 +00:00
newSecrets = append ( newSecrets , secret )
}
manifest . Secrets = newSecrets
}
2020-07-19 16:09:27 +00:00
app := appContext {
2021-10-19 16:26:43 +00:00
manifest : * manifest ,
checkMode : opts . checkMode ,
ignorePasswd : opts . ignorePasswd ,
secretFiles : make ( map [ string ] secretFile ) ,
2020-07-19 16:09:27 +00:00
}
if err := app . validateManifest ( ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Manifest is not valid: %w" , err )
2020-07-06 06:30:09 +00:00
}
2020-07-19 16:09:27 +00:00
if app . checkMode != Off {
2020-07-19 10:32:59 +00:00
return nil
}
2021-10-19 16:26:43 +00:00
var keysGid int
if opts . ignorePasswd {
keysGid = 0
} else {
keysGid , err = lookupKeysGroup ( )
if err != nil {
return err
}
2020-07-06 06:30:09 +00:00
}
2021-09-02 09:00:57 +00:00
isDry := os . Getenv ( "NIXOS_ACTION" ) == "dry-activate"
2022-03-29 17:14:06 +00:00
if err := mountSecretFs ( manifest . SecretsMountPoint , keysGid , manifest . UserMode ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to mount filesystem for secrets: %w" , err )
2020-07-06 06:30:09 +00:00
}
2020-07-12 12:50:55 +00:00
if len ( manifest . SSHKeyPaths ) != 0 {
2021-09-02 09:00:57 +00:00
keyring , err := setupGPGKeyring ( manifest . Logging , manifest . SSHKeyPaths , manifest . SecretsMountPoint )
2020-07-12 12:50:55 +00:00
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Error setting up gpg keyring: %w" , err )
2020-07-12 12:50:55 +00:00
}
defer keyring . Remove ( )
} else if manifest . GnupgHome != "" {
os . Setenv ( "GNUPGHOME" , manifest . GnupgHome )
2021-09-28 12:07:26 +00:00
}
2021-09-30 13:06:06 +00:00
// Import age keys
if len ( manifest . AgeSshKeyPaths ) != 0 || manifest . AgeKeyFile != "" {
2021-09-02 07:18:17 +00:00
keyfile := filepath . Join ( manifest . SecretsMountPoint , "age-keys.txt" )
2021-09-30 13:06:06 +00:00
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 )
2021-09-02 07:18:17 +00:00
if err != nil {
2021-09-30 13:06:06 +00:00
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 {
2022-07-08 22:04:54 +00:00
err = importAgeSSHKeys ( manifest . Logging , manifest . AgeSshKeyPaths , * ageFile )
2021-09-30 13:06:06 +00:00
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 )
}
2021-08-27 18:09:28 +00:00
}
2020-07-06 06:30:09 +00:00
}
2020-07-12 12:50:55 +00:00
if err := decryptSecrets ( manifest . Secrets ) ; err != nil {
return err
2020-07-06 06:30:09 +00:00
}
2020-07-12 12:50:55 +00:00
2022-03-29 17:14:06 +00:00
secretDir , err := prepareSecretsDir ( manifest . SecretsMountPoint , manifest . SymlinkPath , keysGid , manifest . UserMode )
2020-07-06 06:30:09 +00:00
if err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to prepare new secrets directory: %w" , err )
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00:00
if err := writeSecrets ( * secretDir , manifest . Secrets , keysGid , manifest . UserMode ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot write secrets: %w" , err )
2020-07-06 06:30:09 +00:00
}
2022-03-29 17:14:06 +00:00
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 )
}
2021-09-02 09:00:57 +00:00
}
// No need to perform the actual symlinking
if isDry {
return nil
}
2022-03-29 17:14:06 +00:00
if err := symlinkSecrets ( manifest . SymlinkPath , manifest . Secrets , manifest . UserMode ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Failed to prepare symlinks to secret store: %w" , err )
2020-07-12 12:50:55 +00:00
}
2020-07-06 06:30:09 +00:00
if err := atomicSymlink ( * secretDir , manifest . SymlinkPath ) ; err != nil {
2020-07-30 15:19:14 +00:00
return fmt . Errorf ( "Cannot update secrets symlink: %w" , err )
2020-07-06 06:30:09 +00:00
}
2021-11-06 20:29:22 +00:00
if err := pruneGenerations ( manifest . SecretsMountPoint , * secretDir , manifest . KeepGenerations ) ; err != nil {
return fmt . Errorf ( "Cannot prune old secrets generations: %w" , err )
}
2020-07-06 06:30:09 +00:00
return nil
}
func main ( ) {
if err := installSecrets ( os . Args ) ; err != nil {
2020-07-19 10:32:59 +00:00
if err == flag . ErrHelp {
return
}
2020-07-06 06:30:09 +00:00
fmt . Fprintf ( os . Stderr , "%s: %s\n" , os . Args [ 0 ] , err )
os . Exit ( 1 )
}
}