nixpkgs/nixos/modules/services/misc/portunus.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

289 lines
9.1 KiB
Nix
Raw Normal View History

2022-07-18 22:11:41 +00:00
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.portunus;
in
{
options.services.portunus = {
enable = mkEnableOption (lib.mdDoc "Portunus, a self-contained user/group management and authentication service for LDAP");
2022-07-18 22:11:41 +00:00
domain = mkOption {
type = types.str;
example = "sso.example.com";
description = lib.mdDoc "Subdomain which gets reverse proxied to Portunus webserver.";
2022-07-18 22:11:41 +00:00
};
port = mkOption {
type = types.port;
default = 8080;
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
Port where the Portunus webserver should listen on.
This must be put behind a TLS-capable reverse proxy because Portunus only listens on localhost.
'';
};
package = mkOption {
type = types.package;
default = pkgs.portunus;
defaultText = "pkgs.portunus";
description = lib.mdDoc "The Portunus package to use.";
2022-07-18 22:11:41 +00:00
};
seedPath = mkOption {
type = types.nullOr types.path;
default = null;
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
Path to a portunus seed file in json format.
See <https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration> for available options.
2022-07-18 22:11:41 +00:00
'';
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/portunus";
description = lib.mdDoc "Path where Portunus stores its state.";
2022-07-18 22:11:41 +00:00
};
user = mkOption {
type = types.str;
default = "portunus";
description = lib.mdDoc "User account under which Portunus runs its webserver.";
2022-07-18 22:11:41 +00:00
};
group = mkOption {
type = types.str;
default = "portunus";
description = lib.mdDoc "Group account under which Portunus runs its webserver.";
2022-07-18 22:11:41 +00:00
};
dex = {
enable = mkEnableOption (lib.mdDoc ''
2022-07-18 22:11:41 +00:00
Dex ldap connector.
To activate dex, first a search user must be created in the Portunus web ui
and then the password must to be set as the `DEX_SEARCH_USER_PASSWORD` environment variable
in the [](#opt-services.dex.environmentFile) setting.
'');
2022-07-18 22:11:41 +00:00
oidcClients = mkOption {
type = types.listOf (types.submodule {
options = {
callbackURL = mkOption {
type = types.str;
description = lib.mdDoc "URL where the OIDC client should redirect";
2022-07-18 22:11:41 +00:00
};
id = mkOption {
type = types.str;
description = lib.mdDoc "ID of the OIDC client";
2022-07-18 22:11:41 +00:00
};
};
});
default = [ ];
example = [
{
callbackURL = "https://example.com/client/oidc/callback";
id = "service";
}
];
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
List of OIDC clients.
The OIDC secret must be set as the `DEX_CLIENT_''${id}` environment variable
in the [](#opt-services.dex.environmentFile) setting.
2022-07-18 22:11:41 +00:00
'';
};
port = mkOption {
type = types.port;
default = 5556;
description = lib.mdDoc "Port where dex should listen on.";
2022-07-18 22:11:41 +00:00
};
};
ldap = {
package = mkOption {
type = types.package;
default = pkgs.openldap;
defaultText = "pkgs.openldap";
description = lib.mdDoc "The OpenLDAP package to use.";
2022-07-18 22:11:41 +00:00
};
searchUserName = mkOption {
type = types.str;
default = "";
example = "admin";
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
The login name of the search user.
This user account must be configured in Portunus either manually or via seeding.
'';
};
suffix = mkOption {
type = types.str;
example = "dc=example,dc=org";
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
The DN of the topmost entry in your LDAP directory.
Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory.
'';
};
tls = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
2022-07-18 22:11:41 +00:00
Wether to enable LDAPS protocol.
This also adds two entries to the `/etc/hosts` file to point [](#opt-services.portunus.domain) to localhost,
2022-07-18 22:11:41 +00:00
so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol.
This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs).
2022-07-18 22:11:41 +00:00
'';
};
user = mkOption {
type = types.str;
default = "openldap";
description = lib.mdDoc "User account under which Portunus runs its LDAP server.";
2022-07-18 22:11:41 +00:00
};
group = mkOption {
type = types.str;
default = "openldap";
description = lib.mdDoc "Group account under which Portunus runs its LDAP server.";
2022-07-18 22:11:41 +00:00
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.dex.enable -> cfg.ldap.searchUserName != "";
message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set.";
}
];
# add ldapsearch(1) etc. to interactive shells
environment.systemPackages = [ cfg.ldap.package ];
# allow connecting via ldaps /w certificate without opening ports
networking.hosts = mkIf cfg.ldap.tls {
"::1" = [ cfg.domain ];
"127.0.0.1" = [ cfg.domain ];
};
services.dex = mkIf cfg.dex.enable {
enable = true;
settings = {
issuer = "https://${cfg.domain}/dex";
web.http = "127.0.0.1:${toString cfg.dex.port}";
storage = {
type = "sqlite3";
config.file = "/var/lib/dex/dex.db";
};
enablePasswordDB = false;
connectors = [{
type = "ldap";
id = "ldap";
name = "LDAP";
config = {
host = "${cfg.domain}:636";
bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}";
bindPW = "$DEX_SEARCH_USER_PASSWORD";
userSearch = {
baseDN = "ou=users,${cfg.ldap.suffix}";
filter = "(objectclass=person)";
username = "uid";
idAttr = "uid";
emailAttr = "mail";
nameAttr = "cn";
preferredUsernameAttr = "uid";
};
groupSearch = {
baseDN = "ou=groups,${cfg.ldap.suffix}";
filter = "(objectclass=groupOfNames)";
nameAttr = "cn";
userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }];
};
};
}];
staticClients = forEach cfg.dex.oidcClients (client: {
inherit (client) id;
redirectURIs = [ client.callbackURI ];
name = "OIDC for ${client.id}";
secret = "$DEX_CLIENT_${client.id}";
});
};
};
systemd.services = {
dex.serviceConfig = mkIf cfg.dex.enable {
# `dex.service` is super locked down out of the box, but we need some
# place to write the SQLite database. This creates $STATE_DIRECTORY below
# /var/lib/private because DynamicUser=true, but it gets symlinked into
# /var/lib/dex inside the unit
StateDirectory = "dex";
};
portunus = {
description = "Self-contained authentication service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator";
environment = {
PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
PORTUNUS_SERVER_GROUP = cfg.group;
PORTUNUS_SERVER_USER = cfg.user;
PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
PORTUNUS_SLAPD_USER = cfg.ldap.user;
PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema";
} // (optionalAttrs (cfg.seedPath != null) ({
PORTUNUS_SEED_PATH = cfg.seedPath;
})) // (optionalAttrs cfg.ldap.tls (
let
acmeDirectory = config.security.acme.certs."${cfg.domain}".directory;
in
{
PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = "/etc/ssl/certs/ca-certificates.crt";
PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem";
PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain;
PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem";
}));
};
};
users.users = mkMerge [
(mkIf (cfg.ldap.user == "openldap") {
openldap = {
group = cfg.ldap.group;
isSystemUser = true;
};
})
(mkIf (cfg.user == "portunus") {
portunus = {
group = cfg.group;
isSystemUser = true;
};
})
];
users.groups = mkMerge [
(mkIf (cfg.ldap.user == "openldap") {
openldap = { };
})
(mkIf (cfg.user == "portunus") {
portunus = { };
})
];
};
2022-09-03 03:20:07 +00:00
meta.maintainers = [ maintainers.majewsky ] ++ teams.c3d2.members;
2022-07-18 22:11:41 +00:00
}