nixos: migrate to secretsmgr for sshd and ACME
This commit is contained in:
parent
702cd972ab
commit
b719181dfe
18 changed files with 309 additions and 264 deletions
|
@ -37,6 +37,7 @@ var (
|
||||||
sshHostKeyPrincipals = flag.String("ssh_host_key_principals", defaultPrincipals(hostname()), "Principals to use for SSH host key certificate")
|
sshHostKeyPrincipals = flag.String("ssh_host_key_principals", defaultPrincipals(hostname()), "Principals to use for SSH host key certificate")
|
||||||
sshHostKeyExpiryThreshold = flag.Duration("ssh_host_key_expiry_threshold", 3*24*time.Hour, "Expiry threshold for SSH host key")
|
sshHostKeyExpiryThreshold = flag.Duration("ssh_host_key_expiry_threshold", 3*24*time.Hour, "Expiry threshold for SSH host key")
|
||||||
sshHostKeyOutputDir = flag.String("ssh_host_key_output_dir", "/var/lib/secretsmgr/ssh", "Path to SSH host key output dir")
|
sshHostKeyOutputDir = flag.String("ssh_host_key_output_dir", "/var/lib/secretsmgr/ssh", "Path to SSH host key output dir")
|
||||||
|
sshDummyHostKey = flag.String("ssh_dummy_host_key", "", "Path to dummy SSH hostkey")
|
||||||
sshd = flag.String("sshd", "sshd", "Path to sshd")
|
sshd = flag.String("sshd", "sshd", "Path to sshd")
|
||||||
|
|
||||||
acmeCertificatesConfig = flag.String("acme_certificates_config", "", "Path to ACME certificates config")
|
acmeCertificatesConfig = flag.String("acme_certificates_config", "", "Path to ACME certificates config")
|
||||||
|
@ -58,7 +59,11 @@ func defaultPrincipals(hostname string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshHostKeyPaths(ctx context.Context) ([]string, error) {
|
func sshHostKeyPaths(ctx context.Context) ([]string, error) {
|
||||||
out, err := exec.CommandContext(ctx, *sshd, "-T").Output()
|
args := []string{"-T"}
|
||||||
|
if *sshDummyHostKey != "" {
|
||||||
|
args = append(args, "-h", *sshDummyHostKey)
|
||||||
|
}
|
||||||
|
out, err := exec.CommandContext(ctx, *sshd, args...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +74,11 @@ func sshHostKeyPaths(ctx context.Context) ([]string, error) {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
t := scanner.Text()
|
t := scanner.Text()
|
||||||
if strings.HasPrefix(t, hostkeyPrefix) {
|
if strings.HasPrefix(t, hostkeyPrefix) {
|
||||||
keys = append(keys, fmt.Sprintf("%s.pub", strings.TrimPrefix(t, hostkeyPrefix)))
|
key := strings.TrimPrefix(t, hostkeyPrefix)
|
||||||
|
if key == *sshDummyHostKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, fmt.Sprintf("%s.pub", key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if scanner.Err() != nil {
|
if scanner.Err() != nil {
|
||||||
|
@ -226,7 +235,7 @@ func shouldRenewSSHCert(certPath string, publicKey []byte, principals string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadOrTryRestartServices(ctx context.Context, services []string) error {
|
func reloadOrTryRestartServices(ctx context.Context, services []string) error {
|
||||||
sd, err := dbus.NewSystemdConnection()
|
sd, err := dbus.NewWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -352,7 +361,9 @@ func setGroup(groupName string) (func(), error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
origGid := unix.Getegid()
|
origGid := unix.Getegid()
|
||||||
unix.Setregid(-1, gid)
|
if err := unix.Setregid(-1, gid); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return func() {
|
return func() {
|
||||||
unix.Setregid(-1, origGid)
|
unix.Setregid(-1, origGid)
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -514,6 +525,18 @@ func writeCertificate(certDef acmeCertificate, cert *vapi.Secret) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uniqueStrings(vs []string) []string {
|
||||||
|
m := map[string]bool{}
|
||||||
|
for _, v := range vs {
|
||||||
|
m[v] = true
|
||||||
|
}
|
||||||
|
x := make([]string, 0, len(m))
|
||||||
|
for v := range m {
|
||||||
|
x = append(x, v)
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
||||||
certDefs, err := parseACMECertificates()
|
certDefs, err := parseACMECertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -523,6 +546,7 @@ func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
||||||
log.Infof("acme certificate defs: %#v", certDefs)
|
log.Infof("acme certificate defs: %#v", certDefs)
|
||||||
|
|
||||||
var hadErr bool
|
var hadErr bool
|
||||||
|
var unitsToReloadOrRestart []string
|
||||||
for _, certDef := range certDefs {
|
for _, certDef := range certDefs {
|
||||||
log.Infof("maybe issuing %v (role: %v) with hostnames %v into %v", certDef.OutputName, certDef.Role, certDef.Hostnames, certDef.OutputDir())
|
log.Infof("maybe issuing %v (role: %v) with hostnames %v into %v", certDef.OutputName, certDef.Role, certDef.Hostnames, certDef.OutputDir())
|
||||||
|
|
||||||
|
@ -540,7 +564,7 @@ func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := makeOrFixPermissionsOnDir(certDef.OutputDir(), 0750, certDef.Group); err != nil {
|
if err := makeOrFixPermissionsOnDir(certDef.OutputDir(), 0750, certDef.Group); err != nil {
|
||||||
log.Errorf("creating/fixing dir %q: %w", certDef.OutputDir(), err)
|
log.Errorf("creating/fixing dir %q: %v", certDef.OutputDir(), err)
|
||||||
hadErr = true
|
hadErr = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -565,6 +589,16 @@ func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
||||||
hadErr = true
|
hadErr = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unitsToReloadOrRestart = append(unitsToReloadOrRestart, certDef.UnitsToReloadOrRestart...)
|
||||||
|
}
|
||||||
|
unitsToReloadOrRestart = uniqueStrings(unitsToReloadOrRestart)
|
||||||
|
if len(unitsToReloadOrRestart) > 0 {
|
||||||
|
log.Infof("reloading/restarting %d units: %v", len(unitsToReloadOrRestart), unitsToReloadOrRestart)
|
||||||
|
if err := reloadOrTryRestartServices(ctx, unitsToReloadOrRestart); err != nil {
|
||||||
|
log.Errorf("failed to restart units: %v", err)
|
||||||
|
hadErr = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hadErr
|
return hadErr
|
||||||
|
|
|
@ -82,7 +82,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates."objdump.zxcvbnm.ninja" = {
|
my.vault.acmeCertificates."objdump.zxcvbnm.ninja" = {
|
||||||
extraNames = [ "*.objdump.zxcvbnm.ninja" ];
|
hostnames = [ "objdump.zxcvbnm.ninja" "*.objdump.zxcvbnm.ninja" ];
|
||||||
nginxVirtualHosts = [ "objdump.zxcvbnm.ninja" ];
|
nginxVirtualHosts = [ "objdump.zxcvbnm.ninja" ];
|
||||||
};
|
};
|
||||||
my.fup.listen = [
|
my.fup.listen = [
|
||||||
|
|
|
@ -167,7 +167,7 @@ in {
|
||||||
|
|
||||||
my.vault.acmeCertificates."matrix.zxcvbnm.ninja" = {
|
my.vault.acmeCertificates."matrix.zxcvbnm.ninja" = {
|
||||||
group = "matrixcert";
|
group = "matrixcert";
|
||||||
extraNames = [ "element.zxcvbnm.ninja" "zxcvbnm.ninja" ];
|
hostnames = [ "matrix.zxcvbnm.ninja" "element.zxcvbnm.ninja" "zxcvbnm.ninja" ];
|
||||||
nginxVirtualHosts = [ "zxcvbnm.ninja" "element.zxcvbnm.ninja" "matrix.zxcvbnm.ninja" ];
|
nginxVirtualHosts = [ "zxcvbnm.ninja" "element.zxcvbnm.ninja" "matrix.zxcvbnm.ninja" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -93,10 +93,13 @@ in {
|
||||||
my.vault.acmeCertificates = {
|
my.vault.acmeCertificates = {
|
||||||
"xmpp.lukegb.com" = {
|
"xmpp.lukegb.com" = {
|
||||||
group = "prosody";
|
group = "prosody";
|
||||||
extraNames = [ "*.xmpp.lukegb.com" "lukegb.com" ];
|
hostnames = [ "xmpp.lukegb.com" "*.xmpp.lukegb.com" "lukegb.com" ];
|
||||||
|
reloadOrRestartUnits = [ "prosody.service" ];
|
||||||
};
|
};
|
||||||
"turn.lukegb.com" = {
|
"turn.lukegb.com" = {
|
||||||
group = "turnserver";
|
group = "turnserver";
|
||||||
|
hostnames = [ "turn.lukegb.com" ];
|
||||||
|
reloadOrRestartUnits = [ "coturn.service" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ in {
|
||||||
|
|
||||||
my.vault.acmeCertificates."as205479.net" = {
|
my.vault.acmeCertificates."as205479.net" = {
|
||||||
group = "acme";
|
group = "acme";
|
||||||
|
hostnames = [ "as205479.net" ];
|
||||||
reloadOrRestartUnits = [ "freeradius.service" ];
|
reloadOrRestartUnits = [ "freeradius.service" ];
|
||||||
};
|
};
|
||||||
users.users.nginx.extraGroups = lib.mkAfter [ "acme" ];
|
users.users.nginx.extraGroups = lib.mkAfter [ "acme" ];
|
||||||
|
|
|
@ -193,7 +193,7 @@ in {
|
||||||
useLegacyConfig = false;
|
useLegacyConfig = false;
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates."znc.lukegb.com" = {
|
my.vault.acmeCertificates."znc.lukegb.com" = {
|
||||||
extraNames = [ "akiichiro.lukegb.com" ];
|
hostnames = [ "znc.lukegb.com" "akiichiro.lukegb.com" ];
|
||||||
group = "znc-acme";
|
group = "znc-acme";
|
||||||
};
|
};
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
|
@ -354,7 +354,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates."lukegb.com" = {
|
my.vault.acmeCertificates."lukegb.com" = {
|
||||||
extraNames = [
|
hostnames = [
|
||||||
"lukegb.com"
|
"lukegb.com"
|
||||||
"*.lukegb.com"
|
"*.lukegb.com"
|
||||||
"*.int.lukegb.com"
|
"*.int.lukegb.com"
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates."as205479.net" = {
|
my.vault.acmeCertificates."as205479.net" = {
|
||||||
role = "letsencrypt-gcloud-as205479";
|
role = "letsencrypt-gcloud-as205479";
|
||||||
extraNames = [ "www.as205479.net" ];
|
hostnames = [ "as205479.net" "www.as205479.net" ];
|
||||||
nginxVirtualHosts = [ "as205479.net" ];
|
nginxVirtualHosts = [ "as205479.net" ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,7 +189,7 @@ in
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates = {
|
my.vault.acmeCertificates = {
|
||||||
"baserow.lukegb.com" = {
|
"baserow.lukegb.com" = {
|
||||||
extraNames = [ "api.baserow.lukegb.com" "baserow-media.zxcvbnm.ninja" ];
|
hostnames = [ "baserow.lukegb.com" "api.baserow.lukegb.com" "baserow-media.zxcvbnm.ninja" ];
|
||||||
nginxVirtualHosts = [ "baserow.lukegb.com" "api.baserow.lukegb.com" "baserow-media.zxcvbnm.ninja" ];
|
nginxVirtualHosts = [ "baserow.lukegb.com" "api.baserow.lukegb.com" "baserow-media.zxcvbnm.ninja" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,8 +18,9 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
../../../third_party/home-manager/nixos
|
../../../third_party/home-manager/nixos
|
||||||
./vault-agent.nix
|
./vault-agent.nix
|
||||||
./vault-agent-acme.nix
|
|
||||||
./vault-agent-secrets.nix
|
./vault-agent-secrets.nix
|
||||||
|
./secretsmgr.nix
|
||||||
|
./secretsmgr-acme.nix
|
||||||
./ssh-ca-vault.nix
|
./ssh-ca-vault.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ in
|
||||||
}) config.my.fup.listen);
|
}) config.my.fup.listen);
|
||||||
in {
|
in {
|
||||||
my.vault.acmeCertificates."p.lukegb.com" = {
|
my.vault.acmeCertificates."p.lukegb.com" = {
|
||||||
|
hostnames = [ "p.lukegb.com" ];
|
||||||
nginxVirtualHosts = [ "p.lukegb.com" ];
|
nginxVirtualHosts = [ "p.lukegb.com" ];
|
||||||
};
|
};
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
|
@ -25,8 +25,8 @@ in
|
||||||
ssl = true;
|
ssl = true;
|
||||||
}) config.my.quotesdb.listen);
|
}) config.my.quotesdb.listen);
|
||||||
in {
|
in {
|
||||||
my.vault.acmeCertificates."bfob.gg" = {
|
my.vault.acmeCertificates.bfob = {
|
||||||
extraNames = [ "*.bfob.gg" ];
|
hostnames = [ "bfob.gg" "*.bfob.gg" ];
|
||||||
nginxVirtualHosts = [ "qdb.bfob.gg" "quotes.bfob.gg" "dev-quotes.bfob.gg" ];
|
nginxVirtualHosts = [ "qdb.bfob.gg" "quotes.bfob.gg" "dev-quotes.bfob.gg" ];
|
||||||
};
|
};
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
93
ops/nixos/lib/secretsmgr-acme.nix
Normal file
93
ops/nixos/lib/secretsmgr-acme.nix
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ pkgs, config, depot, lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption types mkBefore optionalAttrs mkDefault mkIf mapAttrsToList mkAfter;
|
||||||
|
|
||||||
|
acmeCertificates = builtins.attrValues config.my.vault.acmeCertificates;
|
||||||
|
|
||||||
|
# Work out where we're being asked to write things, and which groups, so we can correctly get permissions.
|
||||||
|
fullchainPath = c: pathFor c "fullchain.pem";
|
||||||
|
chainPath = c: pathFor c "chain.pem";
|
||||||
|
keyPath = c: pathFor c "privkey.pem";
|
||||||
|
pathFor = c: suffix: "/var/lib/acme/${c.name}/${suffix}";
|
||||||
|
|
||||||
|
isNginx = c: builtins.length c.nginxVirtualHosts > 0;
|
||||||
|
defaultGroup = c: if isNull c.group then if isNginx c then "nginx" else "acme" else c.group;
|
||||||
|
groupOrDefault = p: c: if isNull p then defaultGroup c else p;
|
||||||
|
|
||||||
|
allRestartableUnits = builtins.concatMap (c: c.reloadOrRestartUnits) acmeCertificates;
|
||||||
|
allGroups = lib.unique (map (c: c.group) acmeCertificates);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./secretsmgr.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options.my.vault.acmeCertificates = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
||||||
|
options = let
|
||||||
|
isNginx = builtins.length config.nginxVirtualHosts > 0;
|
||||||
|
in {
|
||||||
|
name = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = name;
|
||||||
|
description = "Path to put the certificate.";
|
||||||
|
};
|
||||||
|
|
||||||
|
nginxVirtualHosts = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
default = [];
|
||||||
|
description = "List of nginx virtual hosts to apply SSL to.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = if isNginx then "nginx" else "acme";
|
||||||
|
description = "Owner group to set for the ${what}. If null, taken from parent.";
|
||||||
|
};
|
||||||
|
|
||||||
|
role = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "letsencrypt-cloudflare";
|
||||||
|
description = "Which role to use for certificate issuance.";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostnames = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of hostnames to issue certificate for.";
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadOrRestartUnits = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
default = lib.optional isNginx "nginx.service";
|
||||||
|
description = "List of systemd units to reload/restart after obtaining a new certificate.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf config.my.vault.secretsmgr.acmeCertificates.enable {
|
||||||
|
services.nginx = optionalAttrs config.my.vault.enable {
|
||||||
|
virtualHosts = builtins.listToAttrs (builtins.concatMap (certData: let
|
||||||
|
fullchain = fullchainPath certData;
|
||||||
|
chain = chainPath certData;
|
||||||
|
key = keyPath certData;
|
||||||
|
in map (hostName: lib.nameValuePair hostName {
|
||||||
|
sslCertificate = mkDefault (fullchainPath certData);
|
||||||
|
sslCertificateKey = mkDefault (keyPath certData);
|
||||||
|
sslTrustedCertificate = mkDefault (chainPath certData);
|
||||||
|
}) certData.nginxVirtualHosts) acmeCertificates);
|
||||||
|
};
|
||||||
|
|
||||||
|
my.vault.secretsmgr.groups = mkAfter allGroups;
|
||||||
|
my.vault.secretsmgr.restartableUnits = mkAfter allRestartableUnits;
|
||||||
|
my.vault.secretsmgr.acmeCertificates.config = mapAttrsToList (_: c: {
|
||||||
|
inherit (c) group hostnames role;
|
||||||
|
output_name = c.name;
|
||||||
|
units_to_reload_or_restart = c.reloadOrRestartUnits;
|
||||||
|
}) config.my.vault.acmeCertificates;
|
||||||
|
};
|
||||||
|
}
|
155
ops/nixos/lib/secretsmgr.nix
Normal file
155
ops/nixos/lib/secretsmgr.nix
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
{ depot, pkgs, config, lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkIf mkOption types concatStringsSep unique optional mkAfter;
|
||||||
|
|
||||||
|
pkg = depot.go.secretsmgr;
|
||||||
|
cfg = config.my.vault.secretsmgr;
|
||||||
|
|
||||||
|
jsonFormat = pkgs.formats.json { };
|
||||||
|
|
||||||
|
dummyHostKey = pkgs.runCommandLocal "dummy-host-key" {
|
||||||
|
nativeBuildInputs = [ config.programs.ssh.package ];
|
||||||
|
} ''
|
||||||
|
ssh-keygen -q -f $out -N ""
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.my.vault.secretsmgr = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = config.my.vault.enable;
|
||||||
|
};
|
||||||
|
groups = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
restartableUnits = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
acmeCertificates.enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
acmeCertificates.expiryThreshold = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "744h";
|
||||||
|
};
|
||||||
|
acmeCertificates.config = mkOption {
|
||||||
|
type = jsonFormat.type;
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
|
acmeCertificates.mount = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "acme";
|
||||||
|
};
|
||||||
|
|
||||||
|
sshCertificates.enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
sshCertificates.mount = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "ssh-host";
|
||||||
|
};
|
||||||
|
sshCertificates.expiryThreshold = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "72h";
|
||||||
|
};
|
||||||
|
sshCertificates.outputDir = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/var/lib/secretsmgr/ssh";
|
||||||
|
};
|
||||||
|
sshCertificates.principals = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = let inherit (config.networking) hostName; in [
|
||||||
|
"${hostName}.as205479.net"
|
||||||
|
"${hostName}.int.as205479.net"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
sshCertificates.role = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = config.networking.hostName;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
my.vault.secretsmgr.restartableUnits = mkIf cfg.sshCertificates.enable (mkAfter ["sshd.service"]);
|
||||||
|
|
||||||
|
users.groups.secretsmgr = {};
|
||||||
|
users.users.secretsmgr = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "secretsmgr";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.secretsmgr = {
|
||||||
|
requires = ["vault-agent.service"];
|
||||||
|
after = ["vault-agent.service"];
|
||||||
|
restartIfChanged = true;
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
User = "secretsmgr";
|
||||||
|
Group = "secretsmgr";
|
||||||
|
SupplementaryGroups = cfg.groups;
|
||||||
|
AmbientCapabilities = [ "CAP_SETGID" ];
|
||||||
|
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = concatStringsSep " " ([
|
||||||
|
"${pkg}/bin/secretsmgr"
|
||||||
|
"--logtostderr"
|
||||||
|
] ++ lib.optionals cfg.acmeCertificates.enable [
|
||||||
|
"--acme_certificates_config=${jsonFormat.generate "secretsmgr-acme-certificates-config.json" cfg.acmeCertificates.config}"
|
||||||
|
"--acme_certificates_expiry_threshold=${cfg.acmeCertificates.expiryThreshold}"
|
||||||
|
"--acme_certificates_mount=${cfg.acmeCertificates.mount}"
|
||||||
|
] ++ [
|
||||||
|
"--sign_ssh_host_keys=${toString cfg.sshCertificates.enable}"
|
||||||
|
] ++ lib.optionals cfg.sshCertificates.enable [
|
||||||
|
"--ssh_host_key_ca_path=${cfg.sshCertificates.mount}"
|
||||||
|
"--ssh_host_key_expiry_threshold=${cfg.sshCertificates.expiryThreshold}"
|
||||||
|
"--ssh_host_key_output_dir=${cfg.sshCertificates.outputDir}"
|
||||||
|
"--ssh_host_key_principals=${concatStringsSep "," cfg.sshCertificates.principals}"
|
||||||
|
"--ssh_host_key_role=${cfg.sshCertificates.role}"
|
||||||
|
"--ssh_dummy_host_key=${dummyHostKey}"
|
||||||
|
"--sshd=${config.programs.ssh.package}/bin/sshd"
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/acme 0711 secretsmgr secretsmgr - -"
|
||||||
|
"d /var/lib/secretsmgr 0711 secretsmgr secretsmgr - -"
|
||||||
|
"d /var/lib/secretsmgr/ssh 0711 secretsmgr secretsmgr - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
security.polkit.extraConfig = lib.mkAfter ''
|
||||||
|
// NixOS module: depot/lib/secretsmgr.nix
|
||||||
|
polkit.addRule(function(action, subject) {
|
||||||
|
if (action.id !== "org.freedesktop.systemd1.manage-units" ||
|
||||||
|
subject.user !== "secretsmgr") {
|
||||||
|
return polkit.Result.NOT_HANDLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var verb = action.lookup("verb");
|
||||||
|
if (verb !== "restart" && verb !== "reload-or-restart" && verb != "reload-or-try-restart") {
|
||||||
|
return polkit.Result.NOT_HANDLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedUnits = ${builtins.toJSON (unique cfg.restartableUnits)};
|
||||||
|
var unit = action.lookup("unit");
|
||||||
|
for (var i = 0; i < allowedUnits.length; i++) {
|
||||||
|
if (allowedUnits[i] === unit) {
|
||||||
|
return polkit.Result.YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polkit.Result.NOT_HANDLED;
|
||||||
|
});
|
||||||
|
'';
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -5,31 +5,11 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) listToAttrs nameValuePair mkAfter concatMapStrings;
|
inherit (lib) listToAttrs nameValuePair mkAfter concatMapStrings;
|
||||||
|
|
||||||
#keyTypes = [ "ed25519" "rsa" ];
|
|
||||||
keyTypes = [ ];
|
|
||||||
hostKeyForKeyType = keyType: "/etc/ssh/ssh_host_${keyType}_key.pub";
|
|
||||||
secretNameForKeyType = keyType: "openssh-cert-${keyType}";
|
|
||||||
|
|
||||||
signedPaths = map (keyType: config.my.vault.secrets.${secretNameForKeyType keyType}.path) keyTypes;
|
|
||||||
in {
|
in {
|
||||||
config = {
|
config = {
|
||||||
my.vault.secrets = let
|
services.openssh.extraConfig = ''
|
||||||
hostname = config.networking.hostName;
|
HostCertificate /var/lib/secretsmgr/ssh/ssh_host_ed25519_key-cert.pub
|
||||||
fromKey = keyType: {
|
HostCertificate /var/lib/secretsmgr/ssh/ssh_host_rsa_key-cert.pub
|
||||||
template = ''
|
|
||||||
{{ with file "${hostKeyForKeyType keyType}" | printf "public_key=%s" | secret "ssh-host/sign/${hostname}" "cert_type=host" "valid_principals=${hostname}.as205479.net,${hostname}.int.as205479.net" }}
|
|
||||||
{{ .Data.signed_key }}
|
|
||||||
{{ end }}
|
|
||||||
'';
|
|
||||||
group = "root";
|
|
||||||
reloadOrRestartUnits = [ "sshd.service" ];
|
|
||||||
};
|
|
||||||
in listToAttrs (map (keyType: nameValuePair (secretNameForKeyType keyType) (fromKey keyType)) keyTypes);
|
|
||||||
|
|
||||||
systemd.services.vault-agent.serviceConfig.ReadOnlyPaths = mkAfter (map hostKeyForKeyType keyTypes);
|
|
||||||
|
|
||||||
services.openssh.extraConfig = concatMapStrings (c: "HostCertificate ${c}\n") signedPaths + ''
|
|
||||||
TrustedUserCAKeys ${../../secrets/client-ca.pub}
|
TrustedUserCAKeys ${../../secrets/client-ca.pub}
|
||||||
AuthorizedPrincipalsCommand /etc/ssh/authorized_principals_cmd %u
|
AuthorizedPrincipalsCommand /etc/ssh/authorized_principals_cmd %u
|
||||||
AuthorizedPrincipalsCommandUser sshd
|
AuthorizedPrincipalsCommandUser sshd
|
||||||
|
|
|
@ -1,224 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2020 Luke Granger-Brown <depot@lukegb.com>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
{ pkgs, config, depot, lib, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib) mkOption types mkBefore optionalAttrs mkDefault;
|
|
||||||
|
|
||||||
acmeCertificates = lib.mapAttrsToList (name: cOrig: cOrig // { inherit name; }) config.my.vault.acmeCertificates;
|
|
||||||
|
|
||||||
# Work out where we're being asked to write things, and which groups, so we can correctly get permissions.
|
|
||||||
fullchainPath = c: pathFor c.fullchain c "fullchain.pem";
|
|
||||||
chainPath = c: pathFor c.chain c "chain.pem";
|
|
||||||
keyPath = c: pathFor c.key c "privkey.pem";
|
|
||||||
pathFor = p: c: suffix: if isNull p.path then "/var/lib/acme/${c.name}/${suffix}" else p.path;
|
|
||||||
|
|
||||||
isNginx = c: builtins.length c.nginxVirtualHosts > 0;
|
|
||||||
defaultGroup = c: if isNull c.group then if isNginx c then "nginx" else "acme" else c.group;
|
|
||||||
groupOrDefault = p: c: if isNull p then defaultGroup c else p;
|
|
||||||
|
|
||||||
reloadOrRestartUnits = c: (lib.optional (isNginx c) "nginx.service") ++ c.reloadOrRestartUnits;
|
|
||||||
|
|
||||||
acmeCertificatesGroups = lib.unique (lib.filter (x: x != "") (builtins.concatMap (c: [
|
|
||||||
(groupOrDefault c.fullchain.group c)
|
|
||||||
(groupOrDefault c.chain.group c)
|
|
||||||
(groupOrDefault c.key.group c)
|
|
||||||
]) acmeCertificates));
|
|
||||||
|
|
||||||
acmeCertificatesTemplate = map (c: {
|
|
||||||
contents = ''
|
|
||||||
{{with secret "acme/certs/${c.role}" "common_name=${c.name}" "alternative_names=${builtins.concatStringsSep "," (builtins.sort builtins.lessThan c.extraNames)}"}}
|
|
||||||
{{ .Data.cert | writeToFile "${fullchainPath c}" "vault-agent" "${groupOrDefault c.fullchain.group c}" "${c.fullchain.mode}" "newline" }}
|
|
||||||
{{ .Data.issuer_cert | writeToFile "${chainPath c}" "vault-agent" "${groupOrDefault c.chain.group c}" "${c.chain.mode}" "newline" }}
|
|
||||||
{{ .Data.private_key | writeToFile "${keyPath c}" "vault-agent" "${groupOrDefault c.key.group c}" "${c.key.mode}" "newline" }}
|
|
||||||
{{ end }}
|
|
||||||
'';
|
|
||||||
destination = "/var/lib/acme/${c.name}/token";
|
|
||||||
perms = "0600";
|
|
||||||
command = let
|
|
||||||
grp = groupOrDefault c.fullchain.group c;
|
|
||||||
in pkgs.writeShellScript "post-${c.name}-crt" ''
|
|
||||||
${lib.concatMapStringsSep "\n" (x: ''
|
|
||||||
/run/current-system/sw/bin/systemctl reload-or-restart ${x}
|
|
||||||
'') (reloadOrRestartUnits c)}
|
|
||||||
${lib.concatMapStringsSep "\n" (x: ''
|
|
||||||
/run/current-system/sw/bin/systemctl restart ${x}
|
|
||||||
'') c.restartUnits}
|
|
||||||
${lib.optionalString (c.command != "") c.command}
|
|
||||||
'';
|
|
||||||
}) acmeCertificates;
|
|
||||||
|
|
||||||
extraWritableDirs = lib.unique (builtins.concatMap (c: [
|
|
||||||
(dirOf (fullchainPath c))
|
|
||||||
(dirOf (chainPath c))
|
|
||||||
(dirOf (keyPath c))
|
|
||||||
]) acmeCertificates);
|
|
||||||
acmeCertificatesTmpdirs = lib.unique (builtins.concatMap (c:
|
|
||||||
let
|
|
||||||
fullchainDir = dirOf (fullchainPath c);
|
|
||||||
chainDir = dirOf (chainPath c);
|
|
||||||
keyDir = dirOf (keyPath c);
|
|
||||||
|
|
||||||
fullchainGroup = groupOrDefault c.fullchain.group c;
|
|
||||||
chainGroup = groupOrDefault c.chain.group c;
|
|
||||||
keyGroup = groupOrDefault c.key.group c;
|
|
||||||
|
|
||||||
dirGroup = if fullchainDir == keyDir && chainDir == keyDir && c.fullchain.makeDir && c.chain.makeDir && c.key.makeDir then if fullchainGroup == keyGroup && fullchainGroup == chainGroup then fullchainGroup else "-" else null;
|
|
||||||
|
|
||||||
fullchainDirGroup = if isNull dirGroup then fullchainGroup else dirGroup;
|
|
||||||
chainDirGroup = if isNull dirGroup then chainGroup else dirGroup;
|
|
||||||
keyDirGroup = if isNull dirGroup then keyGroup else dirGroup;
|
|
||||||
in lib.optional c.fullchain.makeDir "d ${fullchainDir} 0750 vault-agent ${fullchainDirGroup} - -"
|
|
||||||
++ lib.optional c.chain.makeDir "d ${chainDir} 0750 vault-agent ${chainDirGroup} - -"
|
|
||||||
++ lib.optional c.key.makeDir "d ${keyDir} 0750 vault-agent ${keyDirGroup} - -"
|
|
||||||
++ [ "d /var/lib/acme/${c.name} 0750 vault-agent - -" ]
|
|
||||||
) acmeCertificates);
|
|
||||||
|
|
||||||
allRestartableUnits = lib.unique (builtins.concatMap (c: (reloadOrRestartUnits c) ++ c.restartUnits) acmeCertificates);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./vault-agent.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.my.vault.acmeCertificates = mkOption {
|
|
||||||
type = with types; attrsOf (submodule {
|
|
||||||
options = let
|
|
||||||
fileType = what: defaultMode: submodule {
|
|
||||||
options = {
|
|
||||||
path = mkOption {
|
|
||||||
type = nullOr path;
|
|
||||||
default = null;
|
|
||||||
description = "Path to put the ${what}.";
|
|
||||||
};
|
|
||||||
mode = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = defaultMode;
|
|
||||||
description = "Mode to set for the ${what}.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = "Owner group to set for the ${what}. If null, taken from parent.";
|
|
||||||
};
|
|
||||||
|
|
||||||
makeDir = mkOption {
|
|
||||||
type = bool;
|
|
||||||
default = true;
|
|
||||||
description = "If true, creates the parent directory.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
role = mkOption {
|
|
||||||
type = str;
|
|
||||||
default = "letsencrypt-cloudflare";
|
|
||||||
description = "Which role to use for certificate issuance.";
|
|
||||||
};
|
|
||||||
extraNames = mkOption {
|
|
||||||
type = listOf str;
|
|
||||||
default = [];
|
|
||||||
description = "Non-empty list of hostnames to include.";
|
|
||||||
};
|
|
||||||
|
|
||||||
command = mkOption {
|
|
||||||
type = lines;
|
|
||||||
default = "";
|
|
||||||
description = "Command to run after generating the certificate.";
|
|
||||||
};
|
|
||||||
reloadOrRestartUnits = mkOption {
|
|
||||||
type = listOf str;
|
|
||||||
default = [];
|
|
||||||
description = "List of systemd units to reload/restart after obtaining a new certificate.";
|
|
||||||
};
|
|
||||||
restartUnits = mkOption {
|
|
||||||
type = listOf str;
|
|
||||||
default = [];
|
|
||||||
description = "List of systemd units to restart after obtaining a new certificate.";
|
|
||||||
};
|
|
||||||
|
|
||||||
nginxVirtualHosts = mkOption {
|
|
||||||
type = listOf str;
|
|
||||||
default = [];
|
|
||||||
description = "List of nginx virtual hosts to apply SSL to.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = nullOr str;
|
|
||||||
default = null;
|
|
||||||
description = "Owner group to set for the generated files. Defaults to 'acme' unless nginxVirtualHosts is set, in which case it defaults to 'nginx'.";
|
|
||||||
};
|
|
||||||
|
|
||||||
fullchain = mkOption {
|
|
||||||
type = fileType "certificate's full chain" "0644";
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
chain = mkOption {
|
|
||||||
type = fileType "certificate chain only" "0644";
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
key = mkOption {
|
|
||||||
type = fileType "certificate's key" "0640";
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
default = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = {
|
|
||||||
my.vault.settings = {
|
|
||||||
# TODO: lukegb: figure out how to not get this to DoS Let's Encrypt.
|
|
||||||
#template = mkBefore acmeCertificatesTemplate;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = optionalAttrs config.my.vault.enable {
|
|
||||||
services.vault-agent = {
|
|
||||||
serviceConfig = {
|
|
||||||
SupplementaryGroups = mkBefore acmeCertificatesGroups;
|
|
||||||
ReadWritePaths = mkBefore extraWritableDirs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
tmpfiles.rules = acmeCertificatesTmpdirs;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx = optionalAttrs config.my.vault.enable {
|
|
||||||
virtualHosts = builtins.listToAttrs (builtins.concatMap (certData: let
|
|
||||||
fullchain = fullchainPath certData;
|
|
||||||
chain = chainPath certData;
|
|
||||||
key = keyPath certData;
|
|
||||||
in map (hostName: lib.nameValuePair hostName {
|
|
||||||
sslCertificate = mkDefault (fullchainPath certData);
|
|
||||||
sslCertificateKey = mkDefault (keyPath certData);
|
|
||||||
sslTrustedCertificate = mkDefault (chainPath certData);
|
|
||||||
}) certData.nginxVirtualHosts) acmeCertificates);
|
|
||||||
};
|
|
||||||
|
|
||||||
security.polkit.extraConfig = lib.mkAfter ''
|
|
||||||
// NixOS module: depot/lib/vault-agent-acme.nix
|
|
||||||
polkit.addRule(function(action, subject) {
|
|
||||||
if (action.id !== "org.freedesktop.systemd1.manage-units" ||
|
|
||||||
subject.user !== "vault-agent") {
|
|
||||||
return polkit.Result.NOT_HANDLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
var verb = action.lookup("verb");
|
|
||||||
if (verb !== "restart" && verb !== "reload-or-restart") {
|
|
||||||
return polkit.Result.NOT_HANDLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowedUnits = ${builtins.toJSON allRestartableUnits};
|
|
||||||
var unit = action.lookup("unit");
|
|
||||||
for (var i = 0; i < allowedUnits.length; i++) {
|
|
||||||
if (allowedUnits[i] === unit) {
|
|
||||||
return polkit.Result.YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return polkit.Result.NOT_HANDLED;
|
|
||||||
});
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -551,8 +551,8 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
my.vault.acmeCertificates = {
|
my.vault.acmeCertificates = {
|
||||||
"plex-totoro.lukegb.com" = { nginxVirtualHosts = [ "plex-totoro.lukegb.com" ]; };
|
"plex-totoro.lukegb.com" = { hostnames = [ "plex-totoro.lukegb.com" ]; nginxVirtualHosts = [ "plex-totoro.lukegb.com" ]; };
|
||||||
"invoices.lukegb.com" = { nginxVirtualHosts = [ "invoices.lukegb.com" ]; };
|
"invoices.lukegb.com" = { hostnames = [ "invoices.lukegb.com" ]; nginxVirtualHosts = [ "invoices.lukegb.com" ]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
system.stateVersion = "20.03";
|
system.stateVersion = "20.03";
|
||||||
|
|
|
@ -141,6 +141,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
my.vault.acmeCertificates."ha.lukegb.com" = {
|
my.vault.acmeCertificates."ha.lukegb.com" = {
|
||||||
|
hostnames = [ "ha.lukegb.com" ];
|
||||||
nginxVirtualHosts = [ "ha.lukegb.com" ];
|
nginxVirtualHosts = [ "ha.lukegb.com" ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue