Add kanidm identity provider on id.qo.is #184
Loading…
Reference in a new issue
No description provided.
Delete branch "kanidm-identity-provider"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a Kanidm instance at
https://id.qo.isas a reusableqois.kanidmmodule, andonboards Grafana as the first OIDC relying party. Groups are provisioned from the repo
(append-only, so UI-managed membership survives a redeploy); persons are not.
Kanidm has no plaintext listener and haproxy forwards 443 by SNI with PROXY protocol, so
nginx keeps terminating public TLS and
security.acme.certs."id.qo.is".postRuncopies thecertificate into
/var/lib/kanidm/askanidm:kanidm0400.kanidm.serviceis orderedafter
acme-order-renew-id.qo.is.service, so the certificate is there before it firststarts. Kanidm binds
[::1]:8443behind nginx. LDAPS listens on636but no firewall portis opened, so it is host-local.
The kanidm↔grafana coupling lives in
nixos-modules/kanidm-grafana/, so the OAuth2 clientis provisioned on whichever host runs kanidm regardless of where grafana runs. Grafana
itself only knows
qois.grafana.sso.*(enabled by default,sso.enable = falsein tests).The client secret is shared across hosts through the new
qois.sharedSecretsFileoption.Access is a single kanidm group: members of
sysadmingetGrafanaAdmin, everyone elsewho can log in gets
Viewer.Blocking: secret migration in
private/kanidm/oauth2/grafanacurrently sits in the host secrets file and must move to theshared one, otherwise
lindberg-webappsdoes not build:Verified locally
vm-test-run-kanidm— now runs a pebble ACME node: the certificate is really issued,postRuncreates/var/lib/kanidmand installs it askanidm:kanidm 0400, and kanidmstarts on that ordering. Plus
/status, OIDC discovery, the provisioned client secret(400 vs. 401 on
/oauth2/token), LDAPS reachable locally but not from a second node,telegraf
x509_certvm-test-run-kanidm-grafana— thegrafanaclient is provisioned in kanidm and/login/generic_oauthredirects to kanidm's/ui/oauth2withclient_id=grafanaand aPKCE
code_challengevm-test-run-grafanaandchecks.formattinglindberg-webapps, which is blocked on the migrationabove
To verify after deploying
A/AAAArecord forid.qo.ispointing at the loadbalancer existsls -l /var/lib/kanidm/{fullchain,key}.pemiskanidm:kanidm0400andkanidm.servicecame up on the first tryhttps://id.qo.is/uiasidm_admin, create a person, add them tosysadmin, then confirm the OAuth button onhttps://monitoring.qo.is/loginlogsthem in as a Grafana admin
ldapsearch -H ldaps://id.qo.is:636 -x -b 'dc=id,dc=qo,dc=is' '(name=<person>)'works on lindberg-webapps and fails from anywhere else
@ -9,0 +12,4 @@oauthRoles = {editors = [ "Editor" ];admins = [ "Admin" ];server-admins = [ "GrafanaAdmin" ];Just add a sysadmin group in kandidm, which is server-admins; no need for others at this point.
@ -124,2 +141,4 @@};# Single sign-on through kanidm. The local admin account stays available as a fallback.qois.kanidm.oauth2Clients.grafana = mkIf kanidm.enable {This will not be applied on hosts where kanidm.enable is true, but grafana.enable is false. Move the kanidm logic into
kanidm-grafananixos module; for grafana's config, keep it in here and don't rely on kanidm.enable (sso should be enabled by default. think how to make it easy to run tests without it, e.g. an enableSSO option or so?)@ -81,12 +82,30 @@ inqois.prometheus.enable = true;qois.loki.enable = true;qois.kanidm = {Move this test also to kanidm-grafana module :)
@ -0,0 +2,4 @@[Kanidm](https://kanidm.github.io/kanidm/stable/) runs on `lindberg-webapps` and serves[id.qo.is](https://id.qo.is). It is the identity provider for services that support OIDC;Grafana is currently the only relying party.Don't mention grafana and lindberg-webapps here.
@ -0,0 +4,4 @@[id.qo.is](https://id.qo.is). It is the identity provider for services that support OIDC;Grafana is currently the only relying party.Persons are **not** provisioned from this repository — create them in the web UI. GroupsIs this called persons in kanidm? Not users? (it should match, check)
@ -0,0 +21,4 @@'["kanidm"]["oauth2"]["grafana"]' "\"$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 48)\""```Afterwards publish the submodule as described in [the README](../../README.md):Remove the submodule mention, and "Both passwords are re-app..." - the user should know what they are doing.
@ -0,0 +36,4 @@Both passwords are re-applied on every start, so changing them in sops is enough.## First DeployRemove first deploy block
@ -0,0 +52,4 @@## AdministrationLog in at [id.qo.is/ui](https://id.qo.is/ui) as `idm_admin` with the password from sops toMake this a table with keywords only, no sentences
@ -0,0 +59,4 @@Group membership is appended by provisioning, never overwritten, so members added in theUI survive a redeploy. Removing a declared group from the repository does delete it.## Onboarding a ServiceRemove this section
@ -0,0 +78,4 @@and a sops secret `kanidm/oauth2/<service>` readable by kanidm and the service. Add thesecret as shown above before deploying.## LDAPCondense this section to the bash ldapsearch block only, no explanation except port is only open on localhost at this time
@ -0,0 +94,4 @@## Backup / Restore`/var/lib/kanidm` is part of the borg backup and holds both the live database and thenightly online backups kanidm writes to `/var/lib/kanidm/backups` (7 versions kept). OnlyReduce this versions to 2 (it's in borg after a day anyway). make this paragraph a bullet list with keywords only
@ -0,0 +29,4 @@cfg = config.qois.kanidm;stateDir = "/var/lib/kanidm";Make sure that kanidm does actually use this dir - or take the dir from some existing
config...option (e.g. kanidm config or systemd service config)@ -0,0 +92,4 @@package = mkOption {type = package;default = pkgs.kanidmWithSecretProvisioning_1_11;Add a note to the update docs about this
@ -0,0 +236,4 @@tls_key = tlsKey;http_client_address_info.x-forward-for = [ "::1" ];# Consistent database snapshots; copying the live sqlite file is not restore-safe.online_backup.versions = 7;Reduce this, it's in borg (mentioned earlier as well)
@ -0,0 +266,4 @@# The certificate only appears once ACME has issued it for the first time, so kanidm# keeps retrying until nginx' certificate has been copied over.systemd.services.kanidm.serviceConfig = {Is there a max retires by default? There should be some limit, but relatively generous compared to some other services (e.g. 10m)
@ -0,0 +271,4 @@RestartSec = 60;};systemd.tmpfiles.settings."10-qois-kanidm".${stateDir}.d = {What is this needed for?
@ -0,0 +48,4 @@};};# TODO: Migrate this to the testing helper acme serverresolve todo
All 17 comments addressed in
7f6ba3c.Module layering — new
nixos-modules/kanidm-grafana/owns the coupling: it declaresoptions.qois.grafana.sso(enabledefaults totrue), the kanidm OAuth2 client, and theshared client secret. The kanidm side is gated on
qois.kanidm.enable, so it applies on ahost running kanidm without grafana;
qois.grafanano longer readsconfig.qois.kanidmat all. Tests opt out with
sso.enable = false. The grafana subtest moved to the newmodule's own test.
Shared secret — added
qois.sharedSecretsFile(nixos-modules/meta/secrets.nix,pointing at the private submodule's shared sops file) so
kanidm/oauth2/grafanais asingle secret readable by both sides regardless of which host they land on. This needs the
migration in
private/noted in the PR description; without itlindberg-webappsdoes notbuild.
Roles — reduced to the
sysadmingroup mapping toGrafanaAdmin.rolesin theoauth2 client API now keys on real kanidm group names instead of deriving
<client>.<suffix>groups.kanidm/default.nixstateDirisdirOf services.kanidm.server.settings.db_path— the read-only optionkanidm actually uses.
online_backup.versions7 → 2.kanidm.serviceis now orderedafter/wantsacme-order-renew-id.qo.is.service, the unit whosepostRuninstalls the certificate,so the certificate is there before the first start.
install -din the cert script creates the directory, whichis needed because
postRunnow runs before systemd createsStateDirectory.updates.mdnotes the pinnedkanidmWithSecretProvisioning_1_11and that kanidm minorversions must be upgraded one at a time.
Test TODO — resolved.
kanidm/test.nixhas a pebble ACME node and no certificatemkForces left; the test downloads pebble's runtime-generated CA and asserts the issuedcertificate lands in
/var/lib/kanidmaskanidm:kanidm 0400.README — trimmed as requested: no host or service names in the intro, secrets section
is just the
sops setcommands, First Deploy and Onboarding sections gone, Administrationis a table, LDAP is the
ldapsearchblock plus the localhost note, Backup/Restore prosereplaced by bullets (numbered steps kept). Confirmed kanidm's own term is persons
(
services.kanidm.provision.persons,kanidm person create).7f6ba3caf568249ec4f268249ec4f29a6dff0868Second round addressed. Two commits:
Add telegraf serviceInputs optionandMove grafana single sign-on into the kanidm-grafana module.Module layering.
options.qois.grafana.ssois gone. The glue module now ownsoptions.qois.kanidm-grafana(enable,clientId,scopes,roles,secretFile) and configuresservices.grafana.settings."auth.generic_oauth"itself, including the JMESPath role mapping.enabledefaults to false and is set explicitly next toqois.kanidm.enableonlindberg-webapps, so neithergrafana/test.nixnorkanidm/test.nixmentions it any more. Endpoints come fromqois.kanidm.domain/qois.grafana.domain, which hold their defaults regardless ofenable, so a split-host setup still works.Secret scope.
qois.sharedSecretsFileis removed.sops-configgained a rule per host forprivate/nixos-modules/kanidm/<host>.sops.yaml, encrypted for that host pluskanidmHost. The generated.sops.yamlhas five hosts at two age keys andlindberg-webappsat one, since it is the kanidm host. The secret has been migrated out ofshared-secrets.kanidm module. postRun inlined, with
User/Groupread fromsystemd.services.kanidm.serviceConfigandinstallcoming from the acme unit's ownpath— no coreutils reference.extraSettings→settings. Firewall sentence dropped fromldapPort.networking.hostskept, so the local probes measure the local nginx rather than hairpinning through the loadbalancer.Observability. kanidm 1.11 links only the tracing half of the OTel SDK (
opentelemetry_sdk/src/trace/*,TracerProvider, noMeterProvider), sootel_grpc_endpointemits sampled spans and no metrics — it can't answer "is kanidm up". The module now runsinputs.http_responseagainst/status, which the test asserts instead ofx509_cert. Theid.qo.is/statusentry added to the central blackbox list in the first round is removed: on the host running both it was the identical probe twice, and the module-level one follows kanidm wherever it runs.Tests.
kanidm-grafananow uses a pebble node and real issuance for bothmonitoring.acme.testandid.acme.test; thetls_chain/tls_keyand nginx certificate overrides are gone, so the module's own certificate path is exercised, andcurlverifies properly rather than using-k.Keeping test telegraf config in sync. Rather than another "keep in sync" comment, service modules now declare
qois.telegraf.serviceInputs, and tests usemkForce config.qois.telegraf.serviceInputs. Thegit,postgresqlandloadbalancertests lose their copied literals too — including loadbalancer's second copy of the internalstatsIpPortconstant.Two things worth flagging that came out of verifying this:
serviceInputsastypes.attrsfirst — shallow//merge — silently dropped four of the fivex509_certdeclarations onlindberg-webapps. It now uses the same TOML format type asservices.telegraf.extraConfig, so lists concatenate. Verified against the generated config: vault, static-page, kanidm, grafana and git all present.SSL_CERT_FILEand restarts it once the file exists, since Go caches the pool on first handshake.Verified locally:
vm-test-run-{kanidm,kanidm-grafana,grafana,git,postgresql,loadbalancer,telegraf,prometheus,vault},formatting,deploy-activate,deploy-schema, and thelindberg-webappsbuild.40b26e020ad4324b45c9@ -17,2 +17,4 @@private.nixosModules.default];qois.kanidm.secretsFile = "${private}/nixos-modules/kanidm/${config.networking.hostName}.sops.yaml";move this config to the module (you can add another file and import it unconditionally)
@ -77,2 +77,4 @@enable = true;domain = serverDomain;# Single sign-on is covered by the kanidm-grafana module test.sso.enable = false;this option should be defined in this module.
@ -0,0 +4,4 @@...}:# Binds grafana to kanidm as an OIDC relying party. Both sides may live on different# hosts, so each half activates on its own and the shared client secret is declared here.Remove the comment
@ -0,0 +86,4 @@};config = mkIf cfg.enable (mkMerge [# The client is provisioned wherever kanidm runs, even if grafana runs on another host.Remove the comment
@ -0,0 +91,4 @@})(mkIf configureGrafana {qois.grafana.sso.secretFile = config.sops.secrets.${secretName}.path;Also configure the rest of the grafana sso config here, not in the grafana/default.nix.
@ -0,0 +125,4 @@sops.secrets.${secretName} = {sopsFile = kanidm.secretsFile;mode = "0440";owner = if kanidm.enable then "kanidm" else config.users.users.grafana.name;Do this in a less hacky way like described in https://raw.githubusercontent.com/Mic92/sops-nix/refs/heads/master/README.md
@ -0,0 +36,4 @@};qois.kanidm = {enable = true;use our kanidm module, it makes no sense to configure kanidm separately, then the test isn't worth much.
@ -0,0 +60,4 @@qois.postgresql.package = pkgs.postgresql;# Dummy sops file so secret paths resolve at eval time; nothing reads them atRemove this comment, it's obvious
@ -0,0 +62,4 @@installCert = pkgs.writeShellScript "kanidm-install-cert" ''# Runs before kanidm's first start, so systemd has not created StateDirectory yet.${pkgs.coreutils}/bin/install -d -o kanidm -g kanidm -m 0700 ${stateDir}This group and user name should be taken from the systemd service. Maybe it better do declare coreutils as builddep, so that we save some duplication here.
@ -0,0 +93,4 @@type = port;default = 636;description = ''Port of the LDAPS interface. No firewall port is opened for it, so it is onlyNo mention of firewall here
@ -0,0 +182,4 @@'';};extraSettings = mkOption {just name this settings.
@ -0,0 +231,4 @@"kanidm/idm-admin-password".owner = "kanidm";};# postRun of this unit installs the certificate kanidm needs to start.Remove the comment
@ -0,0 +240,4 @@};security.acme.certs.${cfg.domain} = {postRun = "${installCert}";declare ths postrun inline here, it's short enough.
@ -0,0 +250,4 @@{ sources = [ "https://${cfg.domain}:443" ]; }];networking.hosts."127.0.0.1" = [ cfg.domain ];is the localhost reference required?
@ -0,0 +62,4 @@sops.secrets = mkForce { };# Covered by the kanidm-grafana module test, and it would need a sops secret here.qois.grafana.sso.enable = false;This option should be named qois.kanidm-grafana.enable = false;
also, it should not be neccessary here since we'd manually define this along with qois.kanidm.enable = true; on the host it's actually used (and in that test). Remove any mention here and make sure this is true.
@ -0,0 +69,4 @@443];qois.telegraf.enable = mkForce true;Make sure telegraf only runs the modules we want in this tests, not the whole rest.
@ -0,0 +34,4 @@download_ca(client, caDomain)with subtest("acme-certificate"):# postRun installed the issued certificate where kanidm reads it.the subtest name should be clear enough so this comment is not needed.
@ -0,0 +80,4 @@with subtest("ldaps-port-isolation"):client.fail(ldapsearch(serverDomain))with subtest("telegraf-metrics"):The x509 is not relevant to check - https://kanidm.github.io/kanidm/stable/monitoring_the_platform.html however, the kanidm observability should be checked.
@ -0,0 +7,4 @@inherit (lib.types) path;in{options.qois.sharedSecretsFile = mkOption {The secrets should not be accessible for all hosts - just the one that runs grafana, and the one that kanidm is running on. Create an additional encryption config in
sops-configfor the kanidm module, that has one file per host and encrypts the secrets for them and the host kanidm is running on.View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.