{ config, lib, pkgs, ... }: with lib; let cfg = config.fudo.paris-container; packages = with pkgs; [ rtorrent ]; hostname = config.instance.hostname; hostSecrets = config.fudo.secrets.host-secrets."${hostname}"; hostKeypairs = config.fudo.secrets.files.ssh.host-keypairs.paris; keytabFilename = keypair: "host-paris-${keypair.key-type}-private-key"; in { options.fudo.paris-container = with types; { enable = mkEnableOption "Enable Fudo Paris user server."; state-directory = mkOption { type = str; description = "Directory at which to store server state."; }; ports = mkOption { type = listOf port; description = "List of ports to open to the public internet."; default = [ ]; }; ssh-keys = mkOption { type = listOf str; description = "List of SSH keys to use."; default = [ ]; }; ldap = { image = mkOption { type = str; description = "Authentik LDAP outpost Docker image."; default = "ghcr.io/goauthentik/ldap:latest"; }; port = mkOption { type = port; description = "Port on which to listen for LDAP requests."; default = 4389; }; domain = mkOption { type = str; description = "Domain for which data is served. Only used for internal mapping."; }; authentik-host = mkOption { type = str; description = "Hostname of the LDAP outpost provider."; default = "authentik.${toplevel.config.fudo.mail.primary-domain}"; }; outpost-token-file = mkOption { type = str; description = "File containing token with which to authenticate to the Authentik host."; }; bind-dn = mkOption { type = str; description = "DN as which to bind with the LDAP server."; }; bind-token-file = mkOption { type = str; description = "File containing token with which to bind with the LDAP server."; }; base = mkOption { type = str; description = "Base of the LDAP server."; example = "dc=fudo,dc=org"; }; user-ou = mkOption { type = str; description = "Organizational unit containing users."; default = "ou=users"; }; group-ou = mkOption { type = str; description = "Organizational unit containing users."; default = "ou=groups"; }; }; networking = { interface = mkOption { type = str; description = "Parent host interface on which to listen."; }; ipv4 = mkOption { type = nullOr (submodule { options = { address = mkOption { type = str; description = "IP address."; }; prefixLength = mkOption { type = int; description = "Significant bits in the address."; }; }; }); default = null; }; ipv6 = mkOption { type = nullOr (submodule { options = { address = mkOption { type = str; description = "IP address."; }; prefixLength = mkOption { type = int; description = "Significant bits in the address."; }; }; }); default = null; }; }; }; config = mkIf cfg.enable { systemd.tmpfiles.rules = [ "d ${cfg.state-directory}/home 0700 root root - -" ]; fudo.secrets.host-secrets."${hostname}" = { parisLdapEnv = { source-file = pkgs.writeText "paris-ldap-proxy.env" (concatStringsSep "\n" [ "AUTHENTIK_HOST=${cfg.ldap.authentik-host}" "AUTHENTIK_TOKEN=${readFile cfg.ldap.outpost-token-file}" "AUTHENTIK_INSECURE=false" ]); target-file = "/run/paris/ldap.env"; }; parisSssdEnv = { source-file = pkgs.writeText "paris-sssd.env" "LDAP_DEFAULT_AUTHTOKEN=${readFile cfg.ldap.bind-token-file}"; target-file = "/run/paris/sssd.env"; }; } // (listToAttrs (map (keypair: nameValuePair (keypairFilename keypair) { source-file = keypair.private-key; target-file = "/run/paris/openssh/${keypairFilename keypair}"; }) parisKeypairs)); virtualisation.oci-containers.containers.paris-ldap-proxy = { image = cfg.ldap.image; autoStart = true; ports = [ "${toString cfg.ldap.port}:389" ]; environmentFiles = [ hostSecrets.parisLdapEnv.target-file ]; }; containers.paris = { macvlans = [ cfg.networking.interface ]; bindMounts = { "/home" = { hostPath = "${cfg.state-directory}/home"; isReadOnly = false; }; } // (listToAttrs (map (keypair: nameValuePair "/run/openssh/keys/${keypairFilename keypair}" { hostPath = "/run/paris/openssh/${keypairFilename keypair}"; isReadOnly = true; }) parisKeypairs)); additionalCapabilities = [ "CAP_NET_ADMIN" ]; config = { nixpkgs.pkgs = pkgs; environment.systemPackages = packages; services = { opensssh = { enable = true; startWhenNeeded = true; hostKeys = map (keypair: { path = "/run/openssh/keys/${keypairFilename keypair}"; type = keypair.key-type; }) parisKeypairs; settings = { UseDns = true; PermitRootLogin = "no"; }; }; sssd = { enable = true; kcm = true; environmentFile = hostSecrets.parisSssdEnv.target-file; config = lib.generators.toINI { } { sssd = { config_file_version = 2; reconnection_retries = 3; sbus_timeout = 30; services = concatStringsSep " " [ "nss" "pam" "ssh" ]; domains = concatStringsSep " " [ cfg.ldap.domain ]; }; pam = { reconnection_retries = 3; }; nss = { filter_groups = "root"; filter_users = "root"; reconnection_retries = 3; }; "domain/${cfg.ldap.domain}" = { cache_credentials = true; id_provider = "ldap"; chpass_provider = "ldap"; auth_provider = "ldap"; access_provider = "ldap"; ldap_uri = "ldap://localhost:${toString cfg.ldap.port}"; ldap_schema = "rfc2307bis"; ldap_search_base = cfg.ldap.base; ldap_user_search_base = "${cfg.ldap.user-ou},${cfg.ldap.base}"; ldap_group_search_base = "${cfg.ldap.group-ou},${cfg.ldap.base}"; ladp_user_object_class = "user"; ldap_user_cn = "cn"; ldap_group_object_class = "group"; ldap_group_name = "cn"; ldap_default_bind_dn = cfg.ldap.bind-dn; ldap_default_authtok = "$LDAP_DEFAULT_AUTHTOKEN"; }; }; }; }; networking = { defaultGateway = { address = config.networking.defaultGateway.address; interface = "mv-${cfg.networking.interface}"; }; enableIPv6 = !isNull cfg.networking.ipv6; nameservers = config.networking.nameservers; firewall = { enable = true; allowedTCPPorts = [ 22 ] ++ cfg.ports; }; interfaces."mv-${cfg.networking.interface}" = { ipv4.addresses = optional (!isNull cfg.networking.ipv4) { address = cfg.networking.ipv4.address; prefixLength = cfg.networking.ipv4.prefixLength; }; ipv6.addresses = optional (!isNull cfg.networking.ipv6) { address = cfg.networking.ipv6.address; prefixLength = cfg.networking.ipv6.prefixLength; }; }; }; }; }; }; }