diff --git a/nixosModules/default.nix b/nixosModules/default.nix index ecec7d75..8639e4d5 100644 --- a/nixosModules/default.nix +++ b/nixosModules/default.nix @@ -20,6 +20,7 @@ _: { buildbot = import ./services/buildbot; forgejo-runner = import ./services/forgejo-runner; scrutiny = import ./services/scrutiny; + voidauth = import ./services/voidauth; hm-pyrox = import ./homes/pyrox; hm-thehedgehog = import ./homes/thehedgehog; diff --git a/nixosModules/services/voidauth/default.nix b/nixosModules/services/voidauth/default.nix new file mode 100644 index 00000000..e92dad7e --- /dev/null +++ b/nixosModules/services/voidauth/default.nix @@ -0,0 +1,273 @@ +{ + lib, + config, + self, + pkgs, + ... +}: +let + inherit (lib) types; + cfg = config.dish.voidauth; + dlib = self.lib; + + nameToEnvVar = + name: if builtins.match "[A-Z0-9_]+" name != null then name else dlib.toUpperCamelCase name; + + configEnv = + (lib.concatMapAttrs ( + name: value: + lib.optionalAttrs (value != null) { + ${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value; + } + ) cfg.settings) + // { + DB_ADAPTER = cfg.database.type; + } + // ( + if (cfg.database.type == "postgres") then + { + DB_USER = "voidauth"; + DB_NAME = "voidauth"; + DB_HOST = "/run/postgresql"; + } + else + { } + ); + + ageSecrets = builtins.listToAttrs ( + map ( + c: + let + secFile = c.clientSecretFile; + secFileName = lib.removeSuffix ".age" (baseNameOf secFile); + in + rec { + name = "${c.clientId}-${secFileName}"; + value = { + file = secFile; + owner = "voidauth"; + group = "voidauth"; + path = "/run/agenix/${name}"; + }; + } + ) cfg.declaredClients + ); + + oidcSecretFiles = lib.mapAttrsToList (_: v: v.path) ageSecrets; + + voidauthFolder = { + user = "voidauth"; + group = "voidauth"; + mode = "0700"; + }; + + mkService = + svc: + let + pre = "OIDC_${svc.clientId}_CLIENT"; + fmtList = l: lib.concatStringsSep ", " l; + in + { + "${pre}_DISPLAY_NAME" = svc.displayName; + "${pre}_HOMEPAGE_URL" = svc.homepageUrl; + "${pre}_LOGO_URL" = lib.mkIf (svc.logoUrl != null) svc.logoUrl; + "${pre}_AUTH_METHOD" = svc.authMethod; + "${pre}_POST_LOGOUT_URLS" = lib.mkIf (svc.logoUrl != null) svc.postLogoutUrl; + "${pre}_GROUPS" = fmtList svc.groups; + "${pre}_REDIRECT_URLS" = fmtList svc.redirectUrls; + "${pre}_RESPONSE_TYPES" = fmtList svc.responseTypes; + "${pre}_GRANT_TYPES" = fmtList svc.grantTypes; + "${pre}_CLIENT_SKIP_CONSENT" = lib.boolToString svc.skipConsent; + }; + + oidcClientOptions = _: { + options = { + clientId = lib.mkOption { + description = "ID to use for this client, cannot have an underscore in the name because of VoidAuth limitations."; + type = types.addCheck types.str (x: !(lib.hasInfix "_" x)); + example = "exampleClientID"; + }; + displayName = lib.mkOption { + description = "Name to show for this OIDC client in the VoidAuth WebUI"; + type = types.str; + example = "App Display Name"; + }; + homepageUrl = lib.mkOption { + description = "Homepage to direct users to from the dashboard"; + type = types.str; + example = "https://example.com"; + }; + clientSecretFile = lib.mkOption { + description = "Environment file containing the client secret environment variable for this OIDC client. Will be included in VoidAuth's configuration"; + type = types.path; + }; + authMethod = lib.mkOption { + description = "Authentication method to use for this OIDC client"; + type = types.enum [ + "client_secret_basic" + "client_secret_post" + "none" + ]; + default = "client_secret_basic"; + }; + groups = lib.mkOption { + description = "User groups to send to this OIDC client"; + type = types.listOf types.str; + default = [ ]; + }; + redirectUrls = lib.mkOption { + description = "Valid URLs that this OIDC client can redirect to after authentication with VoidAuth"; + type = types.listOf types.str; + default = [ ]; + example = [ "https://example.com/oidc-redirect" ]; + }; + responseTypes = lib.mkOption { + description = "Allowed OIDC response types for this OIDC client"; + type = types.listOf ( + types.enum [ + "code" + "id_token" + "token" + "none" + ] + ); + default = [ "code" ]; + }; + grantTypes = lib.mkOption { + description = "Available grant types for this OIDC client"; + type = types.listOf ( + types.enum [ + "authorization_code" + "implicit" + "refresh_token" + ] + ); + default = [ + "authorization_code" + "refresh_token" + ]; + }; + postLogoutUrl = lib.mkOption { + description = "Logout URLs to send users to post-logout"; + type = types.nullOr types.str; + example = "https://example.com/post-logout"; + default = null; + }; + logoUrl = lib.mkOption { + description = "URL for the service's logo"; + type = types.nullOr types.str; + example = "https://example.com/logo.png"; + default = null; + }; + skipConsent = lib.mkEnableOption "skipping the explicit consent screen for this OIDC client"; + }; + }; +in +{ + options.dish = { + oidcClients = lib.mkOption { + description = "OIDC Clients to add to VoidAuth's configuration"; + type = types.listOf (types.submodule oidcClientOptions); + }; + voidauth = { + enable = lib.mkEnableOption "Voidauth"; + package = lib.mkPackageOption self.legacyPackages.${pkgs.stdenv.hostPlatform.system} "voidauth" { }; + + declaredClients = lib.mkOption { + description = "Apps to secure with VoidAuth by default. Will overwrite any configured app with the same client ID in the database"; + type = types.listOf (types.submodule oidcClientOptions); + }; + + secretsFile = lib.mkOption { + description = "File storing secret environment variables such as `STORAGE_KEY`, `DB_PASSWORD`, and `SMTP_PASS`."; + type = types.str; + }; + + database = { + type = lib.mkOption { + description = "The database type to use for VoidAuth. It currently supports SQLite and PostgreSQL"; + type = types.enum [ + "sqlite" + "postgres" + ]; + default = "postgres"; + example = "sqlite"; + }; + createLocally = lib.mkOption { + description = "If enabled and `database.type` is postgres, creates a local postgres database."; + type = types.bool; + default = true; + }; + }; + + settings = lib.mkOption { + description = '' + Options to set for this service. Will be converted from camelCase to UPPER_SNAKE_CASE as used by VoidAuth. + + All available options can be found in the [upstream documentation](https://voidauth.app/#/Getting-Started?id=environment-variables) + ''; + type = types.submodule { + freeformType = types.attrsOf types.str; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.voidauth = { + isSystemUser = true; + group = "voidauth"; + }; + users.groups.voidauth = { }; + systemd.services.voidauth = { + description = "VoidAuth OIDC Server"; + wantedBy = [ "multi-user.target" ]; + after = [ "postgresql.target" ]; + environment = + let + serviceSets = lib.mergeAttrsList (map mkService cfg.declaredClients); + in + configEnv // serviceSets; + serviceConfig = { + User = "voidauth"; + Group = "voidauth"; + StateDirectory = "voidauth"; + ExecStart = "${lib.getExe cfg.package} server"; + WorkingDirectory = "/var/lib/voidauth"; + EnvironmentFile = oidcSecretFiles ++ [ cfg.secretsFile ]; + + # Hardening + ProtectProc = "invisible"; + NoNewPrivileges = true; + ProtectSystem = true; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + }; + + services.postgresql = lib.mkIf (cfg.database.createLocally && cfg.database.type == "postgres") { + ensureUsers = [ + { + name = "voidauth"; + ensureDBOwnership = true; + } + ]; + ensureDatabases = [ "voidauth" ]; + }; + + systemd.tmpfiles.settings."10-voidauth" = { + "/var/lib/voidauth/config".d = voidauthFolder; + "/var/lib/voidauth/db".d = lib.mkIf (cfg.database.type == "sqlite") voidauthFolder; + "/var/lib/voidauth/theme".d = voidauthFolder; + }; + age.secrets = ageSecrets; + }; +}