WIP: Consul ACLs #117

Draft
max wants to merge 8 commits from pr-consul-acl into master
7 changed files with 111 additions and 11 deletions

View file

@ -28,6 +28,35 @@ in
bootstrap_expect = builtins.length cfg.nodes.agent; bootstrap_expect = builtins.length cfg.nodes.agent;
addresses.http = config.links.consulAgent.ipv4; addresses.http = config.links.consulAgent.ipv4;
ports.http = config.links.consulAgent.port; ports.http = config.links.consulAgent.port;
acl = {
enabled = true;
default_policy = "deny";
};
};
};
systemd.services = {
consul.serviceConfig.Type = "notify";
consul-load-smt = {
wantedBy = [ "consul.service" ];
after = [ "consul.service" ];
environment.CONSUL_HTTP_ADDR = config.links.consulAgent.tuple;
path = [
config.services.consul.package
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
while ! test -e /run/locksmith/consul-systemManagementToken; do
echo Waiting for System Management Token
systemctl start locksmith.service
sleep 5
done
export CONSUL_HTTP_TOKEN_FILE=/run/locksmith/consul-systemManagementToken
consul acl set-agent-token default "$(< /run/locksmith/consul-systemManagementToken)" # TODO: don't leak token on cmdline
'';
}; };
}; };

View file

@ -0,0 +1,65 @@
{ cluster, config, lib, pkgs, ... }:
let
sentinelFile = "/var/lib/consul/nixos-acl-bootstrapped";
bootstrapTokenFile = "/run/keys/consul-bootstrap-token";
bootstrapConfig = "consul-bootstrap-config.json";
writeRules = rules: pkgs.writeText "consul-policy.json" (builtins.toJSON rules);
in
{
systemd.services = {
consul-acl-bootstrap = {
requires = [ "consul.service" ];
after = [ "consul.service" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = "!${sentinelFile}";
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
};
environment.CONSUL_HTTP_ADDR = config.links.consulAgent.tuple;
path = [
config.services.consul.package
pkgs.jq
];
script = ''
umask 77
if consul acl bootstrap --format=json > ${bootstrapConfig}; then
echo Bootstrapping:
jq -r .SecretID < ${bootstrapConfig} > ${bootstrapTokenFile}
export CONSUL_HTTP_TOKEN_FILE=${bootstrapTokenFile}
consul acl policy create --name operator-read --description "Read-only operator actions" --rules @${writeRules { operator = "read"; }}
consul acl policy create --name smt-read --description "Allow reading the encrypted system management token" --rules @${writeRules { key_prefix."secrets/locksmith/consul-systemManagementToken/".policy = "read"; }}
consul acl token update --id 00000000-0000-0000-0000-000000000002 --append-policy-name operator-read --append-policy-name smt-read
else
echo Bootstrap is already in progress elsewhere.
touch ${sentinelFile}
fi
'';
};
locksmith-provider-consul = {
unitConfig.ConditionPathExists = bootstrapTokenFile;
distributed.enable = lib.mkForce false;
environment = {
CONSUL_HTTP_ADDR = config.links.consulAgent.tuple;
CONSUL_HTTP_TOKEN_FILE = bootstrapTokenFile;
};
postStop = ''
rm -f ${bootstrapTokenFile}
touch ${sentinelFile}
'';
};
};
services.locksmith.providers.consul = {
wantedBy = [ "consul-acl-bootstrap.service" ];
after = [ "consul-acl-bootstrap.service" ];
secrets.systemManagementToken = {
nodes = cluster.config.services.consul.nodes.agent;
checkUpdate = "test -e ${bootstrapTokenFile}";
command = "cat ${bootstrapTokenFile}";
};
};
}

View file

@ -14,6 +14,7 @@ in
nodes = { nodes = {
agent = [ "checkmate" "grail" "thunderskin" "VEGAS" "prophet" ]; agent = [ "checkmate" "grail" "thunderskin" "VEGAS" "prophet" ];
ready = config.services.consul.nodes.agent; ready = config.services.consul.nodes.agent;
bootstrap = [ "grail" "VEGAS" ];
}; };
nixos = { nixos = {
agent = [ agent = [
@ -21,10 +22,11 @@ in
./remote-api.nix ./remote-api.nix
]; ];
ready = ./ready.nix; ready = ./ready.nix;
bootstrap = ./bootstrap.nix;
}; };
simulacrum = { simulacrum = {
enable = true; enable = true;
deps = [ "wireguard" ]; deps = [ "wireguard" "locksmith" ];
settings = ./test.nix; settings = ./test.nix;
}; };
}; };

View file

@ -51,4 +51,9 @@ in
Type = "oneshot"; Type = "oneshot";
}; };
}; };
systemd.targets.consul-ready = {
description = "Consul is Ready";
requires = [ "consul-ready.service" ] ++ lib.optional config.services.consul.enable "consul-load-smt.service";
};
} }

View file

