machines/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix
Kaare Hoff Skovgaard d842025c81
Some checks failed
/ rust-packages (push) Successful in 4m2s
/ dev-shell (push) Successful in 1m3s
/ terraform-providers (push) Successful in 8m7s
/ check (push) Failing after 7m43s
/ systems (push) Successful in 30m24s
Support multiple dns zones per host
2025-07-23 23:28:15 +02:00

470 lines
15 KiB
Nix

{
config,
pkgs,
lib,
...
}:
let
domain = "login.kaareskovgaard.net";
bootstrapping = config.khscodes."security.kaareskovgaard.net".bootstrap.enable;
openbaoAppBasicSecretFile = "/run/kanidm/openbao_basic_secret";
openbaoCliAppBasicSecretFile = "/run/kanidm/openbao_cli_basic_secret";
monitoringAppBasicSecretFile = "/run/kanidm/monitoring_basic_secret";
forgejoAppBasicSecretFile = "/run/kanidm/forgejo_basic_secret";
secretFiles = [
openbaoAppBasicSecretFile
openbaoCliAppBasicSecretFile
monitoringAppBasicSecretFile
forgejoAppBasicSecretFile
];
openbaoDomain = config.khscodes.infrastructure.openbao.domain;
openbaoAllowedRedirectUrls = [
"https://${openbaoDomain}/ui/vault/auth/kanidm/oidc/callback"
"https://${openbaoDomain}/kanidm/callback"
];
kanidm-reset-password = pkgs.writeShellApplication {
name = "kanidm-reset-password";
runtimeInputs = [
config.services.kanidm.package
pkgs.jq
pkgs.gnugrep
pkgs.expect
];
text = ''
username="''${1:-}"
if [[ "$username" == "" ]]; then
>&2 echo "Usage kanidm-reset-password <username>"
exit 1
fi
echo "Resetting password for idm_admin, please provide sudo password"
password="$(sudo -u kanidm kanidmd recover-account -c /etc/kanidm/server.toml -o json idm_admin 2> /dev/null | grep '^{' | jq --raw-output '.password')"
expect <<EOD
spawn kanidm login --url https://login.kaareskovgaard.net --name idm_admin
expect "Enter password"
send "$password\n"
expect eof
EOD
kanidm person credential create-reset-token --url https://login.kaareskovgaard.net "$username"
trap "kanidm logout --url https://login.kaareskovgaard.net" EXIT
'';
};
in
{
services.kanidm = {
enableServer = true;
# Verify upgrade will be successful by running `kanidmd domain upgrade-check` before bumping package version.
package = pkgs.kanidmWithSecretProvisioning_1_6;
serverSettings = {
inherit domain;
tls_chain = "${config.security.acme.certs.${domain}.directory}/fullchain.pem";
tls_key = "${config.security.acme.certs.${domain}.directory}/key.pem";
origin = "https://${domain}";
trust_x_forward_for = true;
};
provision = {
enable = true;
idmAdminPasswordFile = null;
adminPasswordFile = null;
acceptInvalidCerts = true;
persons.khs = {
present = true;
mailAddresses = [ "kaare@kaareskovgaard.net" ];
legalName = "Kaare Skovgaard";
displayName = "khs";
};
groups.openbao_admin = {
present = true;
members = [ "khs" ];
};
groups.forgejo_user = {
present = true;
members = [ "khs" ];
};
groups.forgejo_comitter = {
present = true;
members = [ "khs" ];
};
groups.forgejo_admin = {
present = true;
members = [ "khs" ];
};
# We cannot add oauth2 apps before the secrets for them are generated.
systems.oauth2 = lib.mkIf (!bootstrapping) {
openbao = {
present = true;
public = false;
preferShortUsername = true;
basicSecretFile = lib.mkIf (!bootstrapping) openbaoAppBasicSecretFile;
originUrl = openbaoAllowedRedirectUrls;
originLanding = "https://${openbaoDomain}";
displayName = "OpenBAO";
scopeMaps = {
"openbao_admin" = [
"profile"
"email"
"openid"
];
};
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"openbao_admin" = [
"openbao_writer"
"openbao_cli_writer"
];
};
};
};
openbao-cli = {
present = true;
public = false;
preferShortUsername = true;
basicSecretFile = lib.mkIf (!bootstrapping) openbaoCliAppBasicSecretFile;
originUrl = [ "http://localhost:8250/oidc/callback" ];
originLanding = "http://localhost:8250";
displayName = "OpenBAO CLI";
scopeMaps = {
"openbao_admin" = [
"profile"
"email"
"openid"
];
};
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"openbao_admin" = [
"openbao_writer"
];
};
};
};
monitoring = {
present = true;
public = false;
preferShortUsername = true;
basicSecretFile = lib.mkIf (!bootstrapping) monitoringAppBasicSecretFile;
originUrl = [ "https://monitoring.kaareskovgaard.net/login/generic_oauth" ];
originLanding = "http://monitoring.kaareskovgaard.net";
displayName = "Monitoring";
scopeMaps = {
"openbao_admin" = [
"profile"
"email"
"openid"
];
};
};
forgejo = {
present = true;
public = false;
preferShortUsername = true;
basicSecretFile = lib.mkIf (!bootstrapping) forgejoAppBasicSecretFile;
originUrl = [ "https://kas.codes/user/oauth2/Kanidm/callback" ];
originLanding = "http://kas.codes";
displayName = "KAS: Codes";
scopeMaps = {
"forgejo_user" = [
"profile"
"email"
"openid"
];
};
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"forgejo_comitter" = [
"comitter"
];
"forgejo_admin" = [
"admin"
];
};
};
};
};
};
};
# Don't add dependencies from bootstrapping when not bootstrapping.
systemd.services.kanidm = lib.mkIf (!bootstrapping) {
unitConfig = {
ConditionPathExists = secretFiles;
};
};
# Allow the server to read the secrets for its own apps
khscodes.infrastructure.vault-server-approle.policy."kanidm/data/apps/*" = {
capabilities = [ "read" ];
};
khscodes.infrastructure.provisioning.post.modules = [
# Creates mount for kanidm apps and generates random passwords for the apps
(
{ config, ... }:
{
terraform.required_providers.random = {
source = "hashicorp/random";
version = "3.7.2";
};
provider.random = { };
# NOTE: This gets stored in the state file
resource.random_password.openbao_secret = {
length = 48;
numeric = true;
lower = true;
upper = true;
special = false;
};
khscodes.vault = {
enable = true;
mount.kanidm = {
path = "kanidm";
type = "kv";
options = {
version = "2";
};
description = "Secrets used for kanidm";
};
};
resource.vault_kv_secret_v2.openbao_secret = {
mount = config.khscodes.vault.output.mount.kanidm.path;
name = "apps/openbao";
data_json = ''
{ "basic_secret": "''${ resource.random_password.openbao_secret.result }" }
'';
};
resource.random_password.openbao_cli_secret = {
length = 48;
numeric = true;
lower = true;
upper = true;
special = false;
};
resource.vault_kv_secret_v2.openbao_cli_secret = {
mount = config.khscodes.vault.output.mount.kanidm.path;
name = "apps/openbao_cli";
data_json = ''
{ "basic_secret": "''${ resource.random_password.openbao_cli_secret.result }" }
'';
};
resource.random_password.monitoring = {
length = 48;
numeric = true;
lower = true;
upper = true;
special = false;
};
resource.vault_kv_secret_v2.monitoring_secret = {
mount = config.khscodes.vault.output.mount.kanidm.path;
name = "apps/monitoring";
data_json = ''
{ "basic_secret": "''${ resource.random_password.monitoring.result }" }
'';
};
resource.random_password.forgejo = {
length = 48;
numeric = true;
lower = true;
upper = true;
special = false;
};
resource.vault_kv_secret_v2.forgejo_secret = {
mount = config.khscodes.vault.output.mount.kanidm.path;
name = "apps/forgejo";
data_json = ''
{ "basic_secret": "''${ resource.random_password.forgejo.result }" }
'';
};
}
)
# Sets up OIDC authentication within OpenBAO.
# OpenBAO queries the openid url for its configuration when adding it, so it is not possible,
# to add this before
(
if bootstrapping then
{ }
else
{
resource.vault_jwt_auth_backend.kanidm_cli = {
description = "Kanidm cli auth backend";
path = "oidc";
type = "oidc";
oidc_discovery_url = "https://${domain}/oauth2/openid/openbao-cli";
oidc_client_id = "openbao-cli";
oidc_client_secret = "\${ resource.random_password.openbao_cli_secret.result }";
default_role = "kanidm_cli_writer";
jwt_supported_algs = [
"ES256"
];
tune = [
{
listing_visibility = "hidden";
default_lease_ttl = "2h";
max_lease_ttl = "2h";
token_type = "default-service";
passthrough_request_headers = [ ];
allowed_response_headers = [ ];
audit_non_hmac_request_keys = [ ];
audit_non_hmac_response_keys = [ ];
}
];
};
resource.vault_jwt_auth_backend.kanidm = {
description = "Kanidm auth backend";
path = "kanidm";
type = "oidc";
oidc_discovery_url = "https://${domain}/oauth2/openid/openbao";
oidc_client_id = "openbao";
oidc_client_secret = "\${ resource.random_password.openbao_secret.result }";
default_role = "kanidm_writer";
jwt_supported_algs = [
"ES256"
];
tune = [
{
listing_visibility = "unauth";
default_lease_ttl = "2h";
max_lease_ttl = "2h";
token_type = "default-service";
passthrough_request_headers = [ ];
allowed_response_headers = [ ];
audit_non_hmac_request_keys = [ ];
audit_non_hmac_response_keys = [ ];
}
];
};
resource.vault_jwt_auth_backend_role.kanidm_cli_writer = {
backend = "\${ resource.vault_jwt_auth_backend.kanidm_cli.path }";
role_name = "kanidm_cli_writer";
bound_audiences = [ "openbao-cli" ];
allowed_redirect_uris = [ "http://localhost:8250/oidc/callback" ];
user_claim = "sub";
token_policies = [ "writer" ];
groups_claim = "groups";
oidc_scopes = [
"openid"
"profile"
"email"
];
};
resource.vault_jwt_auth_backend_role.kanidm_writer = {
backend = "\${ resource.vault_jwt_auth_backend.kanidm.path }";
role_name = "kanidm_writer";
bound_audiences = [ "openbao" ];
allowed_redirect_uris = openbaoAllowedRedirectUrls;
user_claim = "sub";
token_policies = [ "writer" ];
groups_claim = "groups";
oidc_scopes = [
"openid"
"profile"
"email"
];
};
resource.vault_identity_group.kanidm_writer = {
name = "kanidm_writer";
policies = [ "writer" ];
type = "external";
};
resource.vault_identity_group.kanidm_cli_writer = {
name = "kanidm_cli_writer";
policies = [ "writer" ];
type = "external";
};
resource.vault_identity_group_alias.oidc_writer = {
name = "openbao_cli_writer";
mount_accessor = "\${ vault_jwt_auth_backend.kanidm_cli.accessor }";
canonical_id = "\${ vault_identity_group.kanidm_cli_writer.id }";
};
resource.vault_identity_group_alias.kanidm_writer = {
name = "openbao_writer";
mount_accessor = "\${ vault_jwt_auth_backend.kanidm.accessor }";
canonical_id = "\${ vault_identity_group.kanidm_writer.id }";
};
resource.vault_policy.writer = {
name = "writer";
policy = ''
path "*" {
capabilities = ["create", "update", "patch", "read", "delete", "list"]
}
'';
};
}
)
];
khscodes.services.vault-agent.templates = [
{
contents = ''
{{- with secret "kanidm/data/apps/openbao" -}}
{{ .Data.data.basic_secret }}
{{- end -}}
'';
destination = openbaoAppBasicSecretFile;
perms = "0600";
owner = "kanidm";
group = "kanidm";
reloadOrRestartUnits = [ "kanidm.service" ];
}
{
contents = ''
{{- with secret "kanidm/data/apps/openbao_cli" -}}
{{ .Data.data.basic_secret }}
{{- end -}}
'';
destination = openbaoCliAppBasicSecretFile;
perms = "0600";
owner = "kanidm";
group = "kanidm";
reloadOrRestartUnits = [ "kanidm.service" ];
}
{
contents = ''
{{- with secret "kanidm/data/apps/monitoring" -}}
{{ .Data.data.basic_secret }}
{{- end -}}
'';
destination = monitoringAppBasicSecretFile;
perms = "0600";
owner = "kanidm";
group = "kanidm";
reloadOrRestartUnits = [ "kanidm.service" ];
}
{
contents = ''
{{- with secret "kanidm/data/apps/forgejo" -}}
{{ .Data.data.basic_secret }}
{{- end -}}
'';
destination = forgejoAppBasicSecretFile;
perms = "0600";
owner = "kanidm";
group = "kanidm";
reloadOrRestartUnits = [ "kanidm.service" ];
}
];
security.acme.certs.${domain}.reloadServices = [ "kanidm.service" ];
# Allow kanidm to read the certificate file
users.groups.nginx.members = [ "kanidm" ];
khscodes.services.nginx.virtualHosts.${domain} = {
locations."/" = {
proxyPass = "https://${config.services.kanidm.serverSettings.bindaddress}/";
recommendedProxySettings = true;
};
};
environment.systemPackages = [
kanidm-reset-password
];
}