{ config, lib, pkgs, ... }@toplevel: with lib; let cfg = config.fudo.paris-container; packages = with pkgs; [ rtorrent ]; hostname = config.instance.hostname; hostSecrets = config.fudo.secrets.host-secrets."${hostname}"; parisKeypairs = config.fudo.secrets.files.ssh.host-keypairs.paris; keypairFilename = 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 = [ ]; }; nixos-modules = mkOption { type = listOf unspecified; default = [ ]; description = "NixOS modules to add to Paris."; }; kerberos = mkOption { type = nullOr (submodule { options = { keytab = mkOption { type = str; description = "Location of Paris keytab."; }; }; }); default = null; }; ldap = { image = mkOption { type = str; description = "Authentik LDAP outpost Docker image."; default = "ghcr.io/goauthentik/ldap:latest"; }; listen-ips = mkOption { type = listOf str; description = "Address on which to listen for requests."; }; port = mkOption { type = port; description = "Port on which to listen for LDAP requests."; default = 4636; }; access-group = mkOption { type = str; description = "Group to which users must belong for access."; default = let ldapCfg = toplevel.config.fudo.paris-container.ldap; in "cn=shell,${ldapCfg.group-ou},${ldapCfg.base}"; }; 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."; }; 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 = { internal = { interface = mkOption { type = str; description = "Parent host interface on which to listen for internal traffic."; }; 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; }; }; external = { 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 { fudo.secrets.host-secrets."${hostname}" = { parisLdapEnv = { source-file = pkgs.writeText "paris-ldap-proxy.env" (concatStringsSep "\n" [ "AUTHENTIK_HOST=https://${cfg.ldap.authentik-host}" "AUTHENTIK_TOKEN=${readFile cfg.ldap.outpost-token-file}" "AUTHENTIK_INSECURE=0" ]); 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"; }; parisKeytab = mkIf (!isNull cfg.kerberos) { source-file = cfg.kerberos.keytab; target-file = "/run/paris/keytab"; }; } // (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 = map (ip: "${ip}:${toString cfg.ldap.port}:6636") cfg.ldap.listen-ips; environmentFiles = [ hostSecrets.parisLdapEnv.target-file ]; }; systemd = { tmpfiles.rules = [ "d ${cfg.state-directory}/home 0700 - - - -" ]; services."container@paris".after = optional (!isNull cfg.kerberos) config.fudo.secrets.secret-target; }; containers.paris = { autoStart = true; macvlans = [ cfg.networking.internal.interface cfg.networking.external.interface ]; bindMounts = { "/home" = { hostPath = "${cfg.state-directory}/home"; isReadOnly = false; }; "/run/paris/sssd.env" = { hostPath = hostSecrets.parisSssdEnv.target-file; isReadOnly = true; }; "/etc/krb5.keytab" = { hostPath = hostSecrets.parisKeytab.target-file; isReadOnly = true; }; "/etc/krb5.conf" = { hostPath = "/etc/krb5.conf"; isReadOnly = true; }; } // (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; imports = cfg.nixos-modules; environment.systemPackages = packages; security = { acme.acceptTerms = true; krb5 = mkIf (!isNull cfg.kerberos) { enable = true; package = pkgs.heimdal; settings = config.security.krb5.settings; }; }; programs.ssh = { extraConfig = '' GSSAPIAuthentication yes GSSAPIDelegateCredentials yes ''; }; security.pam = { krb5.enable = true; services.sshd.makeHomeDir = true; }; services = { openssh = { enable = true; startWhenNeeded = true; permitRootLogin = "no"; hostKeys = map (keypair: { path = "/run/openssh/keys/${keypairFilename keypair}"; type = keypair.key-type; }) parisKeypairs; settings = { UseDns = true; PermitRootLogin = "no"; }; extraConfig = optionalString (!isNull cfg.kerberos) '' GSSAPIAuthentication yes GSSAPICleanupCredentials yes ''; }; tailscale.enable = true; nginx = { enable = true; recommendedGzipSettings = true; recommendedOptimisation = true; virtualHosts."users.fudo.org" = { enableACME = true; forceSSL = true; locations = { "~ ^/~(.+?)(/.*)?$" = { alias = "/home/$1/public_html$2"; index = "index.html"; extraConfig = "autoindex on;"; }; "/".return = "404 not found"; }; }; }; sssd = { enable = true; kcm = true; environmentFile = "/run/paris/sssd.env"; config = lib.generators.toINI { } { sssd = { config_file_version = 2; reconnection_retries = 3; services = concatStringsSep ", " [ "nss" "pam" "ssh" ]; domains = concatStringsSep ", " [ cfg.ldap.domain ]; }; pam = { reconnection_retries = 3; }; nss.reconnection_retries = 3; "domain/${cfg.ldap.domain}" = { cache_credentials = true; id_provider = "ldap"; chpass_provider = "ldap"; auth_provider = "ldap"; access_provider = "ldap"; ldap_uri = "ldaps://${head cfg.ldap.listen-ips}:${ 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}"; ldap_user_object_class = "user"; ldap_user_name = "cn"; ldap_group_object_class = "group"; ldap_group_name = "cn"; ldap_access_filter = "memberOf=${cfg.ldap.access-group}"; ldap_default_bind_dn = cfg.ldap.bind-dn; ldap_default_authtok = "$LDAP_DEFAULT_AUTHTOKEN"; ldap_tls_reqcert = "allow"; enumerate = true; }; }; }; }; networking = let external = cfg.networking.external; internal = cfg.networking.internal; in { defaultGateway = { address = config.networking.defaultGateway.address; interface = "mv-${external.interface}"; }; enableIPv6 = !isNull internal.ipv6 || !isNull external.ipv6; nameservers = config.networking.nameservers; firewall = { enable = true; allowedTCPPorts = [ 22 ] ++ cfg.ports; }; interfaces = { "mv-${external.interface}" = { ipv4.addresses = optional (!isNull external.ipv4) { inherit (external.ipv4) address prefixLength; }; ipv6.addresses = optional (!isNull external.ipv6) { inherit (external.ipv6) address prefixLength; }; }; "mv-${internal.interface}" = { ipv4.addresses = optional (!isNull internal.ipv4) { inherit (internal.ipv4) address prefixLength; }; ipv6.addresses = optional (!isNull internal.ipv6) { inherit (internal.ipv6) address prefixLength; }; }; }; }; }; }; }; }