mirror of
https://github.com/Mic92/sops-nix.git
synced 2025-12-26 14:14:58 +08:00
parent
a81ce6c961
commit
acaf36a1bf
4 changed files with 314 additions and 23 deletions
35
README.md
35
README.md
|
|
@ -19,6 +19,7 @@ environment variables that can be passed to sops.
|
||||||
- Compatible with all NixOS deployment frameworks: [NixOps](https://github.com/NixOS/nixops), nixos-rebuild, [krops](https://github.com/krebs/krops/), [morph](https://github.com/DBCDK/morph), [nixus](https://github.com/Infinisil/nixus), etc.
|
- Compatible with all NixOS deployment frameworks: [NixOps](https://github.com/NixOS/nixops), nixos-rebuild, [krops](https://github.com/krebs/krops/), [morph](https://github.com/DBCDK/morph), [nixus](https://github.com/Infinisil/nixus), etc.
|
||||||
- Version-control friendly: Since all files are encrypted they can be directly committed to version control without worry. Diffs of the secrets are readable, and [can be shown in cleartext](https://github.com/mozilla/sops#showing-diffs-in-cleartext-in-git).
|
- Version-control friendly: Since all files are encrypted they can be directly committed to version control without worry. Diffs of the secrets are readable, and [can be shown in cleartext](https://github.com/mozilla/sops#showing-diffs-in-cleartext-in-git).
|
||||||
- CI friendly: Since sops files can be added to the Nix store without leaking secrets, a machine definition can be built as a whole from a repository, without needing to rely on external secrets or services.
|
- CI friendly: Since sops files can be added to the Nix store without leaking secrets, a machine definition can be built as a whole from a repository, without needing to rely on external secrets or services.
|
||||||
|
- Home-manager friendly: Provides a home-manager module
|
||||||
- Works well in teams: sops-nix comes with `nix-shell` hooks that allows multiple people to quickly import all GPG keys.
|
- Works well in teams: sops-nix comes with `nix-shell` hooks that allows multiple people to quickly import all GPG keys.
|
||||||
The cryptography used in sops is designed to be scalable: Secrets are only encrypted once with a master key
|
The cryptography used in sops is designed to be scalable: Secrets are only encrypted once with a master key
|
||||||
instead of encrypted per machine/developer key.
|
instead of encrypted per machine/developer key.
|
||||||
|
|
@ -34,7 +35,7 @@ There is a `configuration.nix` example in the [deployment step](#deploy-example)
|
||||||
|
|
||||||
## Supported encryption methods
|
## Supported encryption methods
|
||||||
|
|
||||||
sops-nix supports two basic ways of encryption, GPG and `age`.
|
sops-nix supports two basic ways of encryption, GPG and `age`.
|
||||||
|
|
||||||
GPG is based on [GnuPG](https://gnupg.org/) and encrypts against GPG public keys. Private GPG keys may
|
GPG is based on [GnuPG](https://gnupg.org/) and encrypts against GPG public keys. Private GPG keys may
|
||||||
be used to decrypt the secrets on the target machine. The tool [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) can
|
be used to decrypt the secrets on the target machine. The tool [`ssh-to-pgp`](https://github.com/Mic92/ssh-to-pgp) can
|
||||||
|
|
@ -733,6 +734,38 @@ This is how it can be included in your `configuration.nix`:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Use with home manager
|
||||||
|
|
||||||
|
sops-nix also provides a home-manager module.
|
||||||
|
This module provides a subset of features provided by the system-wide sops-nix since features like the creation of the ramfs and changing the owner of the secrets are not available for non-root users.
|
||||||
|
|
||||||
|
Instead of running as an activation script, sops-nix runs as a systemd user service called `sops-nix.service`.
|
||||||
|
And instead of decrypting to `/run/secrets`, the secrets are decrypted to `$XDG_RUNTIME_DIR/secrets`.
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
# NixOS home-manager configuration
|
||||||
|
home-manager.sharedModules = [
|
||||||
|
/path/to/sops-nix/modules/home-manager/sops.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Configuration via home.nix
|
||||||
|
imports = [
|
||||||
|
/path/to/sops-nix/modules/home-manager/sops.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Configuration of secrets
|
||||||
|
sops = {
|
||||||
|
age.sshKeyPaths = [ "/home/user/path-to-ssh-key" ]; # must have no password!
|
||||||
|
sops.secrets.test = {
|
||||||
|
sopsFile = ./secrets.yml.enc;
|
||||||
|
path = "%r/test.txt"; # %r gets replaced with your $XDG_RUNTIME_DIR
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Use with GPG instead of SSH keys
|
## Use with GPG instead of SSH keys
|
||||||
|
|
||||||
If you prefer having a separate GPG key, sops-nix also comes with a helper tool, `sops-init-gpg-key`:
|
If you prefer having a separate GPG key, sops-nix also comes with a helper tool, `sops-init-gpg-key`:
|
||||||
|
|
|
||||||
226
modules/home-manager/sops.nix
Normal file
226
modules/home-manager/sops.nix
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.sops;
|
||||||
|
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets;
|
||||||
|
secretType = lib.types.submodule ({ config, name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = ''
|
||||||
|
Name of the file used in /run/user/*/secrets
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
key = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = name;
|
||||||
|
description = ''
|
||||||
|
Key used to lookup in the sops file.
|
||||||
|
No tested data structures are supported right now.
|
||||||
|
This option is ignored if format is binary.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "%r/secrets/${name}";
|
||||||
|
description = ''
|
||||||
|
Path where secrets are symlinked to.
|
||||||
|
If the default is kept no symlink is created.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
format = lib.mkOption {
|
||||||
|
type = lib.types.enum [ "yaml" "json" "binary" ];
|
||||||
|
default = cfg.defaultSopsFormat;
|
||||||
|
description = ''
|
||||||
|
File format used to decrypt the sops secret.
|
||||||
|
Binary files are written to the target file as is.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mode = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0400";
|
||||||
|
description = ''
|
||||||
|
Permissions mode of the in octal.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
sopsFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = cfg.defaultSopsFile;
|
||||||
|
defaultText = "\${config.sops.defaultSopsFile}";
|
||||||
|
description = ''
|
||||||
|
Sops file the secret is loaded from.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
manifestFor = suffix: secrets: pkgs.writeTextFile {
|
||||||
|
name = "manifest${suffix}.json";
|
||||||
|
text = builtins.toJSON {
|
||||||
|
secrets = builtins.attrValues secrets;
|
||||||
|
secretsMountPoint = "%r/secrets.d";
|
||||||
|
symlinkPath = "%r/secrets";
|
||||||
|
keepGenerations = cfg.keepGenerations;
|
||||||
|
gnupgHome = cfg.gnupg.home;
|
||||||
|
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||||
|
ageKeyFile = cfg.age.keyFile;
|
||||||
|
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||||
|
userMode = true;
|
||||||
|
logging = {
|
||||||
|
keyImport = builtins.elem "keyImport" cfg.log;
|
||||||
|
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
checkPhase = ''
|
||||||
|
${sops-install-secrets}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
manifest = manifestFor "" cfg.secrets;
|
||||||
|
in {
|
||||||
|
options.sops = {
|
||||||
|
secrets = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf secretType;
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Secrets to decrypt.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultSopsFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = ''
|
||||||
|
Default sops file used for all secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultSopsFormat = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "yaml";
|
||||||
|
description = ''
|
||||||
|
Default sops format used for all secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
validateSopsFiles = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Check all sops files at evaluation time.
|
||||||
|
This requires sops files to be added to the nix store.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
keepGenerations = lib.mkOption {
|
||||||
|
type = lib.types.ints.unsigned;
|
||||||
|
default = 1;
|
||||||
|
description = ''
|
||||||
|
Number of secrets generations to keep. Setting this to 0 disables pruning.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
log = lib.mkOption {
|
||||||
|
type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]);
|
||||||
|
default = [ "keyImport" "secretChanges" ];
|
||||||
|
description = "What to log";
|
||||||
|
};
|
||||||
|
|
||||||
|
age = {
|
||||||
|
keyFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/var/lib/sops-nix/key.txt";
|
||||||
|
description = ''
|
||||||
|
Path to age key file used for sops decryption.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
generateKey = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether or not to generate the age key. If this
|
||||||
|
option is set to false, the key must already be
|
||||||
|
present at the specified location.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
sshKeyPaths = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.path;
|
||||||
|
default = [];
|
||||||
|
defaultText = lib.literalDocBook "The ed25519 keys from <option>config.services.openssh.hostKeys</option>";
|
||||||
|
description = ''
|
||||||
|
Paths to ssh keys added as age keys during sops description.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
gnupg = {
|
||||||
|
home = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/root/.gnupg";
|
||||||
|
description = ''
|
||||||
|
Path to gnupg database directory containing the key for decrypting the sops file.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
sshKeyPaths = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.path;
|
||||||
|
default = [];
|
||||||
|
defaultText = lib.literalDocBook "The rsa keys from <option>config.services.openssh.hostKeys</option>";
|
||||||
|
description = ''
|
||||||
|
Path to ssh keys added as GPG keys during sops description.
|
||||||
|
This option must be explicitly unset if <literal>config.sops.gnupg.sshKeyPaths</literal> is set.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (cfg.secrets != {}) {
|
||||||
|
assertions = [{
|
||||||
|
assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != [];
|
||||||
|
message = "No key source configurated for sops";
|
||||||
|
} {
|
||||||
|
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []);
|
||||||
|
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set";
|
||||||
|
}] ++ lib.optionals cfg.validateSopsFiles (
|
||||||
|
lib.concatLists (lib.mapAttrsToList (name: secret: [{
|
||||||
|
assertion = builtins.pathExists secret.sopsFile;
|
||||||
|
message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile";
|
||||||
|
} {
|
||||||
|
assertion =
|
||||||
|
builtins.isPath secret.sopsFile ||
|
||||||
|
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile);
|
||||||
|
message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false";
|
||||||
|
}]) cfg.secrets)
|
||||||
|
);
|
||||||
|
|
||||||
|
systemd.user.services.sops-nix = {
|
||||||
|
Unit = {
|
||||||
|
Description = "sops-nix activation";
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
Environment = lib.mkIf (cfg.gnupg.home != null) [ "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg" ];
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = toString (pkgs.writeShellScript "sops-nix-user" (lib.optionalString cfg.age.generateKey ''
|
||||||
|
if [[ ! -f '${cfg.age.keyFile}' ]]; then
|
||||||
|
echo generating machine-specific age key...
|
||||||
|
mkdir -p $(dirname ${cfg.age.keyFile})
|
||||||
|
# age-keygen sets 0600 by default, no need to chmod.
|
||||||
|
${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile}
|
||||||
|
fi
|
||||||
|
'' + ''
|
||||||
|
${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd '${manifest}'
|
||||||
|
''));
|
||||||
|
};
|
||||||
|
Install.WantedBy = [ "default.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -126,6 +126,7 @@ let
|
||||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||||
ageKeyFile = cfg.age.keyFile;
|
ageKeyFile = cfg.age.keyFile;
|
||||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||||
|
userMode = false;
|
||||||
logging = {
|
logging = {
|
||||||
keyImport = builtins.elem "keyImport" cfg.log;
|
keyImport = builtins.elem "keyImport" cfg.log;
|
||||||
secretChanges = builtins.elem "secretChanges" cfg.log;
|
secretChanges = builtins.elem "secretChanges" cfg.log;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ type manifest struct {
|
||||||
GnupgHome string `json:"gnupgHome"`
|
GnupgHome string `json:"gnupgHome"`
|
||||||
AgeKeyFile string `json:"ageKeyFile"`
|
AgeKeyFile string `json:"ageKeyFile"`
|
||||||
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
|
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
|
||||||
|
UserMode bool `json:"userMode"`
|
||||||
Logging loggingConfig `json:"logging"`
|
Logging loggingConfig `json:"logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,15 +180,17 @@ func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, secret *secr
|
||||||
return linkTarget == targetFile && validUG
|
return linkTarget == targetFile && validUG
|
||||||
}
|
}
|
||||||
|
|
||||||
func symlinkSecret(targetFile string, secret *secret) error {
|
func symlinkSecret(targetFile string, secret *secret, userMode bool) error {
|
||||||
for {
|
for {
|
||||||
stat, err := os.Lstat(secret.Path)
|
stat, err := os.Lstat(secret.Path)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.Symlink(targetFile, secret.Path); err != nil {
|
if err := os.Symlink(targetFile, secret.Path); err != nil {
|
||||||
return fmt.Errorf("Cannot create symlink '%s': %w", secret.Path, err)
|
return fmt.Errorf("Cannot create symlink '%s': %w", secret.Path, err)
|
||||||
}
|
}
|
||||||
if err := secureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil {
|
if !userMode {
|
||||||
return fmt.Errorf("Cannot chown symlink '%s': %w", secret.Path, err)
|
if err := secureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil {
|
||||||
|
return fmt.Errorf("Cannot chown symlink '%s': %w", secret.Path, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
@ -209,7 +212,7 @@ func symlinkSecret(targetFile string, secret *secret) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func symlinkSecrets(targetDir string, secrets []secret) error {
|
func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error {
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
targetFile := filepath.Join(targetDir, secret.Name)
|
targetFile := filepath.Join(targetDir, secret.Name)
|
||||||
if targetFile == secret.Path {
|
if targetFile == secret.Path {
|
||||||
|
|
@ -219,7 +222,7 @@ func symlinkSecrets(targetDir string, secrets []secret) error {
|
||||||
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
|
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
|
||||||
return fmt.Errorf("Cannot create parent directory of '%s': %w", secret.Path, err)
|
return fmt.Errorf("Cannot create parent directory of '%s': %w", secret.Path, err)
|
||||||
}
|
}
|
||||||
if err := symlinkSecret(targetFile, &secret); err != nil {
|
if err := symlinkSecret(targetFile, &secret, userMode); err != nil {
|
||||||
return fmt.Errorf("Failed to symlink secret '%s': %w", secret.Path, err)
|
return fmt.Errorf("Failed to symlink secret '%s': %w", secret.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,11 +331,16 @@ func decryptSecrets(secrets []secret) error {
|
||||||
|
|
||||||
const RAMFS_MAGIC int32 = -2054924042
|
const RAMFS_MAGIC int32 = -2054924042
|
||||||
|
|
||||||
func mountSecretFs(mountpoint string, keysGid int) error {
|
func mountSecretFs(mountpoint string, keysGid int, userMode bool) error {
|
||||||
if err := os.MkdirAll(mountpoint, 0751); err != nil {
|
if err := os.MkdirAll(mountpoint, 0751); err != nil {
|
||||||
return fmt.Errorf("Cannot create directory '%s': %w", mountpoint, err)
|
return fmt.Errorf("Cannot create directory '%s': %w", mountpoint, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We can't create a ramfs as user
|
||||||
|
if userMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
buf := unix.Statfs_t{}
|
buf := unix.Statfs_t{}
|
||||||
if err := unix.Statfs(mountpoint, &buf); err != nil {
|
if err := unix.Statfs(mountpoint, &buf); err != nil {
|
||||||
return fmt.Errorf("Cannot get statfs for directory '%s': %w", mountpoint, err)
|
return fmt.Errorf("Cannot get statfs for directory '%s': %w", mountpoint, err)
|
||||||
|
|
@ -350,7 +358,7 @@ func mountSecretFs(mountpoint string, keysGid int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int) (*string, error) {
|
func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int, userMode bool) (*string, error) {
|
||||||
var generation uint64
|
var generation uint64
|
||||||
linkTarget, err := os.Readlink(linkName)
|
linkTarget, err := os.Readlink(linkName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -374,13 +382,15 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGid int) (*
|
||||||
if err := os.Mkdir(dir, os.FileMode(0751)); err != nil {
|
if err := os.Mkdir(dir, os.FileMode(0751)); err != nil {
|
||||||
return nil, fmt.Errorf("mkdir(): %w", err)
|
return nil, fmt.Errorf("mkdir(): %w", err)
|
||||||
}
|
}
|
||||||
if err := os.Chown(dir, 0, int(keysGid)); err != nil {
|
if !userMode {
|
||||||
return nil, fmt.Errorf("Cannot change owner/group of '%s' to 0/%d: %w", dir, keysGid, err)
|
if err := os.Chown(dir, 0, int(keysGid)); err != nil {
|
||||||
|
return nil, fmt.Errorf("Cannot change owner/group of '%s' to 0/%d: %w", dir, keysGid, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &dir, nil
|
return &dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSecrets(secretDir string, secrets []secret, keysGid int) error {
|
func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool) error {
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
fp := filepath.Join(secretDir, secret.Name)
|
fp := filepath.Join(secretDir, secret.Name)
|
||||||
|
|
||||||
|
|
@ -391,16 +401,20 @@ func writeSecrets(secretDir string, secrets []secret, keysGid int) error {
|
||||||
if err := os.MkdirAll(pathSoFar, 0751); err != nil {
|
if err := os.MkdirAll(pathSoFar, 0751); err != nil {
|
||||||
return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, fp, err)
|
return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, fp, err)
|
||||||
}
|
}
|
||||||
if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil {
|
if !userMode {
|
||||||
return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, fp, err)
|
if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil {
|
||||||
|
return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, fp, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(fp, []byte(secret.value), secret.mode); err != nil {
|
if err := ioutil.WriteFile(fp, []byte(secret.value), secret.mode); err != nil {
|
||||||
return fmt.Errorf("Cannot write %s: %w", fp, err)
|
return fmt.Errorf("Cannot write %s: %w", fp, err)
|
||||||
}
|
}
|
||||||
if err := os.Chown(fp, secret.owner, secret.group); err != nil {
|
if !userMode {
|
||||||
return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err)
|
if err := os.Chown(fp, secret.owner, secret.group); err != nil {
|
||||||
|
return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -488,7 +502,7 @@ func (app *appContext) validateSecret(secret *secret) error {
|
||||||
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" {
|
||||||
secret.owner = 0
|
secret.owner = 0
|
||||||
secret.group = 0
|
secret.group = 0
|
||||||
} else if app.checkMode == Off {
|
} else if app.checkMode == Off || app.ignorePasswd {
|
||||||
// we only access to the user/group during deployment
|
// we only access to the user/group during deployment
|
||||||
owner, err := user.Lookup(secret.Owner)
|
owner, err := user.Lookup(secret.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -873,7 +887,7 @@ func parseFlags(args []string) (*options, error) {
|
||||||
}
|
}
|
||||||
var checkMode string
|
var checkMode string
|
||||||
fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`)
|
fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`)
|
||||||
fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root`)
|
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`)
|
||||||
if err := fs.Parse(args[1:]); err != nil {
|
if err := fs.Parse(args[1:]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -904,6 +918,21 @@ func installSecrets(args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if manifest.UserMode {
|
||||||
|
rundir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
|
||||||
|
if !ok {
|
||||||
|
rundir = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||||
|
}
|
||||||
|
manifest.SecretsMountPoint = strings.ReplaceAll(manifest.SecretsMountPoint, "%r", rundir)
|
||||||
|
manifest.SymlinkPath = strings.ReplaceAll(manifest.SymlinkPath, "%r", rundir)
|
||||||
|
var newSecrets []secret
|
||||||
|
for _, secret := range manifest.Secrets {
|
||||||
|
secret.Path = strings.ReplaceAll(secret.Path, "%r", rundir)
|
||||||
|
newSecrets = append(newSecrets, secret)
|
||||||
|
}
|
||||||
|
manifest.Secrets = newSecrets
|
||||||
|
}
|
||||||
|
|
||||||
app := appContext{
|
app := appContext{
|
||||||
manifest: *manifest,
|
manifest: *manifest,
|
||||||
checkMode: opts.checkMode,
|
checkMode: opts.checkMode,
|
||||||
|
|
@ -931,7 +960,7 @@ func installSecrets(args []string) error {
|
||||||
|
|
||||||
isDry := os.Getenv("NIXOS_ACTION") == "dry-activate"
|
isDry := os.Getenv("NIXOS_ACTION") == "dry-activate"
|
||||||
|
|
||||||
if err := mountSecretFs(manifest.SecretsMountPoint, keysGid); err != nil {
|
if err := mountSecretFs(manifest.SecretsMountPoint, keysGid, manifest.UserMode); err != nil {
|
||||||
return fmt.Errorf("Failed to mount filesystem for secrets: %w", err)
|
return fmt.Errorf("Failed to mount filesystem for secrets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -983,21 +1012,23 @@ func installSecrets(args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid)
|
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid, manifest.UserMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to prepare new secrets directory: %w", err)
|
return fmt.Errorf("Failed to prepare new secrets directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := writeSecrets(*secretDir, manifest.Secrets, keysGid); err != nil {
|
if err := writeSecrets(*secretDir, manifest.Secrets, keysGid, manifest.UserMode); err != nil {
|
||||||
return fmt.Errorf("Cannot write secrets: %w", err)
|
return fmt.Errorf("Cannot write secrets: %w", err)
|
||||||
}
|
}
|
||||||
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil {
|
if !manifest.UserMode {
|
||||||
return fmt.Errorf("Cannot request units to restart: %w", err)
|
if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil {
|
||||||
|
return fmt.Errorf("Cannot request units to restart: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No need to perform the actual symlinking
|
// No need to perform the actual symlinking
|
||||||
if isDry {
|
if isDry {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets); err != nil {
|
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.UserMode); err != nil {
|
||||||
return fmt.Errorf("Failed to prepare symlinks to secret store: %w", err)
|
return fmt.Errorf("Failed to prepare symlinks to secret store: %w", err)
|
||||||
}
|
}
|
||||||
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
|
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue