{ 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"; }; }; }; }; }