@ -1,8 +1,4 @@
{ lib, ... }:
{ {
defaults.options.services.locksmith = lib.mkSinkUndeclaredOptions { };
testScript = '' testScript = ''
import json import json
@ -11,12 +7,12 @@
with subtest("should form cluster"): with subtest("should form cluster"):
nodes = [ n for n in machines if n != nowhere ] nodes = [ n for n in machines if n != nowhere ]
for machine in nodes: for machine in nodes:
machine.succeed("systemctl start consul-ready.service") machine.succeed("systemctl start consul-ready.target")
for machine in nodes: for machine in nodes:
consulConfig = json.loads(machine.succeed("cat /etc/consul.json")) consulConfig = json.loads(machine.succeed("cat /etc/consul.json"))
addr = consulConfig["addresses"]["http"] addr = consulConfig["addresses"]["http"]
port = consulConfig["ports"]["http"] port = consulConfig["ports"]["http"]
setEnv = f"CONSUL_HTTP_ADDR={addr}:{port}" setEnv = f"CONSUL_HTTP_ADDR={addr}:{port} CONSUL_HTTP_TOKEN_FILE=/run/locksmith/consul-systemManagementToken"
memberList = machine.succeed(f"{setEnv} consul members --status=alive") memberList = machine.succeed(f"{setEnv} consul members --status=alive")
for machine2 in nodes: for machine2 in nodes:
assert machine2.name in memberList assert machine2.name in memberList

View file

@ -45,14 +45,15 @@ in
hasSpecialPrefix = elem (substring 0 1 ExecStart) [ "@" "-" ":" "+" "!" ]; hasSpecialPrefix = elem (substring 0 1 ExecStart) [ "@" "-" ":" "+" "!" ];
in assert !hasSpecialPrefix; pkgs.writeTextDir "etc/systemd/system/${n}.service.d/distributed.conf" '' in assert !hasSpecialPrefix; pkgs.writeTextDir "etc/systemd/system/${n}.service.d/distributed.conf" ''
[Unit] [Unit]
Requires=consul-ready.service Requires=consul-ready.target
After=consul-ready.service After=consul-ready.target
[Service] [Service]
ExecStartPre=${waitForConsul} 'services/${n}%i' ExecStartPre=${waitForConsul} 'services/${n}%i'
ExecStart= ExecStart=
ExecStart=${consul}/bin/consul lock --name=${n} --n=${toString cfg.replicas} --shell=false --child-exit-code 'services/${n}%i' ${optionalString (cfg.registerServices != []) runWithRegistration} ${ExecStart} ExecStart=${consul}/bin/consul lock --name=${n} --n=${toString cfg.replicas} --shell=false --child-exit-code 'services/${n}%i' ${optionalString (cfg.registerServices != []) runWithRegistration} ${ExecStart}
Environment="CONSUL_HTTP_ADDR=${consulHttpAddr}" Environment="CONSUL_HTTP_ADDR=${consulHttpAddr}"
Environment="CONSUL_HTTP_TOKEN_FILE=/run/locksmith/consul-systemManagementToken"
${optionalString (v.serviceConfig ? RestrictAddressFamilies) "RestrictAddressFamilies=AF_NETLINK"} ${optionalString (v.serviceConfig ? RestrictAddressFamilies) "RestrictAddressFamilies=AF_NETLINK"}
${optionalString (cfg.registerServices != []) (lib.concatStringsSep "\n" (map (svc: "ExecStopPost=${svc.commands.deregister}") svcs))} ${optionalString (cfg.registerServices != []) (lib.concatStringsSep "\n" (map (svc: "ExecStopPost=${svc.commands.deregister}") svcs))}
'')) ''))

View file

@ -12,6 +12,7 @@ let
consulRegisterScript = pkgs.writeShellScript "consul-register" '' consulRegisterScript = pkgs.writeShellScript "consul-register" ''
export CONSUL_HTTP_ADDR='${consulHttpAddr}' export CONSUL_HTTP_ADDR='${consulHttpAddr}'
export CONSUL_HTTP_TOKEN_FILE=/run/locksmith/consul-systemManagementToken
while ! ${consul} services register "$1"; do while ! ${consul} services register "$1"; do
sleep 1 sleep 1
done done
@ -19,6 +20,7 @@ let
consulDeregisterScript = pkgs.writeShellScript "consul-deregister" '' consulDeregisterScript = pkgs.writeShellScript "consul-deregister" ''
export CONSUL_HTTP_ADDR='${consulHttpAddr}' export CONSUL_HTTP_ADDR='${consulHttpAddr}'
export CONSUL_HTTP_TOKEN_FILE=/run/locksmith/consul-systemManagementToken
for i in {1..5}; do for i in {1..5}; do
if ${consul} services deregister "$1"; then if ${consul} services deregister "$1"; then
break break
@ -81,8 +83,8 @@ let
}.${mode}; }.${mode};
value = { value = {
direct = { direct = {
after = [ "consul-ready.service" ]; after = [ "consul-ready.target" ];
requires = [ "consul-ready.service" ]; requires = [ "consul-ready.target" ];
serviceConfig = { serviceConfig = {
ExecStartPost = register servicesJson; ExecStartPost = register servicesJson;
ExecStopPost = deregister servicesJson; ExecStopPost = deregister servicesJson;