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