Loading nixos/modules/module-list.nix +1 −0 Original line number Diff line number Diff line Loading @@ -361,6 +361,7 @@ ./programs/zsh/zsh.nix ./rename.nix ./security/acme ./security/agnos.nix ./security/apparmor.nix ./security/audit.nix ./security/auditd.nix Loading nixos/modules/security/agnos.nix 0 → 100644 +314 −0 Original line number Diff line number Diff line { config, lib, pkgs, ... }: let cfg = config.security.agnos; format = pkgs.formats.toml { }; name = "agnos"; stateDir = "/var/lib/${name}"; accountType = let inherit (lib) types mkOption; in types.submodule { freeformType = format.type; options = { email = mkOption { type = types.str; description = '' Email associated with this account. ''; }; private_key_path = mkOption { type = types.str; description = '' Path of the PEM-encoded private key for this account. Currently, only RSA keys are supported. If this path does not exist, then the behavior depends on `generateKeys.enable`. When this option is `true`, the key will be automatically generated and saved to this path. When it is `false`, agnos will fail. If a relative path is specified, the key will be looked up (or generated and saved to) under `${stateDir}`. ''; }; certificates = mkOption { type = types.listOf certificateType; description = '' Certificates for agnos to issue or renew. ''; }; }; }; certificateType = let inherit (lib) types literalExpression mkOption; in types.submodule { freeformType = format.type; options = { domains = mkOption { type = types.listOf types.str; description = '' Domains the certificate represents ''; example = literalExpression ''["a.example.com", "b.example.com", "*b.example.com"]''; }; fullchain_output_file = mkOption { type = types.str; description = '' Output path for the full chain including the acquired certificate. If a relative path is specified, the file will be created in `${stateDir}`. ''; }; key_output_file = mkOption { type = types.str; description = '' Output path for the certificate private key. If a relative path is specified, the file will be created in `${stateDir}`. ''; }; }; }; in { options.security.agnos = let inherit (lib) types mkEnableOption mkOption; in { enable = mkEnableOption name; settings = mkOption { description = "Settings"; type = types.submodule { freeformType = format.type; options = { dns_listen_addr = mkOption { type = types.str; default = "0.0.0.0:53"; description = '' Address for agnos to listen on. Note that this needs to be reachable by the outside world, and 53 is required in most situations since `NS` records do not allow specifying the port. ''; }; accounts = mkOption { type = types.listOf accountType; description = '' A list of ACME accounts. Each account is associated with an email address and can be used to obtain an arbitrary amount of certificate (subject to provider's rate limits, see e.g. [Let's Encrypt Rate Limits](https://letsencrypt.org/docs/rate-limits/)). ''; }; }; }; }; generateKeys = { enable = mkOption { type = types.bool; default = false; description = '' Enable automatic generation of account keys. When this is `true`, a key will be generated for each account where the file referred to by the `private_key` path does not exist yet. Currently, only RSA keys can be generated. ''; }; keySize = mkOption { type = types.int; default = 4096; description = '' Key size in bits to use when generating new keys. ''; }; }; server = mkOption { type = types.nullOr types.str; default = null; description = '' ACME Directory Resource URI. Defaults to Let's Encrypt's production endpoint, `https://acme-v02.api.letsencrypt.org/directory`, if unset. ''; }; serverCa = mkOption { type = types.nullOr types.path; default = null; description = '' The root certificate (in PEM format) of the ACME server's HTTPS interface. ''; }; persistent = mkOption { type = types.bool; default = true; description = '' When `true`, use a persistent systemd timer. ''; }; startAt = mkOption { type = types.either types.str (types.listOf types.str); default = "daily"; example = "02:00"; description = '' How often or when to run agnos. The format is described in {manpage}`systemd.time(7)`. ''; }; temporarilyOpenFirewall = mkOption { type = types.bool; default = false; description = '' When `true`, will open the port specified in `settings.dns_listen_addr` before running the agnos service, and close it when agnos finishes running. ''; }; group = mkOption { type = types.str; default = name; description = '' Group to run Agnos as. The acquired certificates will be owned by this group. ''; }; user = mkOption { type = types.str; default = name; description = '' User to run Agnos as. The acquired certificates will be owned by this user. ''; }; }; config = let configFile = format.generate "agnos.toml" cfg.settings; port = lib.toInt (lib.last (builtins.split ":" cfg.settings.dns_listen_addr)); useNftables = config.networking.nftables.enable; # nftables implementation for temporarilyOpenFirewall nftablesSetup = pkgs.writeShellScript "agnos-fw-setup" '' ${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ tcp . ${toString port} }" ${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ udp . ${toString port} }" ''; nftablesTeardown = pkgs.writeShellScript "agnos-fw-teardown" '' ${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ tcp . ${toString port} }" ${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ udp . ${toString port} }" ''; # iptables implementation for temporarilyOpenFirewall helpers = '' function ip46tables() { ${lib.getExe' pkgs.iptables "iptables"} -w "$@" ${lib.getExe' pkgs.iptables "ip6tables"} -w "$@" } ''; fwFilter = ''--dport ${toString port} -j ACCEPT -m comment --comment "agnos"''; iptablesSetup = pkgs.writeShellScript "agnos-fw-setup" '' ${helpers} ip46tables -I INPUT 1 -p tcp ${fwFilter} ip46tables -I INPUT 1 -p udp ${fwFilter} ''; iptablesTeardown = pkgs.writeShellScript "agnos-fw-setup" '' ${helpers} ip46tables -D INPUT -p tcp ${fwFilter} ip46tables -D INPUT -p udp ${fwFilter} ''; in lib.mkIf cfg.enable { assertions = [ { assertion = !cfg.temporarilyOpenFirewall || config.networking.firewall.enable; message = "temporarilyOpenFirewall is only useful when firewall is enabled"; } ]; systemd.services.agnos = { serviceConfig = { ExecStartPre = lib.optional cfg.generateKeys.enable '' ${pkgs.agnos}/bin/agnos-generate-accounts-keys \ --no-confirm \ --key-size ${toString cfg.generateKeys.keySize} \ ${configFile} '' ++ lib.optional cfg.temporarilyOpenFirewall ( "+" + (if useNftables then nftablesSetup else iptablesSetup) ); ExecStopPost = lib.optional cfg.temporarilyOpenFirewall ( "+" + (if useNftables then nftablesTeardown else iptablesTeardown) ); ExecStart = '' ${pkgs.agnos}/bin/agnos \ ${if cfg.server != null then "--acme-url=${cfg.server}" else "--no-staging"} \ ${lib.optionalString (cfg.serverCa != null) "--acme-serv-ca=${cfg.serverCa}"} \ ${configFile} ''; Type = "oneshot"; User = cfg.user; Group = cfg.group; StateDirectory = name; StateDirectoryMode = "0750"; WorkingDirectory = "${stateDir}"; # Allow binding privileged ports if necessary CapabilityBoundingSet = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ]; AmbientCapabilities = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ]; }; after = [ "firewall.target" "network-online.target" "nftables.service" ]; wants = [ "network-online.target" ]; }; systemd.timers.agnos = { timerConfig = { OnCalendar = cfg.startAt; Persistent = cfg.persistent; Unit = "agnos.service"; }; wantedBy = [ "timers.target" ]; }; users.groups = lib.mkIf (cfg.group == name) { ${cfg.group} = { }; }; users.users = lib.mkIf (cfg.user == name) { ${cfg.user} = { isSystemUser = true; description = "Agnos service user"; group = cfg.group; }; }; }; } nixos/tests/agnos.nix 0 → 100644 +209 −0 Original line number Diff line number Diff line { system ? builtins.currentSystem, pkgs ? import ../.. { inherit system; }, lib ? pkgs.lib, }: let inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest; nodeIP = n: n.networking.primaryIPAddress; dnsZone = nodes: pkgs.writeText "agnos.test.zone" '' $TTL 604800 @ IN SOA ns1.agnos.test. root.agnos.test. ( 3 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire 604800 ) ; Negative Cache TTL ; ; name servers - NS records IN NS ns1.agnos.test. ; name servers - A records ns1.agnos.test. IN A ${nodeIP nodes.dnsserver} agnos-ns.agnos.test. IN A ${nodeIP nodes.server} _acme-challenge.a.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.b.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.c.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.d.agnos.test. IN NS agnos-ns.agnos.test. ''; mkTest = { name, extraServerConfig ? { }, checkFirewallClosed ? true, }: makeTest { inherit name; meta = { maintainers = with lib.maintainers; [ justinas ]; }; nodes = { # The fake ACME server which will respond to client requests acme = { nodes, pkgs, ... }: { imports = [ ./common/acme/server ]; environment.systemPackages = [ pkgs.netcat ]; networking.nameservers = lib.mkForce [ (nodeIP nodes.dnsserver) ]; }; # A fake DNS server which points _acme-challenge subdomains to "server" dnsserver = { nodes, ... }: { networking.firewall.allowedTCPPorts = [ 53 ]; networking.firewall.allowedUDPPorts = [ 53 ]; services.bind = { cacheNetworks = [ "192.168.1.0/24" ]; enable = true; extraOptions = '' dnssec-validation no; ''; zones."agnos.test" = { file = dnsZone nodes; master = true; }; }; }; # The server using agnos to request certificates server = { nodes, ... }: { imports = [ extraServerConfig ]; networking.extraHosts = '' ${nodeIP nodes.acme} acme.test ''; security.agnos = { enable = true; generateKeys.enable = true; persistent = false; server = "https://acme.test/dir"; serverCa = ./common/acme/server/ca.cert.pem; temporarilyOpenFirewall = true; settings.accounts = [ { email = "webmaster@agnos.test"; # account with an existing private key private_key_path = "${./common/acme/server/acme.test.key.pem}"; certificates = [ { domains = [ "a.agnos.test" ]; # Absolute paths fullchain_output_file = "/tmp/a.agnos.test.crt"; key_output_file = "/tmp/a.agnos.test.key"; } { domains = [ "b.agnos.test" "*.b.agnos.test" ]; # Relative paths fullchain_output_file = "b.agnos.test.crt"; key_output_file = "b.agnos.test.key"; } ]; } { email = "webmaster2@agnos.test"; # account with a missing private key, should get generated private_key_path = "webmaster2.key"; certificates = [ { domains = [ "c.agnos.test" ]; # Absolute paths fullchain_output_file = "/tmp/c.agnos.test.crt"; key_output_file = "/tmp/c.agnos.test.key"; } { domains = [ "d.agnos.test" "*.d.agnos.test" ]; # Relative paths fullchain_output_file = "d.agnos.test.crt"; key_output_file = "d.agnos.test.key"; } ]; } ]; }; }; }; testScript = '' def check_firewall_closed(caller): """ Check that TCP port 53 is closed again. Since we do not set `networking.firewall.rejectPackets`, "timed out" indicates a closed port, while "connection refused" (after agnos has shut down) indicates an open port. """ out = caller.fail("nc -v -z -w 1 server 53 2>&1") assert "Connection timed out" in out start_all() acme.wait_for_unit('pebble.service') server.wait_for_unit('default.target') # Test that agnos.timer is scheduled server.succeed("systemctl status agnos.timer") server.succeed('systemctl start agnos.service') expected_perms = "640 agnos agnos" outputs = [ "/tmp/a.agnos.test.crt", "/tmp/a.agnos.test.key", "/var/lib/agnos/b.agnos.test.crt", "/var/lib/agnos/b.agnos.test.key", "/var/lib/agnos/webmaster2.key", "/tmp/c.agnos.test.crt", "/tmp/c.agnos.test.key", "/var/lib/agnos/d.agnos.test.crt", "/var/lib/agnos/d.agnos.test.key", ] for o in outputs: out = server.succeed(f"stat -c '%a %U %G' {o}").strip() assert out == expected_perms, \ f"Expected mode/owner/group to be '{expected_perms}', but it was '{out}'" ${lib.optionalString checkFirewallClosed "check_firewall_closed(acme)"} ''; }; in { iptables = mkTest { name = "iptables"; }; nftables = mkTest { name = "nftables"; extraServerConfig = { networking.nftables.enable = true; }; }; no-firewall = mkTest { name = "no-firewall"; extraServerConfig = { networking.firewall.enable = lib.mkForce false; security.agnos.temporarilyOpenFirewall = lib.mkForce false; }; checkFirewallClosed = false; }; } nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -177,6 +177,7 @@ in agate = runTest ./web-servers/agate.nix; agda = runTest ./agda.nix; age-plugin-tpm-decrypt = runTest ./age-plugin-tpm-decrypt.nix; agnos = discoverTests (import ./agnos.nix); agorakit = runTest ./web-apps/agorakit.nix; airsonic = runTest ./airsonic.nix; akkoma = runTestOn [ "x86_64-linux" "aarch64-linux" ] { Loading pkgs/by-name/ag/agnos/package.nix +2 −0 Original line number Diff line number Diff line Loading @@ -28,4 +28,6 @@ rustPlatform.buildRustPackage rec { license = licenses.mit; maintainers = with maintainers; [ justinas ]; }; passthru.tests = nixosTests.agnos; } Loading
nixos/modules/module-list.nix +1 −0 Original line number Diff line number Diff line Loading @@ -361,6 +361,7 @@ ./programs/zsh/zsh.nix ./rename.nix ./security/acme ./security/agnos.nix ./security/apparmor.nix ./security/audit.nix ./security/auditd.nix Loading
nixos/modules/security/agnos.nix 0 → 100644 +314 −0 Original line number Diff line number Diff line { config, lib, pkgs, ... }: let cfg = config.security.agnos; format = pkgs.formats.toml { }; name = "agnos"; stateDir = "/var/lib/${name}"; accountType = let inherit (lib) types mkOption; in types.submodule { freeformType = format.type; options = { email = mkOption { type = types.str; description = '' Email associated with this account. ''; }; private_key_path = mkOption { type = types.str; description = '' Path of the PEM-encoded private key for this account. Currently, only RSA keys are supported. If this path does not exist, then the behavior depends on `generateKeys.enable`. When this option is `true`, the key will be automatically generated and saved to this path. When it is `false`, agnos will fail. If a relative path is specified, the key will be looked up (or generated and saved to) under `${stateDir}`. ''; }; certificates = mkOption { type = types.listOf certificateType; description = '' Certificates for agnos to issue or renew. ''; }; }; }; certificateType = let inherit (lib) types literalExpression mkOption; in types.submodule { freeformType = format.type; options = { domains = mkOption { type = types.listOf types.str; description = '' Domains the certificate represents ''; example = literalExpression ''["a.example.com", "b.example.com", "*b.example.com"]''; }; fullchain_output_file = mkOption { type = types.str; description = '' Output path for the full chain including the acquired certificate. If a relative path is specified, the file will be created in `${stateDir}`. ''; }; key_output_file = mkOption { type = types.str; description = '' Output path for the certificate private key. If a relative path is specified, the file will be created in `${stateDir}`. ''; }; }; }; in { options.security.agnos = let inherit (lib) types mkEnableOption mkOption; in { enable = mkEnableOption name; settings = mkOption { description = "Settings"; type = types.submodule { freeformType = format.type; options = { dns_listen_addr = mkOption { type = types.str; default = "0.0.0.0:53"; description = '' Address for agnos to listen on. Note that this needs to be reachable by the outside world, and 53 is required in most situations since `NS` records do not allow specifying the port. ''; }; accounts = mkOption { type = types.listOf accountType; description = '' A list of ACME accounts. Each account is associated with an email address and can be used to obtain an arbitrary amount of certificate (subject to provider's rate limits, see e.g. [Let's Encrypt Rate Limits](https://letsencrypt.org/docs/rate-limits/)). ''; }; }; }; }; generateKeys = { enable = mkOption { type = types.bool; default = false; description = '' Enable automatic generation of account keys. When this is `true`, a key will be generated for each account where the file referred to by the `private_key` path does not exist yet. Currently, only RSA keys can be generated. ''; }; keySize = mkOption { type = types.int; default = 4096; description = '' Key size in bits to use when generating new keys. ''; }; }; server = mkOption { type = types.nullOr types.str; default = null; description = '' ACME Directory Resource URI. Defaults to Let's Encrypt's production endpoint, `https://acme-v02.api.letsencrypt.org/directory`, if unset. ''; }; serverCa = mkOption { type = types.nullOr types.path; default = null; description = '' The root certificate (in PEM format) of the ACME server's HTTPS interface. ''; }; persistent = mkOption { type = types.bool; default = true; description = '' When `true`, use a persistent systemd timer. ''; }; startAt = mkOption { type = types.either types.str (types.listOf types.str); default = "daily"; example = "02:00"; description = '' How often or when to run agnos. The format is described in {manpage}`systemd.time(7)`. ''; }; temporarilyOpenFirewall = mkOption { type = types.bool; default = false; description = '' When `true`, will open the port specified in `settings.dns_listen_addr` before running the agnos service, and close it when agnos finishes running. ''; }; group = mkOption { type = types.str; default = name; description = '' Group to run Agnos as. The acquired certificates will be owned by this group. ''; }; user = mkOption { type = types.str; default = name; description = '' User to run Agnos as. The acquired certificates will be owned by this user. ''; }; }; config = let configFile = format.generate "agnos.toml" cfg.settings; port = lib.toInt (lib.last (builtins.split ":" cfg.settings.dns_listen_addr)); useNftables = config.networking.nftables.enable; # nftables implementation for temporarilyOpenFirewall nftablesSetup = pkgs.writeShellScript "agnos-fw-setup" '' ${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ tcp . ${toString port} }" ${lib.getExe pkgs.nftables} add element inet nixos-fw temp-ports "{ udp . ${toString port} }" ''; nftablesTeardown = pkgs.writeShellScript "agnos-fw-teardown" '' ${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ tcp . ${toString port} }" ${lib.getExe pkgs.nftables} delete element inet nixos-fw temp-ports "{ udp . ${toString port} }" ''; # iptables implementation for temporarilyOpenFirewall helpers = '' function ip46tables() { ${lib.getExe' pkgs.iptables "iptables"} -w "$@" ${lib.getExe' pkgs.iptables "ip6tables"} -w "$@" } ''; fwFilter = ''--dport ${toString port} -j ACCEPT -m comment --comment "agnos"''; iptablesSetup = pkgs.writeShellScript "agnos-fw-setup" '' ${helpers} ip46tables -I INPUT 1 -p tcp ${fwFilter} ip46tables -I INPUT 1 -p udp ${fwFilter} ''; iptablesTeardown = pkgs.writeShellScript "agnos-fw-setup" '' ${helpers} ip46tables -D INPUT -p tcp ${fwFilter} ip46tables -D INPUT -p udp ${fwFilter} ''; in lib.mkIf cfg.enable { assertions = [ { assertion = !cfg.temporarilyOpenFirewall || config.networking.firewall.enable; message = "temporarilyOpenFirewall is only useful when firewall is enabled"; } ]; systemd.services.agnos = { serviceConfig = { ExecStartPre = lib.optional cfg.generateKeys.enable '' ${pkgs.agnos}/bin/agnos-generate-accounts-keys \ --no-confirm \ --key-size ${toString cfg.generateKeys.keySize} \ ${configFile} '' ++ lib.optional cfg.temporarilyOpenFirewall ( "+" + (if useNftables then nftablesSetup else iptablesSetup) ); ExecStopPost = lib.optional cfg.temporarilyOpenFirewall ( "+" + (if useNftables then nftablesTeardown else iptablesTeardown) ); ExecStart = '' ${pkgs.agnos}/bin/agnos \ ${if cfg.server != null then "--acme-url=${cfg.server}" else "--no-staging"} \ ${lib.optionalString (cfg.serverCa != null) "--acme-serv-ca=${cfg.serverCa}"} \ ${configFile} ''; Type = "oneshot"; User = cfg.user; Group = cfg.group; StateDirectory = name; StateDirectoryMode = "0750"; WorkingDirectory = "${stateDir}"; # Allow binding privileged ports if necessary CapabilityBoundingSet = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ]; AmbientCapabilities = lib.mkIf (port < 1024) [ "CAP_NET_BIND_SERVICE" ]; }; after = [ "firewall.target" "network-online.target" "nftables.service" ]; wants = [ "network-online.target" ]; }; systemd.timers.agnos = { timerConfig = { OnCalendar = cfg.startAt; Persistent = cfg.persistent; Unit = "agnos.service"; }; wantedBy = [ "timers.target" ]; }; users.groups = lib.mkIf (cfg.group == name) { ${cfg.group} = { }; }; users.users = lib.mkIf (cfg.user == name) { ${cfg.user} = { isSystemUser = true; description = "Agnos service user"; group = cfg.group; }; }; }; }
nixos/tests/agnos.nix 0 → 100644 +209 −0 Original line number Diff line number Diff line { system ? builtins.currentSystem, pkgs ? import ../.. { inherit system; }, lib ? pkgs.lib, }: let inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest; nodeIP = n: n.networking.primaryIPAddress; dnsZone = nodes: pkgs.writeText "agnos.test.zone" '' $TTL 604800 @ IN SOA ns1.agnos.test. root.agnos.test. ( 3 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire 604800 ) ; Negative Cache TTL ; ; name servers - NS records IN NS ns1.agnos.test. ; name servers - A records ns1.agnos.test. IN A ${nodeIP nodes.dnsserver} agnos-ns.agnos.test. IN A ${nodeIP nodes.server} _acme-challenge.a.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.b.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.c.agnos.test. IN NS agnos-ns.agnos.test. _acme-challenge.d.agnos.test. IN NS agnos-ns.agnos.test. ''; mkTest = { name, extraServerConfig ? { }, checkFirewallClosed ? true, }: makeTest { inherit name; meta = { maintainers = with lib.maintainers; [ justinas ]; }; nodes = { # The fake ACME server which will respond to client requests acme = { nodes, pkgs, ... }: { imports = [ ./common/acme/server ]; environment.systemPackages = [ pkgs.netcat ]; networking.nameservers = lib.mkForce [ (nodeIP nodes.dnsserver) ]; }; # A fake DNS server which points _acme-challenge subdomains to "server" dnsserver = { nodes, ... }: { networking.firewall.allowedTCPPorts = [ 53 ]; networking.firewall.allowedUDPPorts = [ 53 ]; services.bind = { cacheNetworks = [ "192.168.1.0/24" ]; enable = true; extraOptions = '' dnssec-validation no; ''; zones."agnos.test" = { file = dnsZone nodes; master = true; }; }; }; # The server using agnos to request certificates server = { nodes, ... }: { imports = [ extraServerConfig ]; networking.extraHosts = '' ${nodeIP nodes.acme} acme.test ''; security.agnos = { enable = true; generateKeys.enable = true; persistent = false; server = "https://acme.test/dir"; serverCa = ./common/acme/server/ca.cert.pem; temporarilyOpenFirewall = true; settings.accounts = [ { email = "webmaster@agnos.test"; # account with an existing private key private_key_path = "${./common/acme/server/acme.test.key.pem}"; certificates = [ { domains = [ "a.agnos.test" ]; # Absolute paths fullchain_output_file = "/tmp/a.agnos.test.crt"; key_output_file = "/tmp/a.agnos.test.key"; } { domains = [ "b.agnos.test" "*.b.agnos.test" ]; # Relative paths fullchain_output_file = "b.agnos.test.crt"; key_output_file = "b.agnos.test.key"; } ]; } { email = "webmaster2@agnos.test"; # account with a missing private key, should get generated private_key_path = "webmaster2.key"; certificates = [ { domains = [ "c.agnos.test" ]; # Absolute paths fullchain_output_file = "/tmp/c.agnos.test.crt"; key_output_file = "/tmp/c.agnos.test.key"; } { domains = [ "d.agnos.test" "*.d.agnos.test" ]; # Relative paths fullchain_output_file = "d.agnos.test.crt"; key_output_file = "d.agnos.test.key"; } ]; } ]; }; }; }; testScript = '' def check_firewall_closed(caller): """ Check that TCP port 53 is closed again. Since we do not set `networking.firewall.rejectPackets`, "timed out" indicates a closed port, while "connection refused" (after agnos has shut down) indicates an open port. """ out = caller.fail("nc -v -z -w 1 server 53 2>&1") assert "Connection timed out" in out start_all() acme.wait_for_unit('pebble.service') server.wait_for_unit('default.target') # Test that agnos.timer is scheduled server.succeed("systemctl status agnos.timer") server.succeed('systemctl start agnos.service') expected_perms = "640 agnos agnos" outputs = [ "/tmp/a.agnos.test.crt", "/tmp/a.agnos.test.key", "/var/lib/agnos/b.agnos.test.crt", "/var/lib/agnos/b.agnos.test.key", "/var/lib/agnos/webmaster2.key", "/tmp/c.agnos.test.crt", "/tmp/c.agnos.test.key", "/var/lib/agnos/d.agnos.test.crt", "/var/lib/agnos/d.agnos.test.key", ] for o in outputs: out = server.succeed(f"stat -c '%a %U %G' {o}").strip() assert out == expected_perms, \ f"Expected mode/owner/group to be '{expected_perms}', but it was '{out}'" ${lib.optionalString checkFirewallClosed "check_firewall_closed(acme)"} ''; }; in { iptables = mkTest { name = "iptables"; }; nftables = mkTest { name = "nftables"; extraServerConfig = { networking.nftables.enable = true; }; }; no-firewall = mkTest { name = "no-firewall"; extraServerConfig = { networking.firewall.enable = lib.mkForce false; security.agnos.temporarilyOpenFirewall = lib.mkForce false; }; checkFirewallClosed = false; }; }
nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -177,6 +177,7 @@ in agate = runTest ./web-servers/agate.nix; agda = runTest ./agda.nix; age-plugin-tpm-decrypt = runTest ./age-plugin-tpm-decrypt.nix; agnos = discoverTests (import ./agnos.nix); agorakit = runTest ./web-apps/agorakit.nix; airsonic = runTest ./airsonic.nix; akkoma = runTestOn [ "x86_64-linux" "aarch64-linux" ] { Loading
pkgs/by-name/ag/agnos/package.nix +2 −0 Original line number Diff line number Diff line Loading @@ -28,4 +28,6 @@ rustPlatform.buildRustPackage rec { license = licenses.mit; maintainers = with maintainers; [ justinas ]; }; passthru.tests = nixosTests.agnos; }