initial implementation of terranix for zitadel. SUPER HAPPY, SUPER COOL!!!
Some checks failed
Test action / kaas (push) Failing after 1s

This commit is contained in:
Chris Kruining 2025-10-22 23:26:47 +02:00
parent 81e1574023
commit 1873bb7170
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
5 changed files with 368 additions and 31 deletions

59
flake.lock generated
View file

@ -265,6 +265,27 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"terranix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@ -906,6 +927,7 @@
"snowfall-lib": "snowfall-lib", "snowfall-lib": "snowfall-lib",
"sops-nix": "sops-nix", "sops-nix": "sops-nix",
"stylix": "stylix", "stylix": "stylix",
"terranix": "terranix",
"zen-browser": "zen-browser" "zen-browser": "zen-browser"
} }
}, },
@ -1109,6 +1131,43 @@
"type": "github" "type": "github"
} }
}, },
"systems_7": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"terranix": {
"inputs": {
"flake-parts": "flake-parts_3",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_7"
},
"locked": {
"lastModified": 1757278723,
"narHash": "sha256-hTMi6oGU+6VRnW9SZZ+muFcbfMEf2ajjOp7Z2KM5MMY=",
"owner": "terranix",
"repo": "terranix",
"rev": "924573fa6587ac57b0d15037fbd2d3f0fcdf17fb",
"type": "github"
},
"original": {
"owner": "terranix",
"repo": "terranix",
"type": "github"
}
},
"tinted-foot": { "tinted-foot": {
"flake": false, "flake": false,
"locked": { "locked": {

View file

@ -78,6 +78,11 @@
flake-compat.follows = ""; flake-compat.follows = "";
}; };
}; };
terranix = {
url = "github:terranix/terranix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = inputs: inputs.snowfall-lib.mkFlake { outputs = inputs: inputs.snowfall-lib.mkFlake {

17
lib/strings/default.nix Normal file
View file

@ -0,0 +1,17 @@
{ lib, ...}:
let
inherit (builtins) isString typeOf;
inherit (lib) throwIfNot concatStringsSep splitStringBy toLower map;
in
{
strings = {
toSnakeCase =
str:
throwIfNot (isString str) "toSnakeCase only accepts string values, but got ${typeOf str}" (
str
|> splitStringBy (prev: curr: builtins.match "[a-z]" prev != null && builtins.match "[A-Z]" curr != null) true
|> map (p: toLower p)
|> concatStringsSep "_"
);
};
}

View file

@ -1,6 +1,7 @@
{ config, lib, pkgs, namespace, ... }: { config, lib, pkgs, namespace, system, inputs, ... }:
let let
inherit (lib) mkIf mkEnableOption; inherit (lib) mkIf mkEnableOption mkOption types toUpper nameValuePair;
inherit (lib.${namespace}.strings) toSnakeCase;
cfg = config.${namespace}.services.authentication.zitadel; cfg = config.${namespace}.services.authentication.zitadel;
@ -9,15 +10,223 @@ in
{ {
options.${namespace}.services.authentication.zitadel = { options.${namespace}.services.authentication.zitadel = {
enable = mkEnableOption "Zitadel"; enable = mkEnableOption "Zitadel";
organization = mkOption {
type = types.attrsOf (types.submodule {
options = {
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 = ''
.
'';
};
};
});
};
};
});
};
};
});
};
}; };
config = mkIf cfg.enable { config = let
mapRef = type: name: { "${type}Id" = "\${ resource.zitadel_${type}.${toSnakeCase name}.id }"; };
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);
# this is a nix package, the generated json file to be exact
terraformConfiguration = inputs.terranix.lib.terranixConfiguration {
inherit system;
modules =
let
inherit (lib) mapAttrs' concatMapAttrs nameValuePair getAttrs getAttr hasAttr typeOf head drop length;
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;
in
[
({ 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 = {
zitadel_org = cfg.organization |> select [] (name: value:
value
|> getAttrs [ "isDefault" ]
|> withName name
|> toResource name
);
zitadel_project = cfg.organization |> select [ "project" ] (org: name: value:
value
|> getAttrs [ "hasProjectCheck" "privateLabelingSetting" "projectRoleAssertion" "projectRoleCheck" ]
|> withName name
|> withRef "org" org
|> toResource name
);
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
);
};
};
})
];
};
in
mkIf cfg.enable {
${namespace}.services.persistance.postgresql.enable = true; ${namespace}.services.persistance.postgresql.enable = true;
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
zitadel zitadel
]; ];
systemd.tmpfiles.rules = [
"d /tmp/zitadelApplyTerraform 0755 zitadel zitadel -"
];
systemd.services.zitadelApplyTerraform = {
description = "Zitadel terraform apply";
wantedBy = [ "multi-user.target" ];
wants = [ "zitadel.service" ];
script = ''
#!/usr/bin/env bash
# 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} plan
${lib.getExe pkgs.opentofu} apply -auto-approve
'';
serviceConfig = {
Type = "oneshot";
User = "zitadel";
Group = "zitadel";
WorkingDirectory = "/tmp/zitadelApplyTerraform";
};
};
services = { services = {
zitadel = { zitadel = {
enable = true; enable = true;
@ -41,31 +250,31 @@ in
SecretHasher.Hasher.Algorithm = "argon2id"; SecretHasher.Hasher.Algorithm = "argon2id";
}; };
DefaultInstance = { # DefaultInstance = {
PasswordComplexityPolicy = { # # PasswordComplexityPolicy = {
MinLength = 20; # # MinLength = 0;
HasLowercase = false; # # HasLowercase = false;
HasUppercase = false; # # HasUppercase = false;
HasNumber = false; # # HasNumber = false;
HasSymbol = false; # # HasSymbol = false;
}; # # };
LoginPolicy = { # LoginPolicy = {
AllowRegister = false; # AllowRegister = false;
ForceMFA = true; # ForceMFA = true;
}; # };
LockoutPolicy = { # LockoutPolicy = {
MaxPasswordAttempts = 5; # MaxPasswordAttempts = 5;
MaxOTPAttempts = 10; # MaxOTPAttempts = 10;
}; # };
SMTPConfiguration = { # # SMTPConfiguration = {
SMTP = { # # SMTP = {
Host = "black-mail.nl:587"; # # Host = "black-mail.nl:587";
User = "chris@kruining.eu"; # # User = "chris@kruining.eu";
Password = "__TODO_USE_SOPS__"; # # Password = "__TODO_USE_SOPS__";
}; # # };
FromName = "Amarth Zitadel"; # # FromName = "Amarth Zitadel";
}; # # };
}; # };
Database.postgres = { Database.postgres = {
Host = "localhost"; Host = "localhost";
@ -84,9 +293,16 @@ in
}; };
steps = { steps = {
FirstInstance = { FirstInstance = {
InstanceName = "auth.kruining.eu"; # 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 = { Org = {
Name = "Amarth"; Name = "kruining";
Human = { Human = {
UserName = "chris"; UserName = "chris";
FirstName = "Chris"; FirstName = "Chris";
@ -97,6 +313,20 @@ in
}; };
Password = "KaasIsAwesome1!"; 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";
# };
}; };
}; };
}; };

View file

@ -39,7 +39,33 @@
sneeuwvlok = { sneeuwvlok = {
services = { services = {
# authentication.authelia.enable = true; # authentication.authelia.enable = true;
authentication.zitadel.enable = true; authentication.zitadel = {
enable = true;
organization = {
thisIsMyAwesomeOrg = {};
nix = {
project = {
ulmo = {
application = {
jellyfin = {
redirectUris = [ "https://jellyfin.kruining.eu/sso/OID/redirect/zitadel" ];
grantTypes = [ "authorizationCode" ];
responseTypes = [ "code" ];
};
forgejo = {
redirectUris = [ "https://git.amarth.cloud/user/oauth2/zitadel/callback" ];
grantTypes = [ "authorizationCode" ];
responseTypes = [ "code" ];
};
};
};
};
};
};
};
communication.matrix.enable = true; communication.matrix.enable = true;