diff --git a/flake.nix b/flake.nix index 22a9995..464e3fb 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,12 @@ pkgs = import nixpkgs { inherit system; overlays = [ crane-overlay self.overlays.default ]; + config = { + allowUnfreePredicate = pkg: + builtins.elem (builtins.parseDrvName (pkg.name or pkg.pname)).name [ + "vault" + ]; + }; }; crane-overlay = final: prev: { # crane's lib is not exposed as an overlay in its flake (should be added @@ -25,6 +31,7 @@ in { packages.${system}.${pname} = pkgs.${pname}; defaultPackage.${system} = pkgs.${pname}; + checks.${system}.vault = pkgs.callPackage ./nixos/vault-test.nix {}; overlays.default = final: prev: { "${pname}" = final.craneLib.buildPackage { diff --git a/nixos/vault-test.nix b/nixos/vault-test.nix new file mode 100644 index 0000000..701f314 --- /dev/null +++ b/nixos/vault-test.nix @@ -0,0 +1,251 @@ +{ lib, pkgs }: +let + nixos-lib = import (pkgs.path + "/nixos/lib") { }; + acme-test-module = "${pkgs.path}/nixos/tests/common/acme/server"; + + role_id_path = "/tmp/vault-role-id"; + secret_id_path = "/tmp/vault-secret-id"; + + domain = "faythe.test"; + vault_host = "vault.${domain}"; + ns_host = "ns.${domain}"; + + # dev server + vault_addr = "http://localhost:8200"; + +in +nixos-lib.runTest ( + test@{ nodes, ... }: + { + hostPkgs = pkgs; + name = "faythe-vault-test"; + defaults = { + nixpkgs.pkgs = pkgs; + networking.nameservers = lib.mkForce [ nodes.ns.networking.primaryIPAddress ]; + networking.dhcpcd.enable = false; + security.pki.certificateFiles = [ nodes.acme.test-support.acme.caCert ]; + networking.hosts."${nodes.acme.networking.primaryIPAddress}" = [ nodes.acme.test-support.acme.caDomain ]; + }; + nodes = { + acme = + { pkgs, ... }: + { + imports = [ acme-test-module ]; + }; + + ns = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + dig + dnsutils + ]; + + networking.firewall.allowedTCPPorts = [ 53 ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + + services.bind.enable = true; + + services.bind.zones."${domain}" = { + master = true; + file = "/etc/bind/zones/${domain}.zone"; + # the bind zone module is very opinionated and this sets allow-transfer. + slaves = [ nodes.client.networking.primaryIPAddress ]; + extraConfig = '' + allow-update { ${nodes.client.networking.primaryIPAddress}; }; + ''; + }; + + # Hack to allow access to the directory copied from environment.etc + systemd.services.bind.serviceConfig.ExecStartPre = "+${pkgs.coreutils}/bin/chown named /etc/bind/zones"; + + environment.etc."bind/zones/${domain}.zone" = { + mode = "0644"; + user = "named"; + group = "named"; + text = '' + $TTL 60 + ${domain}. IN SOA ${ns_host}. admin.${domain}. ( 1 3h 1h 1w 1d ) + + @ IN NS ${ns_host}. + + ${ns_host}. IN A ${nodes.ns.networking.primaryIPAddress} + + ${vault_host}. IN A ${nodes.client.networking.primaryIPAddress} + ''; + }; + }; + + client = + { pkgs, config, ... }: + let + + faytheConfig = { + vault_monitor_configs = [ + { + inherit role_id_path secret_id_path vault_addr; + key_prefix = "path1"; + specs = [ + { + name = "path1-test"; + cn = "path1.${domain}"; + } + ]; + } + # Multiple vaultmonitorconfigs break faythe currently, commented out + # { + # inherit role_id_path secret_id_path vault_addr; + # key_prefix = "path2"; + # specs = [ + # { + # name = "path2-test"; + # cn = "path2.${domain}"; + # } + # ]; + # } + ]; + lets_encrypt_url = "https://${nodes.acme.test-support.acme.caDomain}/dir"; + lets_encrypt_email = "test_mail@${domain}"; + zones = { + "${domain}" = { + server = ns_host; + key = "test"; + }; + }; + val_dns_servers = [ ns_host ]; + }; + + faytheConfigFile = pkgs.writeText "faythe.config.json" (builtins.toJSON faytheConfig); + + faytheConfigFileChecked = pkgs.runCommand "faythe.config.checked.json" { } '' + ${pkgs.faythe}/bin/faythe --config-check ${faytheConfigFile} + ln -s ${faytheConfigFile} $out + ''; + in + { + environment.systemPackages = with pkgs; [ + dig + dnsutils + vault + getent + ]; + + environment.variables.VAULT_ADDR = vault_addr; + + services.vault = { + enable = true; + # start unsealed and with known root token + dev = true; + devRootTokenID = "vaultroot"; + }; + + # FIXME: upstream this, makes ordering nicer + systemd.services.vault.serviceConfig.Type = "notify"; + + systemd.services.vault-provision = { + path = with pkgs; [ + vault + getent + ]; + environment.VAULT_ADDR = vault_addr; + wants = [ "vault.service" ]; + after = [ "vault.service" ]; + serviceConfig.Type = "oneshot"; + + script = '' + set -x + set -euo pipefail + + vault login ${config.services.vault.devRootTokenID} + + vault policy write faythe-policy ${ + pkgs.writeText "faythe-policy.json" ( + builtins.toJSON { + path."kv/data/path1/*" = { + capabilities = [ + "list" + "read" + "create" + "update" + ]; + }; + path."kv/metadata/path1/*" = { + capabilities = [ + "list" + "read" + "create" + "update" + ]; + }; + path."kv/data/path2/*" = { + capabilities = [ + "list" + "read" + "create" + "update" + ]; + }; + path."kv/metadata/path2/*" = { + capabilities = [ + "list" + "read" + "create" + "update" + ]; + }; + } + ) + } + + vault auth enable approle + vault write auth/approle/role/faythe type=service policies=faythe-policy + + vault read -field=role_id auth/approle/role/faythe/role-id > ${role_id_path} + vault write -f -field=secret_id auth/approle/role/faythe/secret-id > ${secret_id_path} + + vault secrets enable -path=kv kv-v2 + ''; + }; + + systemd.services.faythe = { + path = with pkgs; [ + dnsutils + dig + ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "vault-provision.service" ]; + after = [ "vault-provision.service" ]; + serviceConfig = { + ExecStart = "${pkgs.faythe}/bin/faythe ${faytheConfigFileChecked}"; + }; + }; + }; + }; + testScript = '' + start_all() + + ns.wait_for_unit("network-online.target") + acme.wait_for_unit("network-online.target") + client.wait_for_unit("network-online.target") + + ns.wait_for_unit("bind.service") + + client.wait_until_succeeds("ping -c1 ${nodes.ns.networking.primaryIPAddress}") + client.wait_until_succeeds("host ${vault_host}") + client.fail("host doesnotexist.${domain}") + + client.wait_for_unit("faythe.service") + + with subtest("Can get certs"): + client.wait_until_succeeds(""" + vault kv get kv/path1/path1-test/cert + # vault kv get kv/path2/path2-test/cert + """) + + with subtest("No failed dispatch in vaultrs"): + client.fail(""" + journalctl -u faythe | grep -q "dispatch task is gone: runtime dropped the dispatch task" + """) + ''; + } +)