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")
|
||||
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")
|
||||
sshDummyHostKey = flag.String("ssh_dummy_host_key", "", "Path to dummy SSH hostkey")
|
||||
sshd = flag.String("sshd", "sshd", "Path to sshd")
|
||||
|
||||
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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -69,7 +74,11 @@ func sshHostKeyPaths(ctx context.Context) ([]string, error) {
|
|||
for scanner.Scan() {
|
||||
t := scanner.Text()
|
||||
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 {
|
||||
|
@ -226,7 +235,7 @@ func shouldRenewSSHCert(certPath string, publicKey []byte, principals string) (b
|
|||
}
|
||||
|
||||
func reloadOrTryRestartServices(ctx context.Context, services []string) error {
|
||||
sd, err := dbus.NewSystemdConnection()
|
||||
sd, err := dbus.NewWithContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
|
@ -352,7 +361,9 @@ func setGroup(groupName string) (func(), error) {
|
|||
}
|
||||
|
||||
origGid := unix.Getegid()
|
||||
unix.Setregid(-1, gid)
|
||||
if err := unix.Setregid(-1, gid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() {
|
||||
unix.Setregid(-1, origGid)
|
||||
}, nil
|
||||
|
@ -514,6 +525,18 @@ func writeCertificate(certDef acmeCertificate, cert *vapi.Secret) error {
|
|||
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 {
|
||||
certDefs, err := parseACMECertificates()
|
||||
if err != nil {
|
||||
|
@ -523,6 +546,7 @@ func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
|||
log.Infof("acme certificate defs: %#v", certDefs)
|
||||
|
||||
var hadErr bool
|
||||
var unitsToReloadOrRestart []string
|
||||
for _, certDef := range certDefs {
|
||||
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 {
|
||||
log.Errorf("creating/fixing dir %q: %w", certDef.OutputDir(), err)
|
||||
log.Errorf("creating/fixing dir %q: %v", certDef.OutputDir(), err)
|
||||
hadErr = true
|
||||
continue
|
||||
}
|
||||
|
@ -565,6 +589,16 @@ func checkAndRenewACMECertificates(ctx context.Context, c *vapi.Client) bool {
|
|||
hadErr = true
|
||||
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
|
||||
|
|
|
@ -82,7 +82,7 @@ in {
|
|||
};
|
||||
};
|
||||
my.vault.acmeCertificates."objdump.zxcvbnm.ninja" = {
|
||||
extraNames = [ "*.objdump.zxcvbnm.ninja" ];
|
||||
hostnames = [ "objdump.zxcvbnm.ninja" "*.objdump.zxcvbnm.ninja" ];
|
||||
nginxVirtualHosts = [ "objdump.zxcvbnm.ninja" ];
|
||||
};
|
||||
my.fup.listen = [
|
||||
|
|
|
@ -167,7 +167,7 @@ in {
|
|||
|
||||
my.vault.acmeCertificates."matrix.zxcvbnm.ninja" = {
|
||||
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" ];
|
||||
};
|
||||
|
||||
|
|
|
@ -93,10 +93,13 @@ in {
|
|||
my.vault.acmeCertificates = {
|
||||
"xmpp.lukegb.com" = {
|
||||
group = "prosody";
|
||||
extraNames = [ "*.xmpp.lukegb.com" "lukegb.com" ];
|
||||
hostnames = [ "xmpp.lukegb.com" "*.xmpp.lukegb.com" "lukegb.com" ];
|
||||
reloadOrRestartUnits = [ "prosody.service" ];
|
||||
};
|
||||
"turn.lukegb.com" = {
|
||||
group = "turnserver";
|
||||
hostnames = [ "turn.lukegb.com" ];
|
||||
reloadOrRestartUnits = [ "coturn.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ in {
|
|||
|
||||
my.vault.acmeCertificates."as205479.net" = {
|
||||
group = "acme";
|
||||
hostnames = [ "as205479.net" ];
|
||||
reloadOrRestartUnits = [ "freeradius.service" ];
|
||||
};
|
||||
users.users.nginx.extraGroups = lib.mkAfter [ "acme" ];
|
||||
|
|
|
@ -193,7 +193,7 @@ in {
|
|||
useLegacyConfig = false;
|
||||
};
|
||||
my.vault.acmeCertificates."znc.lukegb.com" = {
|
||||
extraNames = [ "akiichiro.lukegb.com" ];
|
||||
hostnames = [ "znc.lukegb.com" "akiichiro.lukegb.com" ];
|
||||
group = "znc-acme";
|
||||
};
|
||||
services.nginx = {
|
||||
|
|
|
@ -354,7 +354,7 @@ in {
|
|||
};
|
||||
};
|
||||
my.vault.acmeCertificates."lukegb.com" = {
|
||||
extraNames = [
|
||||
hostnames = [
|
||||
"lukegb.com"
|
||||
"*.lukegb.com"
|
||||
"*.int.lukegb.com"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
};
|
||||
my.vault.acmeCertificates."as205479.net" = {
|
||||
role = "letsencrypt-gcloud-as205479";
|
||||
extraNames = [ "www.as205479.net" ];
|
||||
hostnames = [ "as205479.net" "www.as205479.net" ];
|
||||
nginxVirtualHosts = [ "as205479.net" ];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ in
|
|||
};
|
||||
my.vault.acmeCertificates = {
|
||||
"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" ];
|
||||
};
|
||||
};
|
||||
|
|
|
@ -18,8 +18,9 @@ in
|
|||
imports = [
|
||||
../../../third_party/home-manager/nixos
|
||||
./vault-agent.nix
|
||||
./vault-agent-acme.nix
|
||||
./vault-agent-secrets.nix
|
||||
./secretsmgr.nix
|
||||
./secretsmgr-acme.nix
|
||||
./ssh-ca-vault.nix
|
||||
];
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ in
|
|||
}) config.my.fup.listen);
|
||||
in {
|
||||
my.vault.acmeCertificates."p.lukegb.com" = {
|
||||
hostnames = [ "p.lukegb.com" ];
|
||||
nginxVirtualHosts = [ "p.lukegb.com" ];
|
||||
};
|
||||
services.nginx = {
|
||||
|
|
|
@ -25,8 +25,8 @@ in
|
|||
ssl = true;
|
||||
}) config.my.quotesdb.listen);
|
||||
in {
|
||||
my.vault.acmeCertificates."bfob.gg" = {
|
||||
extraNames = [ "*.bfob.gg" ];
|
||||
my.vault.acmeCertificates.bfob = {
|
||||
hostnames = [ "bfob.gg" "*.bfob.gg" ];
|
||||
nginxVirtualHosts = [ "qdb.bfob.gg" "quotes.bfob.gg" "dev-quotes.bfob.gg" ];
|
||||
};
|
||||
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, ... }:
|
||||
let
|
||||
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 {
|
||||
config = {
|
||||
my.vault.secrets = let
|
||||
hostname = config.networking.hostName;
|
||||
fromKey = keyType: {
|
||||
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 + ''
|
||||
services.openssh.extraConfig = ''
|
||||
HostCertificate /var/lib/secretsmgr/ssh/ssh_host_ed25519_key-cert.pub
|
||||
HostCertificate /var/lib/secretsmgr/ssh/ssh_host_rsa_key-cert.pub
|
||||
TrustedUserCAKeys ${../../secrets/client-ca.pub}
|
||||
AuthorizedPrincipalsCommand /etc/ssh/authorized_principals_cmd %u
|
||||
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 = {
|
||||
"plex-totoro.lukegb.com" = { nginxVirtualHosts = [ "plex-totoro.lukegb.com" ]; };
|
||||
"invoices.lukegb.com" = { nginxVirtualHosts = [ "invoices.lukegb.com" ]; };
|
||||
"plex-totoro.lukegb.com" = { hostnames = [ "plex-totoro.lukegb.com" ]; nginxVirtualHosts = [ "plex-totoro.lukegb.com" ]; };
|
||||
"invoices.lukegb.com" = { hostnames = [ "invoices.lukegb.com" ]; nginxVirtualHosts = [ "invoices.lukegb.com" ]; };
|
||||
};
|
||||
|
||||
system.stateVersion = "20.03";
|
||||
|
|
|
@ -141,6 +141,7 @@ in {
|
|||
};
|
||||
};
|
||||
my.vault.acmeCertificates."ha.lukegb.com" = {
|
||||
hostnames = [ "ha.lukegb.com" ];
|
||||
nginxVirtualHosts = [ "ha.lukegb.com" ];
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue