Some checks failed
Test action / kaas (push) Failing after 1s
WOOP WOOP, it all works! now the next, big, huge, giant, hurdle to overcome is the chicken and egg problem of needing zitadel to generate values that I need inside the nix config of synapse, forgejo, and jellyfin
486 lines
16 KiB
Nix
486 lines
16 KiB
Nix
{ config, lib, pkgs, namespace, system, inputs, ... }:
|
|
let
|
|
inherit (lib) mkIf mkEnableOption mkOption types toUpper nameValuePair mapAttrs' concatMapAttrs getAttrs getAttr hasAttr typeOf head drop length;
|
|
inherit (lib.${namespace}.strings) toSnakeCase;
|
|
|
|
cfg = config.${namespace}.services.authentication.zitadel;
|
|
|
|
database = "zitadel";
|
|
in
|
|
{
|
|
options.${namespace}.services.authentication.zitadel = {
|
|
enable = mkEnableOption "Zitadel";
|
|
|
|
organization = mkOption {
|
|
type = types.attrsOf (types.submodule ({ name, ... }: {
|
|
options =
|
|
let
|
|
org = name;
|
|
in
|
|
{
|
|
isDefault = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
example = "true";
|
|
description = ''
|
|
True sets the org as default org for the instance. Only one org can be default org.
|
|
Nothing happens if you set it to false until you set another org as default org.
|
|
'';
|
|
};
|
|
|
|
project = mkOption {
|
|
default = {};
|
|
type = types.attrsOf (types.submodule {
|
|
options = {
|
|
hasProjectCheck = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
example = "true";
|
|
description = ''
|
|
ZITADEL checks if the org of the user has permission to this project.
|
|
'';
|
|
};
|
|
|
|
privateLabelingSetting = mkOption {
|
|
type = types.nullOr (types.enum [ "unspecified" "enforceProjectResourceOwnerPolicy" "allowLoginUserResourceOwnerPolicy" ]);
|
|
default = null;
|
|
example = "enforceProjectResourceOwnerPolicy";
|
|
description = ''
|
|
Defines from where the private labeling should be triggered,
|
|
|
|
supported values:
|
|
- unspecified
|
|
- enforceProjectResourceOwnerPolicy
|
|
- allowLoginUserResourceOwnerPolicy
|
|
'';
|
|
};
|
|
|
|
projectRoleAssertion = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
example = "true";
|
|
description = ''
|
|
Describes if roles of user should be added in token.
|
|
'';
|
|
};
|
|
|
|
projectRoleCheck = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
example = "true";
|
|
description = ''
|
|
ZITADEL checks if the user has at least one on this project.
|
|
'';
|
|
};
|
|
|
|
application = mkOption {
|
|
default = {};
|
|
type = types.attrsOf (types.submodule {
|
|
options = {
|
|
redirectUris = mkOption {
|
|
type = types.nonEmptyListOf types.str;
|
|
example = ''
|
|
[ "https://example.com/redirect/url" ]
|
|
'';
|
|
description = ''
|
|
.
|
|
'';
|
|
};
|
|
|
|
grantTypes = mkOption {
|
|
type = types.nonEmptyListOf (types.enum [ "authorizationCode" "implicit" "refreshToken" "deviceCode" "tokenExchange" ]);
|
|
example = ''
|
|
[ "authorizationCode" ]
|
|
'';
|
|
description = ''
|
|
.
|
|
'';
|
|
};
|
|
|
|
responseTypes = mkOption {
|
|
type = types.nonEmptyListOf (types.enum [ "code" "idToken" "idTokenToken" ]);
|
|
example = ''
|
|
[ "code" ]
|
|
'';
|
|
description = ''
|
|
.
|
|
'';
|
|
};
|
|
};
|
|
});
|
|
};
|
|
};
|
|
});
|
|
};
|
|
|
|
user = mkOption {
|
|
default = {};
|
|
type = types.attrsOf (types.submodule ({ name, ... }: {
|
|
options =
|
|
let
|
|
username = name;
|
|
in
|
|
{
|
|
email = mkOption {
|
|
type = types.str;
|
|
example = "someone@some.domain";
|
|
description = ''
|
|
Username.
|
|
'';
|
|
};
|
|
|
|
userName = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = cfg.organization.${org}.user.${username}.email;
|
|
example = "someone@some.domain";
|
|
description = ''
|
|
Username. Default value is the user's email, you can overwrite that by setting this option
|
|
'';
|
|
};
|
|
|
|
firstName = mkOption {
|
|
type = types.str;
|
|
example = "John";
|
|
description = ''
|
|
First name of the user.
|
|
'';
|
|
};
|
|
|
|
lastName = mkOption {
|
|
type = types.str;
|
|
example = "Doe";
|
|
description = ''
|
|
Last name of the user.
|
|
'';
|
|
};
|
|
|
|
roles = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
example = "[ \"ORG_OWNER\" ]";
|
|
description = ''
|
|
List of roles granted to organisation.
|
|
'';
|
|
};
|
|
|
|
instanceRoles = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
example = "[ \"IAM_OWNER\" ]";
|
|
description = ''
|
|
List of roles granted to instance.
|
|
'';
|
|
};
|
|
};
|
|
}));
|
|
};
|
|
};
|
|
}));
|
|
};
|
|
};
|
|
|
|
config = let
|
|
_refTypeMap = {
|
|
org = { type = "org"; };
|
|
project = { type = "project"; };
|
|
user = { type = "user"; tfType = "human_user"; };
|
|
};
|
|
|
|
mapRef' = { type, tfType ? type }: name: { "${type}Id" = "\${ resource.zitadel_${tfType}.${toSnakeCase name}.id }"; };
|
|
mapRef = type: name: mapRef' (_refTypeMap.${type}) name;
|
|
mapEnum = prefix: value: "${prefix}_${value |> toSnakeCase |> toUpper}";
|
|
|
|
mapValue = type: value: ({
|
|
grantTypes = map (t: mapEnum "OIDC_GRANT_TYPE" t) value;
|
|
responseTypes = map (t: mapEnum "OIDC_RESPONSE_TYPE" t) value;
|
|
}."${type}" or value);
|
|
|
|
toResource = name: value: nameValuePair
|
|
(toSnakeCase name)
|
|
(lib.mapAttrs' (k: v: nameValuePair (toSnakeCase k) (mapValue k v)) value);
|
|
|
|
withName = name: attrs: attrs // { inherit name; };
|
|
withRef = type: name: attrs: attrs // (mapRef type name);
|
|
withDefaults = defaults: attrs: defaults // attrs;
|
|
|
|
select = keys: callback: set:
|
|
if (length keys) == 0 then
|
|
mapAttrs' callback set
|
|
else let key = head keys; in
|
|
concatMapAttrs (k: v: select (drop 1 keys) (callback k) (v.${key} or {})) set;
|
|
|
|
config' = config;
|
|
|
|
# this is a nix package, the generated json file to be exact
|
|
terraformConfiguration = inputs.terranix.lib.terranixConfiguration {
|
|
inherit system;
|
|
|
|
modules = [
|
|
({ config, lib, ... }: {
|
|
config = {
|
|
terraform.required_providers.zitadel = {
|
|
source = "zitadel/zitadel";
|
|
version = "2.2.0";
|
|
};
|
|
|
|
provider.zitadel = {
|
|
domain = "auth.kruining.eu";
|
|
insecure = "false";
|
|
jwt_profile_file = "/var/lib/zitadel/machine-key.json";
|
|
};
|
|
|
|
resource = {
|
|
# Organizations
|
|
zitadel_org = cfg.organization |> select [] (name: value:
|
|
value
|
|
|> getAttrs [ "isDefault" ]
|
|
|> withName name
|
|
|> toResource name
|
|
);
|
|
|
|
# Projects per organization
|
|
zitadel_project = cfg.organization |> select [ "project" ] (org: name: value:
|
|
value
|
|
|> getAttrs [ "hasProjectCheck" "privateLabelingSetting" "projectRoleAssertion" "projectRoleCheck" ]
|
|
|> withName name
|
|
|> withRef "org" org
|
|
|> toResource name
|
|
);
|
|
|
|
# Each OIDC app per project
|
|
zitadel_application_oidc = cfg.organization |> select [ "project" "application" ] (org: project: name: value:
|
|
value
|
|
|> getAttrs [ "redirectUris" "grantTypes" "responseTypes" ]
|
|
|> withName name
|
|
|> withRef "org" org
|
|
|> withRef "project" project
|
|
|> toResource name
|
|
);
|
|
|
|
# Users
|
|
zitadel_human_user = cfg.organization |> select [ "user" ] (org: name: value:
|
|
value
|
|
|> getAttrs [ "email" "userName" "firstName" "lastName" ]
|
|
|> withRef "org" org
|
|
|> withDefaults { isEmailVerified = true; }
|
|
|> toResource name
|
|
);
|
|
|
|
# Global user roles
|
|
zitadel_instance_member = cfg.organization |> select [ "user" ] (org: name: value:
|
|
{ roles = value.instanceRoles; }
|
|
|> withRef "user" name
|
|
|> toResource name
|
|
);
|
|
|
|
# Organazation specific roles
|
|
zitadel_org_member = cfg.organization |> select [ "user" ] (org: name: value:
|
|
value
|
|
|> getAttrs [ "roles" ]
|
|
|> withRef "org" org
|
|
|> withRef "user" name
|
|
|> toResource name
|
|
);
|
|
|
|
# SMTP config
|
|
zitadel_smtp_config.default = {
|
|
sender_address = "chris@kruining.eu";
|
|
sender_name = "no-reply (Zitadel)";
|
|
tls = true;
|
|
host = "black-mail.nl:587";
|
|
user = "chris@kruining.eu";
|
|
password = lib.tfRef "file(\"${config'.sops.secrets."email/chris_kruining_eu".path}\")";
|
|
set_active = true;
|
|
};
|
|
|
|
# Client credentials per app
|
|
local_sensitive_file = cfg.organization |> select [ "project" "application" ] (org: project: name: value:
|
|
nameValuePair name {
|
|
content = ''
|
|
CLIENT_ID=${lib.tfRef "resource.zitadel_application_oidc.${name}.client_id"}
|
|
CLIENT_SECRET=${lib.tfRef "resource.zitadel_application_oidc.${name}.client_secret"}
|
|
'';
|
|
filename = "/var/lib/zitadel/clients/${name}";
|
|
}
|
|
);
|
|
};
|
|
};
|
|
})
|
|
];
|
|
};
|
|
in
|
|
mkIf cfg.enable {
|
|
${namespace}.services.persistance.postgresql.enable = true;
|
|
|
|
environment.systemPackages = with pkgs; [
|
|
zitadel
|
|
];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d /tmp/zitadelApplyTerraform 0755 zitadel zitadel -"
|
|
"d /var/lib/zitadel/clients 0755 zitadel zitadel -"
|
|
];
|
|
|
|
systemd.services.zitadelApplyTerraform = {
|
|
description = "Zitadel terraform apply";
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "zitadel.service" ];
|
|
|
|
script = ''
|
|
#!/usr/bin/env bash
|
|
|
|
if [ "$(systemctl is-active zitadel)" != "active" ]; then
|
|
echo "Zitadel is not running"
|
|
exit 1
|
|
fi
|
|
|
|
# Copy infra code into workspace
|
|
cp -f ${terraformConfiguration} config.tf.json
|
|
|
|
# Initialize OpenTofu
|
|
${lib.getExe pkgs.opentofu} init
|
|
|
|
# Run the infrastructure code
|
|
${lib.getExe pkgs.opentofu} apply -auto-approve
|
|
'';
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "zitadel";
|
|
Group = "zitadel";
|
|
|
|
WorkingDirectory = "/tmp/zitadelApplyTerraform";
|
|
};
|
|
};
|
|
|
|
services = {
|
|
zitadel = {
|
|
enable = true;
|
|
openFirewall = true;
|
|
masterKeyFile = config.sops.secrets."zitadel/masterKey".path;
|
|
tlsMode = "external";
|
|
settings = {
|
|
Port = 9092;
|
|
|
|
ExternalDomain = "auth.kruining.eu";
|
|
ExternalPort = 443;
|
|
ExternalSecure = true;
|
|
|
|
Metrics.Type = "otel";
|
|
Tracing.Type = "otel";
|
|
Telemetry.Enabled = true;
|
|
|
|
SystemDefaults = {
|
|
PasswordHasher.Hasher.Algorithm = "argon2id";
|
|
SecretHasher.Hasher.Algorithm = "argon2id";
|
|
};
|
|
|
|
Database.postgres = {
|
|
Host = "localhost";
|
|
# Zitadel will report error if port is not set
|
|
Port = 5432;
|
|
Database = database;
|
|
User = {
|
|
Username = database;
|
|
SSL.Mode = "disable";
|
|
};
|
|
Admin = {
|
|
Username = "postgres";
|
|
SSL.Mode = "disable";
|
|
};
|
|
};
|
|
};
|
|
steps = {
|
|
FirstInstance = {
|
|
# Not sure, this option seems to be mostly irrelevant
|
|
InstanceName = "eu";
|
|
|
|
MachineKeyPath = "/var/lib/zitadel/machine-key.json";
|
|
# PatPath = "/var/lib/zitadel/machine-key.pat";
|
|
# LoginClientPatPath = "/var/lib/zitadel/machine-key.json";
|
|
|
|
Org = {
|
|
Name = "kruining";
|
|
|
|
Human = {
|
|
UserName = "chris";
|
|
FirstName = "Chris";
|
|
LastName = "Kruining";
|
|
Email = {
|
|
Address = "chris@kruining.eu";
|
|
Verified = true;
|
|
};
|
|
Password = "KaasIsAwesome1!";
|
|
};
|
|
|
|
Machine = {
|
|
Machine = {
|
|
Username = "terraform-service-user";
|
|
Name = "Terraform";
|
|
};
|
|
MachineKey = { ExpirationDate = "2026-01-01T00:00:00Z"; Type = 1; };
|
|
# Pat = { ExpirationDate = "2026-01-01T00:00:00Z"; };
|
|
};
|
|
|
|
# LoginClient.Machine = {
|
|
# Username = "terraform-service-user";
|
|
# Name = "Terraform";
|
|
# };
|
|
};
|
|
};
|
|
};
|
|
# extraStepsPaths = [
|
|
# config.sops.templates."secrets.yaml".path
|
|
# ];
|
|
};
|
|
|
|
postgresql = {
|
|
enable = true;
|
|
ensureDatabases = [ database ];
|
|
ensureUsers = [
|
|
{
|
|
name = database;
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
|
|
caddy = {
|
|
enable = true;
|
|
virtualHosts = {
|
|
"auth.kruining.eu".extraConfig = ''
|
|
reverse_proxy h2c://::1:9092
|
|
'';
|
|
};
|
|
extraConfig = ''
|
|
(auth) {
|
|
forward_auth h2c://::1:9092 {
|
|
uri /api/authz/forward-auth
|
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
|
}
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
|
|
# Secrets
|
|
sops = {
|
|
secrets = {
|
|
"zitadel/masterKey" = {
|
|
owner = "zitadel";
|
|
group = "zitadel";
|
|
restartUnits = [ "zitadel.service" ]; #EMGDB#6O$8qpGoLI1XjhUhnng1san@0
|
|
};
|
|
|
|
"email/chris_kruining_eu" = {
|
|
owner = "zitadel";
|
|
group = "zitadel";
|
|
restartUnits = [ "zitadel.service" ];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|