From e925b43a2d6ce6af8a14106cb2d1e50899858e38 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:33:33 -0500 Subject: [PATCH] started soteria homeconfiguration --- modules/hosts/john-pc-ubuntu.nix | 40 ++-- modules/hosts/soteria.nix | 57 +++++- modules/services/restic/envoy.nix | 323 ++++++++++++++++++++++++++++++ 3 files changed, 389 insertions(+), 31 deletions(-) create mode 100644 modules/services/restic/envoy.nix diff --git a/modules/hosts/john-pc-ubuntu.nix b/modules/hosts/john-pc-ubuntu.nix index 406182e..c860dc3 100644 --- a/modules/hosts/john-pc-ubuntu.nix +++ b/modules/hosts/john-pc-ubuntu.nix @@ -2,7 +2,8 @@ let username = "john"; hostname = "john-pc-ubuntu"; - testTarget = "fded:fb16:653e:25da:be24:11ff:fea0:753f"; + # testTarget = "fded:fb16:653e:25da:be24:11ff:fea0:753f"; + testTarget = "fded:fb16:653e:25da:be24:11ff:fe89:1cc3"; in { flake.modules.homeManager."${hostname}" = { pkgs, config, ... }: @@ -34,28 +35,14 @@ in home.packages = with pkgs; [ nixos-rebuild (writeShellScriptBin "test-push" '' - nixos-rebuild switch --flake ${flakeDir}#soteria --target-host root@${testTarget} + mkdir -p /var/tmp/nix-build + chmod 1777 /var/tmp/nix-build + nixos-rebuild switch \ + --flake ${flakeDir}#john-pc-ubuntu \ + --target-host root@${testTarget} '') ]; - mtls = { - enable = true; - caURL = "https://janus.john-stream.com/"; - provisioner = "admin"; - subject = hostname; - san = [ - "${hostname}" - "192.168.1.85" - "spiffe://john-stream.com/ubuntu" - ]; - lifetime = "1h"; - renew.onCalendar = "*:1/10"; - }; - - # TODO: Add host-specific settings here: - # - sops secret for `restic_password/john_ubuntu` - # - zsh RESTIC* session variables - # TODO: make this more restrictive, rather than allowing all unfree packages nixpkgs.config.allowUnfree = true; nixpkgs.config.permittedInsecurePackages = [ "openssl-1.1.1w" ]; @@ -93,6 +80,19 @@ in "/home/john/john-nas" ]; }; + mtls = { + enable = true; + caURL = "https://janus.john-stream.com/"; + provisioner = "admin"; + subject = hostname; + san = [ + "${hostname}" + "192.168.1.85" + "spiffe://john-stream.com/ubuntu" + ]; + lifetime = "1h"; + renew.onCalendar = "*:1/10"; + }; }; flake.homeConfigurations."${hostname}" = inputs.home-manager.lib.homeManagerConfiguration { diff --git a/modules/hosts/soteria.nix b/modules/hosts/soteria.nix index 3d5157d..c30799a 100644 --- a/modules/hosts/soteria.nix +++ b/modules/hosts/soteria.nix @@ -14,23 +14,51 @@ in nixos."${username}" nixos.zsh nixos.login-text - nixos.mtls - nixos.restic-server + # nixos.mtls + # nixos.restic-server + # nixos.restic-envoy { networking.hostName = hostname; + nix.settings.build-dir = "/var/tmp/nix-build"; + systemd.tmpfiles.rules = [ + "d /var/tmp/nix-build 1777 root root -" + ]; step-ssh-host = { hostname = hostname; caURL = caURL; }; - mtls = { - enable = true; - subject = hostname; - caURL = caURL; - san = [ - "${hostname}.john-stream.com" - # "192.168.1.244" - ]; - }; + # mtls = { + # enable = true; + # subject = hostname; + # caURL = caURL; + # san = [ + # "${hostname}.john-stream.com" + # # "192.168.1.244" + # ]; + # }; + + # restic.envoy = { + # enable = true; + # port = 10000; + # spiffePrefix = "spiffe://john-stream.com"; + # upstreamHost = "127.0.0.1"; + # upstreamPort = 8000; + # logLevel = "debug"; + # policies = { + # ubuntu-policy = { + # pathPrefix = "/john-ubuntu"; + # principal = "spiffe://john-stream.com/ubuntu"; + # }; + # p14-policy = { + # pathPrefix = "/john-p14s"; + # principal = "spiffe://john-stream.com/john-p14s"; + # }; + # gitea-policy = { + # pathPrefix = "/gitea"; + # principal = "spiffe://john-stream.com/gitea"; + # }; + # }; + # }; home-manager.users."${username}" = { imports = with inputs.self.modules.homeManager; [ @@ -42,4 +70,11 @@ in } ]; }; + + flake.homeConfigurations."${hostname}" = inputs.home-manager.lib.homeManagerConfiguration { + pkgs = import inputs.nixpkgs { system = "x86_64-linux"; }; + modules = with inputs.self.modules; [ + homeManager."${hostname}" + ]; + }; } \ No newline at end of file diff --git a/modules/services/restic/envoy.nix b/modules/services/restic/envoy.nix new file mode 100644 index 0000000..2319b8c --- /dev/null +++ b/modules/services/restic/envoy.nix @@ -0,0 +1,323 @@ +{ lib, ... }: +{ + flake.modules.nixos.restic-envoy = { config, lib, pkgs, ... }: + let + cfg = config.restic.envoy; + + envoyConfig = { + static_resources = { + listeners = [ + { + name = "listener_0"; + address.socket_address = { + address = cfg.listenAddress; + port_value = cfg.port; + }; + filter_chains = [ + { + filter_chain_match.server_names = cfg.serverNames; + transport_socket = { + name = "envoy.transport_sockets.tls"; + typed_config = { + "@type" = "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext"; + require_client_certificate = true; + common_tls_context = { + tls_params.tls_minimum_protocol_version = "TLSv1_3"; + validation_context = { + trusted_ca.filename = cfg.trustedCAPath; + match_typed_subject_alt_names = [ + { + san_type = "URI"; + matcher.prefix = cfg.spiffePrefix; + } + ]; + }; + tls_certificates = [ + { + certificate_chain.filename = cfg.certificatePath; + private_key.filename = cfg.pkcs8PrivateKeyPath; + } + ]; + }; + }; + }; + filters = [ + { + name = "envoy.filters.network.http_connection_manager"; + typed_config = { + "@type" = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; + stat_prefix = "ingress_http"; + use_remote_address = true; + http2_protocol_options.max_concurrent_streams = 100; + access_log = [ + { + name = "envoy.access_loggers.file"; + typed_config = { + "@type" = "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"; + path = cfg.accessLogPath; + }; + } + ]; + route_config = { + name = "local_route"; + virtual_hosts = [ + { + name = "local_service"; + domains = [ "*" ]; + routes = [ + { + match.prefix = "/"; + route.cluster = cfg.clusterName; + } + ]; + } + ]; + }; + http_filters = [ + { + name = "envoy.filters.http.rbac"; + typed_config = { + "@type" = "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"; + rules = { + action = "ALLOW"; + policies = lib.mapAttrs (_: policy: { + permissions = [ + { + and_rules.rules = [ + { + header = { + name = ":path"; + string_match.prefix = policy.pathPrefix; + }; + } + ]; + } + ]; + principals = [ + { + authenticated.principal_name.exact = policy.principal; + } + ]; + }) cfg.policies; + }; + }; + } + { + name = "envoy.filters.http.router"; + typed_config = { + "@type" = "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"; + }; + } + ]; + }; + } + ]; + } + ]; + } + ]; + clusters = [ + { + name = cfg.clusterName; + connect_timeout = cfg.connectTimeout; + type = "STRICT_DNS"; + lb_policy = "ROUND_ROBIN"; + load_assignment = { + cluster_name = cfg.clusterName; + endpoints = [ + { + lb_endpoints = [ + { + endpoint.address.socket_address = { + address = cfg.upstreamHost; + port_value = cfg.upstreamPort; + }; + } + ]; + } + ]; + }; + } + ]; + }; + }; + + configFile = (pkgs.formats.json { }).generate "restic-envoy.json" envoyConfig; + ensurePkcs8Key = pkgs.writeShellScript "restic-envoy-prepare-key" '' + set -euo pipefail + + install -d -m 0750 /run/restic-envoy + ${lib.getExe pkgs.openssl} pkcs8 \ + -topk8 \ + -nocrypt \ + -in ${lib.escapeShellArg cfg.sourcePrivateKeyPath} \ + -out ${lib.escapeShellArg cfg.pkcs8PrivateKeyPath} + chmod 0600 ${lib.escapeShellArg cfg.pkcs8PrivateKeyPath} + ''; + in + { + options.restic.envoy = { + enable = lib.mkEnableOption "an Envoy mTLS front-end for restic"; + + listenAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Address for Envoy to bind to."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 10000; + description = "TCP port for the Envoy listener."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open the configured listener port in the firewall."; + }; + + serverNames = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.john-stream.com" ]; + description = "Accepted SNI server names for the downstream TLS filter chain."; + }; + + spiffePrefix = lib.mkOption { + type = lib.types.str; + default = "spiffe://john-stream.com"; + description = "Allowed SPIFFE URI prefix for client certificate SAN validation."; + }; + + trustedCAPath = lib.mkOption { + type = lib.types.str; + default = "/etc/step/certs/root_ca.crt"; + description = "Path to the CA certificate used to validate client certificates."; + }; + + certificatePath = lib.mkOption { + type = lib.types.str; + default = "/etc/step/certs/cert.pem"; + description = "Path to the server certificate presented by Envoy."; + }; + + sourcePrivateKeyPath = lib.mkOption { + type = lib.types.str; + default = "/etc/step/certs/key.pem"; + description = "Path to the source private key that will be converted to PKCS#8 for Envoy."; + }; + + pkcs8PrivateKeyPath = lib.mkOption { + type = lib.types.str; + default = "/run/restic-envoy/key_pkcs8.pem"; + description = "Path to the PKCS#8 private key file consumed by Envoy."; + }; + + accessLogPath = lib.mkOption { + type = lib.types.str; + default = "/var/log/envoy/access.log"; + description = "Path for the Envoy HTTP access log."; + }; + + clusterName = lib.mkOption { + type = lib.types.str; + default = "restic"; + description = "Name of the upstream Envoy cluster."; + }; + + connectTimeout = lib.mkOption { + type = lib.types.str; + default = "0.25s"; + description = "Cluster connect timeout in Envoy duration format."; + }; + + upstreamHost = lib.mkOption { + type = lib.types.str; + default = "rest-server"; + description = "DNS name or IP address for the upstream restic server."; + }; + + upstreamPort = lib.mkOption { + type = lib.types.port; + default = 8000; + description = "TCP port for the upstream restic server."; + }; + + logLevel = lib.mkOption { + type = lib.types.enum [ "trace" "debug" "info" "warning" "error" "critical" "off" ]; + default = "info"; + description = "Envoy application log level."; + }; + + policies = lib.mkOption { + description = "RBAC policy definitions keyed by Envoy policy name."; + type = lib.types.attrsOf (lib.types.submodule ({ ... }: { + options = { + pathPrefix = lib.mkOption { + type = lib.types.str; + description = "Allowed HTTP path prefix for this principal."; + }; + + principal = lib.mkOption { + type = lib.types.str; + description = "Exact SPIFFE principal required for this path prefix."; + }; + }; + })); + default = { + ubuntu-policy = { + pathPrefix = "/john-ubuntu"; + principal = "spiffe://john-stream.com/ubuntu"; + }; + p14-policy = { + pathPrefix = "/john-p14s"; + principal = "spiffe://john-stream.com/john-p14s"; + }; + gitea-policy = { + pathPrefix = "/gitea"; + principal = "spiffe://john-stream.com/gitea"; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.serverNames != [ ]; + message = "restic.envoy.serverNames must not be empty."; + } + { + assertion = cfg.policies != { }; + message = "restic.envoy.policies must define at least one RBAC policy."; + } + ]; + + networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ]; + + systemd.tmpfiles.rules = [ + "d /var/log/envoy 0750 root root -" + "d /run/restic-envoy 0750 root root -" + ]; + + systemd.services.restic-envoy = { + description = "Envoy reverse proxy for the restic server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + restartIfChanged = true; + serviceConfig = { + Type = "simple"; + User = "root"; + Group = "root"; + ExecStartPre = ensurePkcs8Key; + ExecStart = "${lib.getExe pkgs.envoy} --config-path ${configFile} --log-level ${cfg.logLevel}"; + Restart = "on-failure"; + RestartSec = "5s"; + RuntimeDirectory = "restic-envoy"; + LogsDirectory = "envoy"; + }; + }; + }; + }; +}