style: format tree

This commit is contained in:
nhnn
2025-06-18 19:53:44 +03:00
parent ed990906bd
commit 86233cac27
44 changed files with 2523 additions and 2253 deletions

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
inherit (lib) inherit (lib)
mkOption mkOption
@@ -7,12 +12,12 @@ let
auth-passthru = config.selfprivacy.passthru.auth; auth-passthru = config.selfprivacy.passthru.auth;
keys-path = auth-passthru.keys-path; keys-path = auth-passthru.keys-path;
# generate OAuth2 client secret # generate OAuth2 client secret
mkKanidmExecStartPreScript = oauthClientID: linuxGroup: mkKanidmExecStartPreScript =
oauthClientID: linuxGroup:
let let
secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup; secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup;
in in
pkgs.writeShellScript pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" ''
"${oauthClientID}-kanidm-ExecStartPre-script.sh" ''
set -o pipefail set -o pipefail
set -o errexit set -o errexit
if ! [ -f "${secretFP}" ] if ! [ -f "${secretFP}" ]
@@ -22,17 +27,16 @@ let
chmod 640 "${secretFP}" chmod 640 "${secretFP}"
fi fi
''; '';
mkKanidmExecStartPostScript = oauthClientID: linuxGroup: isMailserver: mkKanidmExecStartPostScript =
oauthClientID: linuxGroup: isMailserver:
let let
kanidmServiceAccountName = "sp.${oauthClientID}.service-account"; kanidmServiceAccountName = "sp.${oauthClientID}.service-account";
kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token";
kanidmServiceAccountTokenFP = kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroup;
auth-passthru.mkServiceAccountTokenFP linuxGroup;
isRW = oauthClientID == "selfprivacy-api"; isRW = oauthClientID == "selfprivacy-api";
in in
pkgs.writeShellScript pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" (
"${oauthClientID}-kanidm-ExecStartPost-script.sh" ''
(''
export HOME=$RUNTIME_DIRECTORY/client_home export HOME=$RUNTIME_DIRECTORY/client_home
readonly KANIDM="${pkgs.kanidm}/bin/kanidm" readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
@@ -82,13 +86,12 @@ let
+ lib.strings.optionalString isRW '' + lib.strings.optionalString isRW ''
$KANIDM group add-members idm_admins "${kanidmServiceAccountName}" $KANIDM group add-members idm_admins "${kanidmServiceAccountName}"
'' ''
); );
in in
{ {
options.selfprivacy.auth = { options.selfprivacy.auth = {
clients = mkOption { clients = mkOption {
description = description = "Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated.";
"Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated.";
default = { }; default = { };
type = types.attrsOf ( type = types.attrsOf (
types.submodule { types.submodule {
@@ -107,69 +110,58 @@ in
}; };
enablePkce = mkOption { enablePkce = mkOption {
type = lib.types.bool; type = lib.types.bool;
description = description = "Whether PKCE must be used between client and Kanidm.";
"Whether PKCE must be used between client and Kanidm.";
default = false; default = false;
}; };
adminsGroup = mkOption { adminsGroup = mkOption {
type = type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins");
types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins"); description = "Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization.";
description =
"Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization.";
default = null; default = null;
}; };
usersGroup = mkOption { usersGroup = mkOption {
type = type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users");
types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users"); description = "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization.";
description =
"Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization.";
default = null; default = null;
}; };
originLanding = mkOption { originLanding = mkOption {
type = types.nullOr lib.types.str; type = types.nullOr lib.types.str;
description = description = "The origin landing of the service for OAuth2 redirects.";
"The origin landing of the service for OAuth2 redirects.";
}; };
originUrl = mkOption { originUrl = mkOption {
type = types.nullOr lib.types.str; type = types.nullOr lib.types.str;
description = description = "The origin URL of the service for OAuth2 redirects.";
"The origin URL of the service for OAuth2 redirects.";
}; };
subdomain = lib.mkOption { subdomain = lib.mkOption {
type = type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain of the service."; description = "Subdomain of the service.";
}; };
# when true, "name" is passed to a service instead of "name@domain" # when true, "name" is passed to a service instead of "name@domain"
useShortPreferredUsername = mkOption { useShortPreferredUsername = mkOption {
description = description = "Use 'name' instead of 'spn' in the preferred_username claim.";
"Use 'name' instead of 'spn' in the preferred_username claim.";
type = types.bool; type = types.bool;
default = true; default = true;
}; };
linuxUserOfClient = mkOption { linuxUserOfClient = mkOption {
type = types.nullOr lib.types.str; type = types.nullOr lib.types.str;
description = description = "Name of a Linux OAuth2 client user, under which it should get access through a folder with keys.";
"Name of a Linux OAuth2 client user, under which it should get access through a folder with keys.";
default = null; default = null;
}; };
linuxGroupOfClient = mkOption { linuxGroupOfClient = mkOption {
type = types.nullOr lib.types.str; type = types.nullOr lib.types.str;
description = description = "Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file.";
"Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file.";
default = null; default = null;
}; };
isTokenNeeded = mkOption { isTokenNeeded = mkOption {
description = description = "Whether a read-only needs to be generated for LDAP access.";
"Whether a read-only needs to be generated for LDAP access.";
type = types.bool; type = types.bool;
default = false; default = false;
}; };
clientSystemdUnits = mkOption { clientSystemdUnits = mkOption {
description = "A list of systemd services, which depend on OAuth service"; description = "A list of systemd services, which depend on OAuth service";
# taken from nixos/lib/systemd-lib.nix: unitNameType # taken from nixos/lib/systemd-lib.nix: unitNameType
type = types.listOf type = types.listOf (
(types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)"); types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)"
);
}; };
scopeMaps = mkOption { scopeMaps = mkOption {
description = '' description = ''
@@ -231,133 +223,132 @@ in
}; };
config = lib.mkIf config.selfprivacy.sso.enable ( config = lib.mkIf config.selfprivacy.sso.enable (
let let
clientsAttrsList = lib.attrsets.mapAttrsToList clientsAttrsList = lib.attrsets.mapAttrsToList (
(name: attrs: attrs // rec { name: attrs:
clientID = attrs
if attrs.clientID == null // rec {
then name clientID = if attrs.clientID == null then name else attrs.clientID;
else attrs.clientID; displayName = if attrs.displayName == null then clientID else attrs.displayName;
displayName = adminsGroup = if attrs.adminsGroup == null then "sp.${clientID}.admins" else attrs.adminsGroup;
if attrs.displayName == null usersGroup = if attrs.usersGroup == null then "sp.${clientID}.users" else attrs.usersGroup;
then clientID basicSecretFile = "${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret";
else attrs.displayName; linuxUserOfClient = if attrs.linuxUserOfClient == null then clientID else attrs.linuxUserOfClient;
adminsGroup =
if attrs.adminsGroup == null
then "sp.${clientID}.admins"
else attrs.adminsGroup;
usersGroup =
if attrs.usersGroup == null
then "sp.${clientID}.users"
else attrs.usersGroup;
basicSecretFile =
"${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret";
linuxUserOfClient =
if attrs.linuxUserOfClient == null
then clientID
else attrs.linuxUserOfClient;
linuxGroupOfClient = linuxGroupOfClient =
if attrs.linuxGroupOfClient == null if attrs.linuxGroupOfClient == null then clientID else attrs.linuxGroupOfClient;
then clientID
else attrs.linuxGroupOfClient;
originLanding = originLanding =
if attrs.originLanding == null if attrs.originLanding == null then
then "https://${attrs.subdomain}.${config.selfprivacy.domain}/" "https://${attrs.subdomain}.${config.selfprivacy.domain}/"
else attrs.originLanding; else
attrs.originLanding;
scopeMaps = scopeMaps =
if attrs.scopeMaps == null if attrs.scopeMaps == null then
then { "${usersGroup}" = [ "email" "openid" "profile" ]; } {
else attrs.scopeMaps; "${usersGroup}" = [
}) "email"
config.selfprivacy.auth.clients; "openid"
"profile"
];
}
else
attrs.scopeMaps;
}
) config.selfprivacy.auth.clients;
in in
{ {
# for each OAuth2 client: member of the `keys` group for directory access # for each OAuth2 client: member of the `keys` group for directory access
users.groups.keys.members = lib.mkMerge (lib.forEach users.groups.keys.members = lib.mkMerge (
clientsAttrsList lib.forEach clientsAttrsList ({ linuxUserOfClient, ... }: [ linuxUserOfClient ])
({ linuxUserOfClient, ... }: [ linuxUserOfClient ])
); );
systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (lib.forEach systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (
clientsAttrsList lib.forEach clientsAttrsList (
({ linuxGroupOfClient, ... }: { { linuxGroupOfClient, ... }:
"${keys-path}/${linuxGroupOfClient}".d = { {
user = "kanidm"; "${keys-path}/${linuxGroupOfClient}".d = {
group = linuxGroupOfClient; user = "kanidm";
mode = "2750"; group = linuxGroupOfClient;
}; mode = "2750";
}) };
}
)
); );
# for each OAuth2 client: scripts with Kanidm CLI commands # for each OAuth2 client: scripts with Kanidm CLI commands
systemd.services.kanidm = { systemd.services.kanidm = {
before = before = lib.lists.concatMap ({ clientSystemdUnits, ... }: clientSystemdUnits) clientsAttrsList;
lib.lists.concatMap serviceConfig = lib.mkMerge (
({ clientSystemdUnits, ... }: clientSystemdUnits) lib.forEach clientsAttrsList (
clientsAttrsList; {
serviceConfig = clientID,
lib.mkMerge (lib.forEach isTokenNeeded,
clientsAttrsList linuxGroupOfClient,
({ clientID, isTokenNeeded, linuxGroupOfClient, isMailserver, ... }: isMailserver,
{ ...
ExecStartPre = [ }:
# "-" prefix means to ignore exit code of prefixed script {
("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) ExecStartPre = [
]; # "-" prefix means to ignore exit code of prefixed script
ExecStartPost = lib.mkIf isTokenNeeded ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient)
(lib.mkAfter [ ];
("-" + ExecStartPost = lib.mkIf isTokenNeeded (
mkKanidmExecStartPostScript lib.mkAfter [
clientID ("-" + mkKanidmExecStartPostScript clientID linuxGroupOfClient isMailserver)
linuxGroupOfClient ]
isMailserver) );
]); }
})); )
);
}; };
# for each OAuth2 client: Kanidm provisioning options # for each OAuth2 client: Kanidm provisioning options
services.kanidm.provision = lib.mkMerge (lib.forEach services.kanidm.provision = lib.mkMerge (
clientsAttrsList lib.forEach clientsAttrsList (
({ adminsGroup {
, basicSecretFile adminsGroup,
, claimMaps basicSecretFile,
, clientID claimMaps,
, displayName clientID,
, enablePkce displayName,
, imageFile enablePkce,
, originLanding imageFile,
, originUrl originLanding,
, scopeMaps originUrl,
, useShortPreferredUsername scopeMaps,
, usersGroup useShortPreferredUsername,
, ... usersGroup,
}: { ...
groups = lib.mkIf (clientID != "selfprivacy-api") { }:
"${adminsGroup}".members = {
[ auth-passthru.admins-group ]; groups = lib.mkIf (clientID != "selfprivacy-api") {
"${usersGroup}".members = "${adminsGroup}".members = [ auth-passthru.admins-group ];
[ adminsGroup auth-passthru.full-users-group ]; "${usersGroup}".members = [
}; adminsGroup
systems.oauth2.${clientID} = { auth-passthru.full-users-group
inherit ];
basicSecretFile };
claimMaps systems.oauth2.${clientID} = {
displayName inherit
imageFile basicSecretFile
originLanding claimMaps
originUrl displayName
scopeMaps imageFile
; originLanding
preferShortUsername = useShortPreferredUsername; originUrl
allowInsecureClientDisablePkce = ! enablePkce; scopeMaps
removeOrphanedClaimMaps = true; ;
preferShortUsername = useShortPreferredUsername;
allowInsecureClientDisablePkce = !enablePkce;
removeOrphanedClaimMaps = true;
# NOTE https://github.com/oddlama/kanidm-provision/issues/15 # NOTE https://github.com/oddlama/kanidm-provision/issues/15
# add more scopes when a user is a member of specific group # add more scopes when a user is a member of specific group
# currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144
# supplementaryScopeMaps."${admins-group}" = # supplementaryScopeMaps."${admins-group}" =
# [ "read:admin" "write:admin" ]; # [ "read:admin" "write:admin" ];
}; };
})); }
)
);
} }
); );
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
subdomain = "auth"; subdomain = "auth";
@@ -38,7 +43,6 @@ lib.mkIf config.selfprivacy.sso.enable {
"127.0.0.1" = [ auth-fqdn ]; "127.0.0.1" = [ auth-fqdn ];
}; };
# kanidm uses TLS in internal connection with nginx too # kanidm uses TLS in internal connection with nginx too
# FIXME revise this: maybe kanidm must not have access to a public TLS # FIXME revise this: maybe kanidm must not have access to a public TLS
users.groups."acmereceivers".members = [ "kanidm" ]; users.groups."acmereceivers".members = [ "kanidm" ];
@@ -69,16 +73,13 @@ lib.mkIf config.selfprivacy.sso.enable {
origin = "https://" + auth-fqdn; origin = "https://" + auth-fqdn;
# TODO revise this: maybe kanidm must not have access to a public TLS # TODO revise this: maybe kanidm must not have access to a public TLS
tls_chain = tls_chain = "${config.security.acme.certs.${domain}.directory}/fullchain.pem";
"${config.security.acme.certs.${domain}.directory}/fullchain.pem"; tls_key = "${config.security.acme.certs.${domain}.directory}/key.pem";
tls_key =
"${config.security.acme.certs.${domain}.directory}/key.pem";
# nginx should proxy requests to it # nginx should proxy requests to it
bindaddress = kanidm-bind-address; bindaddress = kanidm-bind-address;
ldapbindaddress = ldapbindaddress = "${ldap-host}:${toString ldap-port}";
"${ldap-host}:${toString ldap-port}";
# kanidm is behind a proxy # kanidm is behind a proxy
trust_x_forward_for = true; trust_x_forward_for = true;
@@ -101,8 +102,7 @@ lib.mkIf config.selfprivacy.sso.enable {
services.nginx = { services.nginx = {
enable = true; enable = true;
additionalModules = additionalModules = lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ];
lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ];
commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug '' commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug ''
log_format kanidm escape=none '$request $status\n' log_format kanidm escape=none '$request $status\n'
'[Request body]: $request_body\n' '[Request body]: $request_body\n'
@@ -158,8 +158,7 @@ lib.mkIf config.selfprivacy.sso.enable {
systemd.services.kanidm.serviceConfig.ExecStartPre = systemd.services.kanidm.serviceConfig.ExecStartPre =
# idempotent script to run on each startup only for kanidm v1.5.0 # idempotent script to run on each startup only for kanidm v1.5.0
lib.mkIf (pkgs.kanidm.version == "1.5.0") lib.mkIf (pkgs.kanidm.version == "1.5.0") (lib.mkBefore [ kanidmMigrateDbScript ]);
(lib.mkBefore [ kanidmMigrateDbScript ]);
selfprivacy.passthru.auth = { selfprivacy.passthru.auth = {
inherit inherit
@@ -171,25 +170,20 @@ lib.mkIf config.selfprivacy.sso.enable {
keys-path keys-path
; ;
oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; oauth2-introspection-url-prefix = client_id: "https://${client_id}:";
oauth2-introspection-url-postfix = oauth2-introspection-url-postfix = "@${auth-fqdn}/oauth2/token/introspect";
"@${auth-fqdn}/oauth2/token/introspect"; oauth2-discovery-url =
oauth2-discovery-url = client_id: client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration";
"https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration";
oauth2-provider-name = "Kanidm"; oauth2-provider-name = "Kanidm";
oauth2-systemd-service = "kanidm.service"; oauth2-systemd-service = "kanidm.service";
# e.g. "dc=mydomain,dc=com" # e.g. "dc=mydomain,dc=com"
ldap-base-dn = ldap-base-dn = lib.strings.concatMapStringsSep "," (x: "dc=" + x) (
lib.strings.concatMapStringsSep lib.strings.splitString "." domain
"," );
(x: "dc=" + x)
(lib.strings.splitString "." domain);
# TODO consider to pass a value or throw exception if token is not generated # TODO consider to pass a value or throw exception if token is not generated
mkServiceAccountTokenFP = linuxGroup: mkServiceAccountTokenFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-service-account-token";
"${keys-path}/${linuxGroup}/kanidm-service-account-token";
mkOAuth2ClientSecretFP = linuxGroup: mkOAuth2ClientSecretFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-oauth-client-secret";
"${keys-path}/${linuxGroup}/kanidm-oauth-client-secret";
}; };
} }

View File

@@ -1,8 +1,9 @@
{ config {
, lib config,
, options lib,
, pkgs options,
, ... pkgs,
...
}: }:
let let
inherit (lib) inherit (lib)
@@ -40,7 +41,9 @@ let
cfg = config.services.kanidm; cfg = config.services.kanidm;
settingsFormat = pkgs.formats.toml { }; settingsFormat = pkgs.formats.toml { };
# Remove null values, so we can document optional values that don't end up in the generated TOML file. # Remove null values, so we can document optional values that don't end up in the generated TOML file.
filterConfig = converge (a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ])); filterConfig = converge (
a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ])
);
serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
@@ -54,17 +57,16 @@ let
# paths, no new bind mount is added. Adding subpaths caused problems on ofborg. # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
hasPrefixInList = hasPrefixInList =
list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
mergePaths = foldl' mergePaths = foldl' (
( merged: newPath:
merged: newPath: let
let # If the new path is a prefix to some existing path, we need to filter it out
# If the new path is a prefix to some existing path, we need to filter it out filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; # If a prefix of the new path is already in the list, do not add it
# If a prefix of the new path is already in the list, do not add it filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; in
in filteredPaths ++ filteredNew
filteredPaths ++ filteredNew ) [ ];
) [ ];
defaultServiceConfig = { defaultServiceConfig = {
# Setting the type to notify enables additional healthchecks, ensuring units # Setting the type to notify enables additional healthchecks, ensuring units
@@ -127,19 +129,20 @@ let
filterPresent = filterAttrs (_: v: v.present); filterPresent = filterAttrs (_: v: v.present);
selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$"; selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$";
is-selfprivacy-admin-group = name: is-selfprivacy-admin-group =
! builtins.isNull (builtins.match selfprivacy-admin-groups-regex name); name: !builtins.isNull (builtins.match selfprivacy-admin-groups-regex name);
isGroupNonOverwritable = g: false isGroupNonOverwritable =
|| ! g ? members g:
false
|| !g ? members
|| g ? members && g.members == [ ] || g ? members && g.members == [ ]
|| g ? members && builtins.any is-selfprivacy-admin-group g.members; || g ? members && builtins.any is-selfprivacy-admin-group g.members;
provisionStateJson = pkgs.writeText "provision-state.json" ( provisionStateJson = pkgs.writeText "provision-state.json" (
builtins.toJSON { builtins.toJSON {
inherit (cfg.provision) persons systems; inherit (cfg.provision) persons systems;
groups = groups = lib.attrsets.filterAttrs (_n: v: !isGroupNonOverwritable v) cfg.provision.groups;
lib.attrsets.filterAttrs (_n: v: ! isGroupNonOverwritable v) cfg.provision.groups;
} }
); );
@@ -182,8 +185,9 @@ let
fi fi
''; '';
groupsToCreateAndPopulate = groupsToCreateAndPopulate = lib.attrsets.filterAttrs (
lib.attrsets.filterAttrs (_n: isGroupNonOverwritable) cfg.provision.groups; _n: isGroupNonOverwritable
) cfg.provision.groups;
createGroups = '' createGroups = ''
for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)} for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)}
@@ -199,9 +203,9 @@ let
done done
''; '';
createAndPopulateGroups = createAndPopulateGroups = lib.concatLines (
lib.concatLines ([ createGroups ] [ createGroups ] ++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate)
++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate)); );
postStartScript = pkgs.writeShellScript "post-start" '' postStartScript = pkgs.writeShellScript "post-start" ''
set -euo pipefail set -euo pipefail
@@ -250,11 +254,11 @@ let
last (splitString "]:" cfg.serverSettings.bindaddress) last (splitString "]:" cfg.serverSettings.bindaddress)
else else
# ipv4: # ipv4:
if hasInfix "." cfg.serverSettings.bindaddress then if hasInfix "." cfg.serverSettings.bindaddress then
last (splitString ":" cfg.serverSettings.bindaddress) last (splitString ":" cfg.serverSettings.bindaddress)
# default is 8443 # default is 8443
else else
"8443"; "8443";
in in
{ {
options.services.kanidm = { options.services.kanidm = {
@@ -476,11 +480,9 @@ in
config.members = concatLists ( config.members = concatLists (
flip mapAttrsToList cfg.provision.persons ( flip mapAttrsToList cfg.provision.persons (
person: personCfg: person: personCfg:
optional optional (
( personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups ) person
)
person
) )
); );
}) })
@@ -683,12 +685,9 @@ in
++ entityList "oauth2" cfg.provision.systems.oauth2; ++ entityList "oauth2" cfg.provision.systems.oauth2;
# Accumulate entities by name. Track corresponding entity types for later duplicate check. # Accumulate entities by name. Track corresponding entity types for later duplicate check.
entitiesByName = foldl' entitiesByName = foldl' (
( acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } ) { } entities;
)
{ }
entities;
assertGroupsKnown = assertGroupsKnown =
opt: groups: opt: groups:
@@ -800,59 +799,59 @@ in
] ]
++ flip mapAttrsToList (filterPresent cfg.provision.persons) ( ++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
person: personCfg: person: personCfg:
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
) )
++ flip mapAttrsToList (filterPresent cfg.provision.groups) ( ++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
group: groupCfg: group: groupCfg:
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
) )
++ concatLists ( ++ concatLists (
flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
oauth2: oauth2Cfg: oauth2: oauth2Cfg:
[ [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
attrNames oauth2Cfg.scopeMaps attrNames oauth2Cfg.scopeMaps
)) ))
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
attrNames oauth2Cfg.supplementaryScopeMaps attrNames oauth2Cfg.supplementaryScopeMaps
)) ))
] ]
++ concatLists ( ++ concatLists (
flip mapAttrsToList oauth2Cfg.claimMaps ( flip mapAttrsToList oauth2Cfg.claimMaps (
claim: claimCfg: [ claim: claimCfg: [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
attrNames claimCfg.valuesByGroup attrNames claimCfg.valuesByGroup
)) ))
# At least one group must map to a value in each claim map # At least one group must map to a value in each claim map
{ {
assertion = assertion =
(cfg.provision.enable && cfg.enableServer) (cfg.provision.enable && cfg.enableServer)
-> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup);
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
} }
# Public clients cannot define a basic secret # Public clients cannot define a basic secret
{ {
assertion = assertion =
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
} }
# Public clients cannot disable PKCE # Public clients cannot disable PKCE
{ {
assertion = assertion =
(cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
-> !oauth2Cfg.allowInsecureClientDisablePkce; -> !oauth2Cfg.allowInsecureClientDisablePkce;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
} }
# Non-public clients cannot enable localhost redirects # Non-public clients cannot enable localhost redirects
{ {
assertion = assertion =
(cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
-> !oauth2Cfg.enableLocalhostRedirects; -> !oauth2Cfg.enableLocalhostRedirects;
message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
} }
] ]
)
) )
)
) )
); );

View File

@@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
redis-sp-api-srv-name = "sp-api"; redis-sp-api-srv-name = "sp-api";
sp-print-api-token = pkgs.writeShellApplication { sp-print-api-token = pkgs.writeShellApplication {
@@ -76,7 +81,8 @@ in
}; };
}; };
services.do-agent.enable = if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false; services.do-agent.enable =
if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false;
boot.tmp.cleanOnBoot = true; boot.tmp.cleanOnBoot = true;
networking = { networking = {
@@ -84,14 +90,31 @@ in
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
usePredictableInterfaceNames = false; usePredictableInterfaceNames = false;
firewall = { firewall = {
allowedTCPPorts = [ 22 25 80 143 443 465 587 993 4443 8443 ]; allowedTCPPorts = [
allowedUDPPorts = [ 8443 10000 ]; 22
25
80
143
443
465
587
993
4443
8443
];
allowedUDPPorts = [
8443
10000
];
extraCommands = '' extraCommands = ''
iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE
iptables --append FORWARD --in-interface vpn00 -j ACCEPT iptables --append FORWARD --in-interface vpn00 -j ACCEPT
''; '';
}; };
nameservers = [ "1.1.1.1" "1.0.0.1" ]; nameservers = [
"1.1.1.1"
"1.0.0.1"
];
}; };
time.timeZone = config.selfprivacy.timezone; time.timeZone = config.selfprivacy.timezone;
i18n.defaultLocale = "en_GB.UTF-8"; i18n.defaultLocale = "en_GB.UTF-8";
@@ -107,8 +130,15 @@ in
}; };
services.fail2ban.enable = true; services.fail2ban.enable = true;
programs.ssh = { programs.ssh = {
pubkeyAcceptedKeyTypes = [ "ssh-ed25519" "ssh-rsa" "ecdsa-sha2-nistp256" ]; pubkeyAcceptedKeyTypes = [
hostKeyAlgorithms = [ "ssh-ed25519" "ssh-rsa" ]; "ssh-ed25519"
"ssh-rsa"
"ecdsa-sha2-nistp256"
];
hostKeyAlgorithms = [
"ssh-ed25519"
"ssh-rsa"
];
}; };
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
git git
@@ -124,9 +154,9 @@ in
"R! /old-root" "R! /old-root"
"d /etc/selfprivacy/dump 0700 0700 selfprivacy-api selfprivacy-api" "d /etc/selfprivacy/dump 0700 0700 selfprivacy-api selfprivacy-api"
]; ];
system.stateVersion = system.stateVersion = lib.mkIf (
lib.mkIf (config.selfprivacy.stateVersion != null) config.selfprivacy.stateVersion != null
config.selfprivacy.stateVersion; ) config.selfprivacy.stateVersion;
system.autoUpgrade = { system.autoUpgrade = {
enable = config.selfprivacy.autoUpgrade.enable; enable = config.selfprivacy.autoUpgrade.enable;
allowReboot = config.selfprivacy.autoUpgrade.allowReboot; allowReboot = config.selfprivacy.autoUpgrade.allowReboot;
@@ -168,7 +198,11 @@ in
}; };
nix.settings = { nix.settings = {
sandbox = true; sandbox = true;
experimental-features = [ "nix-command" "flakes" "repl-flake" ]; experimental-features = [
"nix-command"
"flakes"
"repl-flake"
];
# auto-optimise-store = true; # auto-optimise-store = true;
# evaluation restrictions: # evaluation restrictions:

258
flake.nix
View File

@@ -2,148 +2,156 @@
description = "SelfPrivacy NixOS configuration flake"; description = "SelfPrivacy NixOS configuration flake";
inputs = { inputs = {
nixpkgs.url = github:nixos/nixpkgs; nixpkgs.url = "github:nixos/nixpkgs";
nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable; nixos-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
selfprivacy-api.url = selfprivacy-api.url = "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git";
git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git;
# make selfprivacy-api use the same shared nixpkgs # make selfprivacy-api use the same shared nixpkgs
selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: { outputs =
nixosConfigurations-fun = {
{ hardware-configuration self,
, deployment nixpkgs,
, userdata nixos-unstable,
, top-level-flake selfprivacy-api,
, sp-modules }:
}: {
{ nixosConfigurations-fun =
default = nixpkgs.lib.nixosSystem { {
modules = [ hardware-configuration,
hardware-configuration deployment,
deployment userdata,
./configuration.nix top-level-flake,
./auth/auth.nix sp-modules,
{ }:
nixpkgs.overlays = [ {
( default = nixpkgs.lib.nixosSystem {
_final: prev: modules =
{ [
hardware-configuration
deployment
./configuration.nix
./auth/auth.nix
{
nixpkgs.overlays = [
(_final: prev: {
inherit (nixos-unstable.legacyPackages.${prev.system}) inherit (nixos-unstable.legacyPackages.${prev.system})
kanidm kanidm
kanidm-provision kanidm-provision
; ;
} })
];
disabledModules = [ "services/security/kanidm.nix" ];
imports = [ ./auth/kanidm.nix ];
}
selfprivacy-api.nixosModules.default
(
{ pkgs, lib, ... }:
{
environment.etc =
(lib.attrsets.mapAttrs' (name: sp-module: {
name = "sp-modules/${name}";
value.text = import ./lib/meta.nix { inherit pkgs sp-module; };
}) sp-modules)
// {
suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules));
};
}
) )
]; (
disabledModules = [ "services/security/kanidm.nix" ]; let
imports = [ ./auth/kanidm.nix ]; deepFilter =
} ref: attrset:
selfprivacy-api.nixosModules.default builtins.foldl' (
({ pkgs, lib, ... }: { acc: key:
environment.etc = (lib.attrsets.mapAttrs' if builtins.hasAttr key ref then
(name: sp-module: { let
name = "sp-modules/${name}"; value = attrset.${key};
value.text = import ./lib/meta.nix { inherit pkgs sp-module; }; refValue = ref.${key};
}) in
sp-modules) // { acc
suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules)); // {
}; ${key} =
}) if builtins.isAttrs value && builtins.isAttrs refValue then deepFilter refValue value else value;
( }
let else
deepFilter = ref: attrset: acc
builtins.foldl' ) { } (builtins.attrNames attrset);
(acc: key: in
if builtins.hasAttr key ref then { options, ... }:
let {
value = attrset.${key}; # pass userdata (parsed from JSON) options to selfprivacy module
refValue = ref.${key}; selfprivacy = deepFilter options.selfprivacy userdata;
in
acc // {
${key} =
if builtins.isAttrs value && builtins.isAttrs refValue then
deepFilter refValue value
else
value;
}
else
acc
)
{ }
(builtins.attrNames attrset);
in
{ options, ... }: {
# pass userdata (parsed from JSON) options to selfprivacy module
selfprivacy = deepFilter options.selfprivacy userdata;
# embed top-level flake source folder into the build # embed top-level flake source folder into the build
environment.etc."selfprivacy/nixos-config-source".source = environment.etc."selfprivacy/nixos-config-source".source = top-level-flake;
top-level-flake;
# for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc # for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc
nix.registry.nixpkgs.flake = nixpkgs; nix.registry.nixpkgs.flake = nixpkgs;
# embed commit sha1 for `nixos-version --configuration-revision` # embed commit sha1 for `nixos-version --configuration-revision`
system.configurationRevision = self.rev system.configurationRevision = self.rev or "@${self.lastModifiedDate}"; # for development
or "@${self.lastModifiedDate}"; # for development # TODO assertion to forbid dirty builds caused by top-level-flake
# TODO assertion to forbid dirty builds caused by top-level-flake
# reset contents of /etc/nixos to match running NixOS generation # reset contents of /etc/nixos to match running NixOS generation
system.activationScripts.selfprivacy-nixos-config-source = '' system.activationScripts.selfprivacy-nixos-config-source = ''
rm -rf /etc/nixos/{*,.[!.]*} rm -rf /etc/nixos/{*,.[!.]*}
cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/ cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/
''; '';
} }
) )
] ]
++ ++
# add SP modules, but constrain available config attributes for each # add SP modules, but constrain available config attributes for each
# (TODO revise evaluation performance of the code below) # (TODO revise evaluation performance of the code below)
nixpkgs.lib.attrsets.mapAttrsToList nixpkgs.lib.attrsets.mapAttrsToList (
(name: sp-module: args@{ config, pkgs, ... }: name: sp-module:
let args@{ config, pkgs, ... }:
lib = nixpkgs.lib; let
configPathsNeeded = sp-module.configPathsNeeded or lib = nixpkgs.lib;
(abort "allowed config paths not set for module \"${name}\""); configPathsNeeded =
constrainConfigArgs = args'@{ pkgs, ... }: args' // { sp-module.configPathsNeeded or (abort "allowed config paths not set for module \"${name}\"");
config = constrainConfigArgs =
# TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05 args'@{ pkgs, ... }:
(builtins.foldl' lib.attrsets.recursiveUpdate { } args'
(map // {
(p: lib.attrsets.setAttrByPath p config =
(lib.attrsets.getAttrFromPath p config)) # TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05
configPathsNeeded (
) builtins.foldl' lib.attrsets.recursiveUpdate { } (
); map (p: lib.attrsets.setAttrByPath p (lib.attrsets.getAttrFromPath p config)) configPathsNeeded
}; )
constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive );
(p: v: };
constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive (
p: v:
# TODO traverse only imports and imports of imports, etc # TODO traverse only imports and imports of imports, etc
# without traversing all attributes # without traversing all attributes
if lib.lists.last p == "imports" if lib.lists.last p == "imports" then
then map (
map m:
(m: (
(args'@{ pkgs, ... }: constrainImportsArgsRecursive args'@{ pkgs, ... }:
(if builtins.isPath m constrainImportsArgsRecursive (
then import m (constrainConfigArgs args') if builtins.isPath m then
import m (constrainConfigArgs args')
else if builtins.isFunction m then
m (constrainConfigArgs args')
else else
if builtins.isFunction m m
then m (constrainConfigArgs args') )
else m))
) )
v ) v
else v); else
in v
constrainImportsArgsRecursive );
(sp-module.nixosModules.default (constrainConfigArgs args)) in
) constrainImportsArgsRecursive (sp-module.nixosModules.default (constrainConfigArgs args))
sp-modules; ) sp-modules;
};
}; };
}; formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt;
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.selfprivacy; cfg = config.selfprivacy;
dnsCredentialsTemplates = { dnsCredentialsTemplates = {
@@ -27,7 +32,11 @@ in
acceptTerms = true; acceptTerms = true;
defaults = { defaults = {
email = "${cfg.username}@${cfg.domain}"; email = "${cfg.username}@${cfg.domain}";
server = if cfg.dns.useStagingACME then "https://acme-staging-v02.api.letsencrypt.org/directory" else "https://acme-v02.api.letsencrypt.org/directory"; server =
if cfg.dns.useStagingACME then
"https://acme-staging-v02.api.letsencrypt.org/directory"
else
"https://acme-v02.api.letsencrypt.org/directory";
reloadServices = [ "nginx" ]; reloadServices = [ "nginx" ];
dnsResolver = "8.8.8.8:53"; dnsResolver = "8.8.8.8:53";
}; };
@@ -38,7 +47,9 @@ in
dnsProvider = lib.strings.toLower cfg.dns.provider; dnsProvider = lib.strings.toLower cfg.dns.provider;
credentialsFile = acme-env-filepath; credentialsFile = acme-env-filepath;
dnsPropagationCheck = dnsPropagationCheck =
! ((lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck); !(
(lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck
);
}; };
"root-${cfg.domain}" = { "root-${cfg.domain}" = {
domain = cfg.domain; domain = cfg.domain;
@@ -51,7 +62,10 @@ in
before = [ "acme-${cfg.domain}.service" ]; before = [ "acme-${cfg.domain}.service" ];
requiredBy = [ "acme-${cfg.domain}.service" ]; requiredBy = [ "acme-${cfg.domain}.service" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
path = with pkgs; [ coreutils jq ]; path = with pkgs; [
coreutils
jq
];
script = '' script = ''
set -o nounset set -o nounset

View File

@@ -1,18 +1,28 @@
{ sp-module, pkgs }: { sp-module, pkgs }:
let let
lib = pkgs.lib; lib = pkgs.lib;
options = (pkgs.lib.evalModules { modules = [{ _module.check = false; } sp-module.nixosModules.default]; }).options; options =
(pkgs.lib.evalModules {
modules = [
{ _module.check = false; }
sp-module.nixosModules.default
];
}).options;
# Transform a Nix option to a JSON structure with metadata # Transform a Nix option to a JSON structure with metadata
optionToMeta = (name: option: { optionToMeta = (
name = name; name: option: {
description = if builtins.hasAttr "description" option then option.description else null; name = name;
loc = option.loc; description = if builtins.hasAttr "description" option then option.description else null;
meta = if builtins.hasAttr "meta" option then option.meta else null; loc = option.loc;
default = if builtins.hasAttr "default" option then option.default else null; meta = if builtins.hasAttr "meta" option then option.meta else null;
}); default = if builtins.hasAttr "default" option then option.default else null;
}
);
in in
builtins.toJSON ({ builtins.toJSON ({
meta = if builtins.hasAttr "meta" sp-module then sp-module.meta { inherit lib; } else null; meta = if builtins.hasAttr "meta" sp-module then sp-module.meta { inherit lib; } else null;
configPathsNeeded = sp-module.configPathsNeeded; configPathsNeeded = sp-module.configPathsNeeded;
options = pkgs.lib.mapAttrs optionToMeta (builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules)); options = pkgs.lib.mapAttrs optionToMeta (
builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules)
);
}) })

View File

@@ -1,6 +1,4 @@
system: system: _final: _prev: {
_final: _prev:
{
# Here is a template to bring a specific package from a given nixpkgs commit: # Here is a template to bring a specific package from a given nixpkgs commit:
# PACKAGE_NAME = (builtins.getFlake "github:nixos/nixpkgs/NIXPKGS_COMMIT_SHA1").legacyPackages.${system}.PACKAGE_NAME; # PACKAGE_NAME = (builtins.getFlake "github:nixos/nixpkgs/NIXPKGS_COMMIT_SHA1").legacyPackages.${system}.PACKAGE_NAME;
# Substitute `PACKAGE_NAME` and `NIXPKGS_COMMIT_SHA1` accordingly. # Substitute `PACKAGE_NAME` and `NIXPKGS_COMMIT_SHA1` accordingly.

View File

@@ -1,22 +1,34 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
sp = config.selfprivacy; sp = config.selfprivacy;
pleroma_location = pleroma_location =
if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma if
then sp.modules.pleroma.location lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma
else null; then
sp.modules.pleroma.location
else
null;
postgres_location = postgres_location =
if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql then
then sp.postgresql.location sp.postgresql.location
else null; else
null;
# Priority: postgresql > pleroma # Priority: postgresql > pleroma
location = if postgres_location != null then postgres_location else pleroma_location; location = if postgres_location != null then postgres_location else pleroma_location;
# Active if there is a location # Active if there is a location
enable = location != null; enable = location != null;
pleroma_enabled = pleroma_enabled =
if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma if
then sp.modules.pleroma.enable lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma
else false; then
sp.modules.pleroma.enable
else
false;
in in
{ {
imports = [ imports = [

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.services.postgresqlUpgrade12to16; cfg = config.services.postgresqlUpgrade12to16;
@@ -62,72 +67,70 @@ in
ExecStartPre = ExecStartPre =
# Stop Pleroma only if pleromaEnabled is true # Stop Pleroma only if pleromaEnabled is true
optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" '' optional cfg.pleromaEnabled
if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then "${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" ''
${pkgs.systemd}/bin/systemctl stop pleroma.service if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then
fi ${pkgs.systemd}/bin/systemctl stop pleroma.service
'' fi
}"; ''}";
ExecStart = "${pkgs.writeShellScript "postgresql-upgrade12to16.sh" '' ExecStart = "${pkgs.writeShellScript "postgresql-upgrade12to16.sh" ''
set -e set -e
oldDataDir="${cfg.dataDir12}" oldDataDir="${cfg.dataDir12}"
newDataDir="${cfg.dataDir16}" newDataDir="${cfg.dataDir16}"
# Only upgrade if old data directory exists, and the new one doesn't yet # Only upgrade if old data directory exists, and the new one doesn't yet
if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then
echo "Detected PostgreSQL 12 data directory at $oldDataDir" echo "Detected PostgreSQL 12 data directory at $oldDataDir"
echo "Upgrading to PostgreSQL 16 data directory at $newDataDir" echo "Upgrading to PostgreSQL 16 data directory at $newDataDir"
# Stop the old PostgreSQL if it's running # Stop the old PostgreSQL if it's running
if systemctl is-active --quiet postgresql.service; then if systemctl is-active --quiet postgresql.service; then
systemctl stop postgresql.service systemctl stop postgresql.service
fi
# Create the new data directory (if not already present)
mkdir -p "$newDataDir"
chown -R postgres:postgres "$(dirname "$newDataDir")"
# Create a temporary working directory
tempDir=$(mktemp -d)
chown -R postgres:postgres "$tempDir"
trap 'rm -rf "$tempDir"' EXIT
# Change to the temporary working directory
cd "$tempDir"
# Initialize the new PostgreSQL 16 data directory
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres
# Run pg_upgrade as the postgres user
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \
--old-datadir "$oldDataDir" \
--new-datadir "$newDataDir" \
--old-bindir ${pkgs.postgresql_12.out}/bin \
--new-bindir ${pkgs.postgresql_16.out}/bin \
--jobs "$(nproc)" \
--link \
--verbose
touch "$newDataDir/.sp_migrated"
echo "PostgreSQL upgrade from 12 to 16 completed."
else
echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping."
fi fi
''}";
# Create the new data directory (if not already present)
mkdir -p "$newDataDir"
chown -R postgres:postgres "$(dirname "$newDataDir")"
# Create a temporary working directory
tempDir=$(mktemp -d)
chown -R postgres:postgres "$tempDir"
trap 'rm -rf "$tempDir"' EXIT
# Change to the temporary working directory
cd "$tempDir"
# Initialize the new PostgreSQL 16 data directory
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres
# Run pg_upgrade as the postgres user
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \
--old-datadir "$oldDataDir" \
--new-datadir "$newDataDir" \
--old-bindir ${pkgs.postgresql_12.out}/bin \
--new-bindir ${pkgs.postgresql_16.out}/bin \
--jobs "$(nproc)" \
--link \
--verbose
touch "$newDataDir/.sp_migrated"
echo "PostgreSQL upgrade from 12 to 16 completed."
else
echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping."
fi
''}";
# Start Pleroma only if pleromaEnabled is true # Start Pleroma only if pleromaEnabled is true
ExecStartPost = ExecStartPost = optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" ''
optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" '' if test -e "${cfg.dataDir16}/.sp_migrated"; then
if test -e "${cfg.dataDir16}/.sp_migrated"; then ${pkgs.systemd}/bin/systemctl start --no-block pleroma.service
${pkgs.systemd}/bin/systemctl start --no-block pleroma.service
rm -f "${cfg.dataDir16}/.sp_migrated" rm -f "${cfg.dataDir16}/.sp_migrated"
fi fi
'' ''}";
}";
}; };
}; };
}; };

View File

@@ -5,13 +5,8 @@ in
# FIXME do we really want to delete passwords on module deactivation!? # FIXME do we really want to delete passwords on module deactivation!?
{ {
config = lib.mkIf (!sp.modules.bitwarden.enable) { config = lib.mkIf (!sp.modules.bitwarden.enable) {
system.activationScripts.bitwarden = system.activationScripts.bitwarden = lib.trivial.warn ("bitwarden service is disabled, ${bitwarden-env} will be removed!") ''
lib.trivial.warn rm -f -v ${bitwarden-env}
( '';
"bitwarden service is disabled, ${bitwarden-env} will be removed!"
)
''
rm -f -v ${bitwarden-env}
'';
}; };
} }

View File

@@ -1,5 +1,4 @@
config: config: {
{
sp = config.selfprivacy; sp = config.selfprivacy;
bitwarden-env = "/var/lib/bitwarden/.env"; bitwarden-env = "/var/lib/bitwarden/.env";
} }

View File

@@ -1,34 +1,41 @@
{ {
description = "PoC SP module for Bitwarden password management solution"; description = "PoC SP module for Bitwarden password management solution";
outputs = { self }: { outputs =
nixosModules.default = _: { self }:
{ imports = [ ./module.nix ./cleanup-module.nix ]; }; {
configPathsNeeded = nixosModules.default = _: {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); imports = [
meta = { lib, ... }: { ./module.nix
spModuleSchemaVersion = 1; ./cleanup-module.nix
id = "bitwarden"; ];
name = "Bitwarden"; };
description = "Bitwarden is a password manager."; configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
svgIcon = builtins.readFile ./icon.svg; meta =
isMovable = true; { lib, ... }:
isRequired = false; {
backupDescription = "Password database, encryption certificate and attachments."; spModuleSchemaVersion = 1;
systemdServices = [ id = "bitwarden";
"vaultwarden.service" name = "Bitwarden";
]; description = "Bitwarden is a password manager.";
user = "vaultwarden"; svgIcon = builtins.readFile ./icon.svg;
folders = [ isMovable = true;
"/var/lib/bitwarden" isRequired = false;
"/var/lib/bitwarden_rs" backupDescription = "Password database, encryption certificate and attachments.";
]; systemdServices = [
license = [ "vaultwarden.service"
lib.licenses.agpl3Only ];
]; user = "vaultwarden";
homepage = "https://github.com/dani-garcia/vaultwarden"; folders = [
sourcePage = "https://github.com/dani-garcia/vaultwarden"; "/var/lib/bitwarden"
supportLevel = "normal"; "/var/lib/bitwarden_rs"
];
license = [
lib.licenses.agpl3Only
];
homepage = "https://github.com/dani-garcia/vaultwarden";
sourcePage = "https://github.com/dani-garcia/vaultwarden";
supportLevel = "normal";
};
}; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
secrets-filepath = "/etc/selfprivacy/secrets.json"; secrets-filepath = "/etc/selfprivacy/secrets.json";
backup-dir = "/var/lib/bitwarden/backup"; backup-dir = "/var/lib/bitwarden/backup";
@@ -7,65 +12,77 @@ let
in in
{ {
options.selfprivacy.modules.bitwarden = { options.selfprivacy.modules.bitwarden = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable Vaultwarden"; type = lib.types.bool;
}) // { description = "Enable Vaultwarden";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Vaultwarden location"; description = "Vaultwarden location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "password"; default = "password";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
}; signupsAllowed =
signupsAllowed = (lib.mkOption { (lib.mkOption {
default = true; default = true;
type = lib.types.bool; type = lib.types.bool;
description = "Allow new user signups"; description = "Allow new user signups";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 1; type = "bool";
weight = 1;
};
}; };
}; sendsAllowed =
sendsAllowed = (lib.mkOption { (lib.mkOption {
default = true; default = true;
type = lib.types.bool; type = lib.types.bool;
description = "Allow users to use Bitwarden Send"; description = "Allow users to use Bitwarden Send";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 2; type = "bool";
weight = 2;
};
}; };
}; emergencyAccessAllowed =
emergencyAccessAllowed = (lib.mkOption { (lib.mkOption {
default = true; default = true;
type = lib.types.bool; type = lib.types.bool;
description = "Allow users to enable Emergency Access"; description = "Allow users to enable Emergency Access";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 3; type = "bool";
weight = 3;
};
}; };
};
}; };
config = lib.mkIf config.selfprivacy.modules.bitwarden.enable { config = lib.mkIf config.selfprivacy.modules.bitwarden.enable {
@@ -118,7 +135,10 @@ in
before = [ "vaultwarden.service" ]; before = [ "vaultwarden.service" ];
requiredBy = [ "vaultwarden.service" ]; requiredBy = [ "vaultwarden.service" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
path = with pkgs; [ coreutils jq ]; path = with pkgs; [
coreutils
jq
];
script = '' script = ''
set -o nounset set -o nounset

View File

@@ -1,35 +1,38 @@
{ {
description = "PoC SP module for Gitea forge service"; description = "PoC SP module for Gitea forge service";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "gitea"; { lib, ... }:
name = "Forgejo"; {
description = "Forgejo is a Git forge."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "gitea";
isMovable = true; name = "Forgejo";
isRequired = false; description = "Forgejo is a Git forge.";
backupDescription = "Git repositories, database and user data."; svgIcon = builtins.readFile ./icon.svg;
systemdServices = [ isMovable = true;
"forgejo.service" isRequired = false;
]; backupDescription = "Git repositories, database and user data.";
folders = [ systemdServices = [
"/var/lib/gitea" "forgejo.service"
]; ];
license = [ folders = [
lib.licenses.gpl3Plus "/var/lib/gitea"
]; ];
homepage = "https://forgejo.org"; license = [
sourcePage = "https://codeberg.org/forgejo/forgejo"; lib.licenses.gpl3Plus
supportLevel = "normal"; ];
sso = { homepage = "https://forgejo.org";
userGroup = "sp.gitea.users"; sourcePage = "https://codeberg.org/forgejo/forgejo";
adminGroup = "sp.gitea.admins"; supportLevel = "normal";
}; sso = {
userGroup = "sp.gitea.users";
adminGroup = "sp.gitea.admins";
};
};
}; };
};
} }

View File

@@ -1,10 +1,12 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
sp = config.selfprivacy; sp = config.selfprivacy;
stateDir = stateDir = if sp.useBinds then "/volumes/${cfg.location}/gitea" else "/var/lib/gitea";
if sp.useBinds
then "/volumes/${cfg.location}/gitea"
else "/var/lib/gitea";
cfg = sp.modules.gitea; cfg = sp.modules.gitea;
themes = [ themes = [
"forgejo-auto" "forgejo-auto"
@@ -18,8 +20,7 @@ let
oauthClientID = "forgejo"; oauthClientID = "forgejo";
auth-passthru = config.selfprivacy.passthru.auth; auth-passthru = config.selfprivacy.passthru.auth;
oauth2-provider-name = auth-passthru.oauth2-provider-name; oauth2-provider-name = auth-passthru.oauth2-provider-name;
redirect-uri = redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback";
"https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback";
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
# SelfPrivacy uses SP Module ID to identify the group! # SelfPrivacy uses SP Module ID to identify the group!
@@ -30,381 +31,401 @@ let
linuxGroupOfService = "gitea"; linuxGroupOfService = "gitea";
forgejoPackage = pkgs.forgejo; forgejoPackage = pkgs.forgejo;
serviceAccountTokenFP = serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroupOfService;
auth-passthru.mkServiceAccountTokenFP linuxGroupOfService; oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
oauthClientSecretFP =
auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
in in
{ {
options.selfprivacy.modules.gitea = { options.selfprivacy.modules.gitea = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable Forgejo"; type = lib.types.bool;
}) // { description = "Enable Forgejo";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Forgejo location"; description = "Forgejo location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "git"; default = "git";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
}; appName =
appName = (lib.mkOption { (lib.mkOption {
default = "SelfPrivacy git Service"; default = "SelfPrivacy git Service";
type = lib.types.str; type = lib.types.str;
description = "The name displayed in the web interface"; description = "The name displayed in the web interface";
}) // { })
meta = { // {
type = "string"; meta = {
weight = 1; type = "string";
weight = 1;
};
}; };
}; enableLfs =
enableLfs = (lib.mkOption { (lib.mkOption {
default = true; default = true;
type = lib.types.bool; type = lib.types.bool;
description = "Enable Git LFS"; description = "Enable Git LFS";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 2; type = "bool";
weight = 2;
};
}; };
}; forcePrivate =
forcePrivate = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Force all new repositories to be private"; description = "Force all new repositories to be private";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 3; type = "bool";
weight = 3;
};
}; };
}; disableRegistration =
disableRegistration = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Disable registration of new users"; description = "Disable registration of new users";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 4; type = "bool";
weight = 4;
};
}; };
}; requireSigninView =
requireSigninView = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Force users to log in to view any page"; description = "Force users to log in to view any page";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 5; type = "bool";
weight = 5;
};
}; };
}; defaultTheme =
defaultTheme = (lib.mkOption { (lib.mkOption {
default = "forgejo-auto"; default = "forgejo-auto";
type = lib.types.enum themes; type = lib.types.enum themes;
description = "Default theme"; description = "Default theme";
}) // { })
meta = { // {
type = "enum"; meta = {
options = themes; type = "enum";
weight = 6; options = themes;
weight = 6;
};
}; };
}; enableSso =
enableSso = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Enable Single Sign-On"; description = "Enable Single Sign-On";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 7; type = "bool";
weight = 7;
};
}; };
}; debug =
debug = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Enable debug logging"; description = "Enable debug logging";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 8; type = "bool";
weight = 8;
};
}; };
};
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ config = lib.mkIf cfg.enable (
{ lib.mkMerge [
assertions = [ {
{ assertions = [
assertion = cfg.enableSso -> sp.sso.enable; {
message = assertion = cfg.enableSso -> sp.sso.enable;
"SSO cannot be enabled for Forgejo when SSO is disabled globally."; message = "SSO cannot be enabled for Forgejo when SSO is disabled globally.";
} }
]; ];
fileSystems = lib.mkIf sp.useBinds { fileSystems = lib.mkIf sp.useBinds {
"/var/lib/gitea" = { "/var/lib/gitea" = {
device = "/volumes/${cfg.location}/gitea"; device = "/volumes/${cfg.location}/gitea";
options = [ "bind" ]; options = [ "bind" ];
};
}; };
}; services.gitea.enable = false;
services.gitea.enable = false; services.forgejo = {
services.forgejo = { enable = true;
enable = true; package = forgejoPackage;
package = forgejoPackage; inherit stateDir;
inherit stateDir;
user = linuxUserOfService;
group = linuxGroupOfService;
database = {
type = "sqlite3";
host = "127.0.0.1";
name = "gitea";
user = linuxUserOfService; user = linuxUserOfService;
path = "${stateDir}/data/gitea.db"; group = linuxGroupOfService;
createDatabase = true; database = {
type = "sqlite3";
host = "127.0.0.1";
name = "gitea";
user = linuxUserOfService;
path = "${stateDir}/data/gitea.db";
createDatabase = true;
};
# ssh = {
# enable = true;
# clonePort = 22;
# };
lfs = {
enable = cfg.enableLfs;
contentDir = "${stateDir}/lfs";
};
repositoryRoot = "${stateDir}/repositories";
# cookieSecure = true;
settings = {
DEFAULT = {
APP_NAME = "${cfg.appName}";
};
server = {
DOMAIN = "${cfg.subdomain}.${sp.domain}";
ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/";
HTTP_ADDR = "0.0.0.0";
HTTP_PORT = 3000;
};
mailer = {
ENABLED = false;
};
ui = {
DEFAULT_THEME = cfg.defaultTheme;
SHOW_USER_EMAIL = false;
};
picture = {
DISABLE_GRAVATAR = true;
};
admin = {
ENABLE_KANBAN_BOARD = true;
};
repository = {
FORCE_PRIVATE = cfg.forcePrivate;
};
session = {
COOKIE_SECURE = true;
};
log = {
ROOT_PATH = "${stateDir}/log";
LEVEL = if cfg.debug then "Warn" else "Trace";
};
service = {
DISABLE_REGISTRATION = cfg.disableRegistration;
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
};
};
}; };
# ssh = {
# enable = true; users.users.gitea = {
# clonePort = 22; home = "${stateDir}";
# }; useDefaultShell = true;
lfs = { group = linuxGroupOfService;
enable = cfg.enableLfs; isSystemUser = true;
contentDir = "${stateDir}/lfs";
}; };
repositoryRoot = "${stateDir}/repositories"; users.groups.${linuxGroupOfService} = { };
# cookieSecure = true; services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
settings = { useACMEHost = sp.domain;
DEFAULT = { forceSSL = true;
APP_NAME = "${cfg.appName}"; extraConfig = ''
add_header Strict-Transport-Security $hsts_header;
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
add_header 'Referrer-Policy' 'origin-when-cross-origin';
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
'';
locations = {
"/" = {
proxyPass = "http://127.0.0.1:3000";
};
}; };
server = { };
DOMAIN = "${cfg.subdomain}.${sp.domain}"; systemd = {
ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/"; services.forgejo = {
HTTP_ADDR = "0.0.0.0"; unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea";
HTTP_PORT = 3000; serviceConfig = {
Slice = "gitea.slice";
};
}; };
mailer = { slices.gitea = {
ENABLED = false; description = "Forgejo service slice";
};
ui = {
DEFAULT_THEME = cfg.defaultTheme;
SHOW_USER_EMAIL = false;
};
picture = {
DISABLE_GRAVATAR = true;
};
admin = {
ENABLE_KANBAN_BOARD = true;
};
repository = {
FORCE_PRIVATE = cfg.forcePrivate;
};
session = {
COOKIE_SECURE = true;
};
log = {
ROOT_PATH = "${stateDir}/log";
LEVEL = if cfg.debug then "Warn" else "Trace";
}; };
};
}
# the following part is active only when enableSso = true
(lib.mkIf is-auth-enabled {
services.forgejo.settings = {
auth.DISABLE_LOGIN_FORM = true;
service = { service = {
DISABLE_REGISTRATION = cfg.disableRegistration; DISABLE_REGISTRATION = cfg.disableRegistration;
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
ENABLE_BASIC_AUTHENTICATION = false;
};
# disallow explore page and access to private repositories, but allow public
"service.explore".REQUIRE_SIGNIN_VIEW = true;
# TODO control via selfprivacy parameter
# "service.explore".DISABLE_USERS_PAGE = true;
oauth2_client = {
REDIRECT_URI = redirect-uri;
ACCOUNT_LINKING = "auto";
ENABLE_AUTO_REGISTRATION = true;
OPENID_CONNECT_SCOPES = "email openid profile";
};
# doesn't work if LDAP auth source is not active!
"cron.sync_external_users" = {
ENABLED = true;
RUN_AT_START = true;
NOTICE_ON_SUCCESS = true;
}; };
}; };
}; systemd.services.forgejo = {
preStart =
let
waitForURL = url: maxRetries: delaySec: ''
for ((i=1; i<=${toString maxRetries}; i++))
do
if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null
then
echo "${url} responds to GET HTTP request (attempt #$i)"
break
else
echo "${url} does not respond to GET HTTP request (attempt #$i)"
echo sleeping for ${toString delaySec} seconds
fi
sleep ${toString delaySec}
done
if [[ "$i" -gt "${toString maxRetries}" ]]
then
echo "error, max attempts to access "${url}" have been used unsuccessfully!"
exit 124
fi
'';
users.users.gitea = { exe = lib.getExe config.services.forgejo.package;
home = "${stateDir}"; # FIXME skip-tls-verify, bind-password
useDefaultShell = true; ldapConfigArgs = ''
group = linuxGroupOfService; --name LDAP \
isSystemUser = true; --active \
}; --security-protocol LDAPS \
users.groups.${linuxGroupOfService} = { }; --skip-tls-verify \
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { --host '${auth-passthru.ldap-host}' \
useACMEHost = sp.domain; --port '${toString auth-passthru.ldap-port}' \
forceSSL = true; --user-search-base '${auth-passthru.ldap-base-dn}' \
extraConfig = '' --user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \
add_header Strict-Transport-Security $hsts_header; --admin-filter '(&(class=person)(memberof=${adminsGroup})' \
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; --username-attribute name \
add_header 'Referrer-Policy' 'origin-when-cross-origin'; --firstname-attribute name \
add_header X-Frame-Options DENY; --surname-attribute displayname \
add_header X-Content-Type-Options nosniff; --email-attribute mail \
add_header X-XSS-Protection "1; mode=block"; --public-ssh-key-attribute sshPublicKey \
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; --bind-dn 'dn=token' \
''; --bind-password "$(< ${serviceAccountTokenFP})" \
locations = { --synchronize-users
"/" = { '';
proxyPass = "http://127.0.0.1:3000"; oauthConfigArgs = ''
--name "${oauth2-provider-name}" \
--provider openidConnect \
--key forgejo \
--secret "$(< ${oauthClientSecretFP})" \
--group-claim-name groups \
--admin-group admins \
--auto-discover-url '${oauthDiscoveryURL}'
'';
in
lib.mkMerge [
(waitForURL oauthDiscoveryURL 10 10)
(lib.mkAfter ''
set -o xtrace
# Check if LDAP is already configured
ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)"
if [[ -n "$ldap_line" ]]; then
# update ldap config
id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs}
else
# initially configure ldap
${exe} admin auth add-ldap ${ldapConfigArgs}
fi
oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)"
if [[ -n "$oauth_line" ]]; then
id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs}
else
${exe} admin auth add-oauth ${oauthConfigArgs}
fi
'')
];
};
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
extraConfig = lib.mkAfter ''
rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last;
# FIXME is it needed?
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
selfprivacy.auth.clients."${oauthClientID}" = {
inherit adminsGroup usersGroup;
imageFile = ./icon.svg;
displayName = "Forgejo";
subdomain = cfg.subdomain;
isTokenNeeded = true;
originLanding = "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f";
originUrl = redirect-uri;
clientSystemdUnits = [ "forgejo.service" ];
enablePkce = lib.versionAtLeast forgejoPackage.version "8.0";
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
claimMaps.groups = {
joinType = "array";
valuesByGroup.${adminsGroup} = [ "admins" ];
}; };
}; };
}; })
systemd = { ]
services.forgejo = { );
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea";
serviceConfig = {
Slice = "gitea.slice";
};
};
slices.gitea = {
description = "Forgejo service slice";
};
};
}
# the following part is active only when enableSso = true
(lib.mkIf is-auth-enabled {
services.forgejo.settings = {
auth.DISABLE_LOGIN_FORM = true;
service = {
DISABLE_REGISTRATION = cfg.disableRegistration;
REQUIRE_SIGNIN_VIEW = cfg.requireSigninView;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
ENABLE_BASIC_AUTHENTICATION = false;
};
# disallow explore page and access to private repositories, but allow public
"service.explore".REQUIRE_SIGNIN_VIEW = true;
# TODO control via selfprivacy parameter
# "service.explore".DISABLE_USERS_PAGE = true;
oauth2_client = {
REDIRECT_URI = redirect-uri;
ACCOUNT_LINKING = "auto";
ENABLE_AUTO_REGISTRATION = true;
OPENID_CONNECT_SCOPES = "email openid profile";
};
# doesn't work if LDAP auth source is not active!
"cron.sync_external_users" = {
ENABLED = true;
RUN_AT_START = true;
NOTICE_ON_SUCCESS = true;
};
};
systemd.services.forgejo = {
preStart =
let
waitForURL = url: maxRetries: delaySec: ''
for ((i=1; i<=${toString maxRetries}; i++))
do
if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null
then
echo "${url} responds to GET HTTP request (attempt #$i)"
break
else
echo "${url} does not respond to GET HTTP request (attempt #$i)"
echo sleeping for ${toString delaySec} seconds
fi
sleep ${toString delaySec}
done
if [[ "$i" -gt "${toString maxRetries}" ]]
then
echo "error, max attempts to access "${url}" have been used unsuccessfully!"
exit 124
fi
'';
exe = lib.getExe config.services.forgejo.package;
# FIXME skip-tls-verify, bind-password
ldapConfigArgs = ''
--name LDAP \
--active \
--security-protocol LDAPS \
--skip-tls-verify \
--host '${auth-passthru.ldap-host}' \
--port '${toString auth-passthru.ldap-port}' \
--user-search-base '${auth-passthru.ldap-base-dn}' \
--user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \
--admin-filter '(&(class=person)(memberof=${adminsGroup})' \
--username-attribute name \
--firstname-attribute name \
--surname-attribute displayname \
--email-attribute mail \
--public-ssh-key-attribute sshPublicKey \
--bind-dn 'dn=token' \
--bind-password "$(< ${serviceAccountTokenFP})" \
--synchronize-users
'';
oauthConfigArgs = ''
--name "${oauth2-provider-name}" \
--provider openidConnect \
--key forgejo \
--secret "$(< ${oauthClientSecretFP})" \
--group-claim-name groups \
--admin-group admins \
--auto-discover-url '${oauthDiscoveryURL}'
'';
in
lib.mkMerge [
(waitForURL oauthDiscoveryURL 10 10)
(lib.mkAfter ''
set -o xtrace
# Check if LDAP is already configured
ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)"
if [[ -n "$ldap_line" ]]; then
# update ldap config
id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs}
else
# initially configure ldap
${exe} admin auth add-ldap ${ldapConfigArgs}
fi
oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)"
if [[ -n "$oauth_line" ]]; then
id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')"
${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs}
else
${exe} admin auth add-oauth ${oauthConfigArgs}
fi
'')
];
};
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
extraConfig = lib.mkAfter ''
rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last;
# FIXME is it needed?
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
selfprivacy.auth.clients."${oauthClientID}" = {
inherit adminsGroup usersGroup;
imageFile = ./icon.svg;
displayName = "Forgejo";
subdomain = cfg.subdomain;
isTokenNeeded = true;
originLanding =
"https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f";
originUrl = redirect-uri;
clientSystemdUnits = [ "forgejo.service" ];
enablePkce = lib.versionAtLeast forgejoPackage.version "8.0";
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
claimMaps.groups = {
joinType = "array";
valuesByGroup.${adminsGroup} = [ "admins" ];
};
};
})
]);
} }

View File

@@ -1,33 +1,36 @@
{ {
description = "PoC SP module for Jitsi Meet video conferences server"; description = "PoC SP module for Jitsi Meet video conferences server";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "jitsi-meet"; { lib, ... }:
name = "JitsiMeet"; {
description = "Jitsi Meet is a free and open-source video conferencing solution."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "jitsi-meet";
isMovable = false; name = "JitsiMeet";
isRequired = false; description = "Jitsi Meet is a free and open-source video conferencing solution.";
backupDescription = "Secrets that are used to encrypt the communication."; svgIcon = builtins.readFile ./icon.svg;
systemdServices = [ isMovable = false;
"prosody.service" isRequired = false;
"jitsi-videobridge2.service" backupDescription = "Secrets that are used to encrypt the communication.";
"jicofo.service" systemdServices = [
]; "prosody.service"
folders = [ "jitsi-videobridge2.service"
"/var/lib/jitsi-meet" "jicofo.service"
]; ];
license = [ folders = [
lib.licenses.asl20 "/var/lib/jitsi-meet"
]; ];
homepage = "https://jitsi.org/meet"; license = [
sourcePage = "https://github.com/jitsi/jitsi-meet"; lib.licenses.asl20
supportLevel = "normal"; ];
homepage = "https://jitsi.org/meet";
sourcePage = "https://github.com/jitsi/jitsi-meet";
supportLevel = "normal";
};
}; };
};
} }

View File

@@ -5,37 +5,43 @@ let
in in
{ {
options.selfprivacy.modules.jitsi-meet = { options.selfprivacy.modules.jitsi-meet = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable JitsiMeet"; type = lib.types.bool;
}) // { description = "Enable JitsiMeet";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "meet"; default = "meet";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
}; appName =
appName = (lib.mkOption { (lib.mkOption {
default = "Jitsi Meet"; default = "Jitsi Meet";
type = lib.types.str; type = lib.types.str;
description = "The name displayed in the web interface"; description = "The name displayed in the web interface";
}) // { })
meta = { // {
type = "string"; meta = {
weight = 1; type = "string";
weight = 1;
};
}; };
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@@ -43,7 +49,9 @@ in
(_: prev: { (_: prev: {
# We disable E2E for clients below # We disable E2E for clients below
jitsi-meet = prev.jitsi-meet.overrideAttrs (old: { jitsi-meet = prev.jitsi-meet.overrideAttrs (old: {
meta = old.meta // { knownVulnerabilities = [ ]; }; meta = old.meta // {
knownVulnerabilities = [ ];
};
}); });
}) })
]; ];

View File

@@ -1,36 +1,39 @@
{ {
description = "PoC SP module for Prometheus-based monitoring"; description = "PoC SP module for Prometheus-based monitoring";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "monitoring"; { lib, ... }:
name = "Prometheus";
description = "Prometheus is used for resource monitoring and alerts.";
svgIcon = builtins.readFile ./icon.svg;
isMovable = false;
isRequired = true;
canBeBackedUp = false;
backupDescription = "Backups are not available for Prometheus.";
systemdServices = [
"prometheus.service"
];
ownedFolders = [
{ {
path = "/var/lib/prometheus"; spModuleSchemaVersion = 1;
owner = "prometheus"; id = "monitoring";
group = "prometheus"; name = "Prometheus";
} description = "Prometheus is used for resource monitoring and alerts.";
]; svgIcon = builtins.readFile ./icon.svg;
license = [ isMovable = false;
lib.licenses.asl20 isRequired = true;
]; canBeBackedUp = false;
homepage = "https://prometheus.io/"; backupDescription = "Backups are not available for Prometheus.";
sourcePage = "https://prometheus.io/"; systemdServices = [
supportLevel = "normal"; "prometheus.service"
];
ownedFolders = [
{
path = "/var/lib/prometheus";
owner = "prometheus";
group = "prometheus";
}
];
license = [
lib.licenses.asl20
];
homepage = "https://prometheus.io/";
sourcePage = "https://prometheus.io/";
supportLevel = "normal";
};
}; };
};
} }

View File

@@ -4,23 +4,27 @@ let
in in
{ {
options.selfprivacy.modules.monitoring = { options.selfprivacy.modules.monitoring = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable monitoring service"; type = lib.types.bool;
}) // { description = "Enable monitoring service";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Monitoring data location"; description = "Monitoring data location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
fileSystems = lib.mkIf config.selfprivacy.useBinds { fileSystems = lib.mkIf config.selfprivacy.useBinds {
@@ -54,15 +58,19 @@ in
scrapeConfigs = [ scrapeConfigs = [
{ {
job_name = "node-exporter"; job_name = "node-exporter";
static_configs = [{ static_configs = [
targets = [ "127.0.0.1:9002" ]; {
}]; targets = [ "127.0.0.1:9002" ];
}
];
} }
{ {
job_name = "cadvisor"; job_name = "cadvisor";
static_configs = [{ static_configs = [
targets = [ "127.0.0.1:9003" ]; {
}]; targets = [ "127.0.0.1:9003" ];
}
];
} }
]; ];
}; };

View File

@@ -1,35 +1,38 @@
{ {
description = "PoC SP module for Mumble conferences server"; description = "PoC SP module for Mumble conferences server";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "mumble"; { lib, ... }:
name = "Mumble"; {
description = "Open Source, Low Latency, High Quality Voice Chat."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "mumble";
showUrl = false; name = "Mumble";
isMovable = true; description = "Open Source, Low Latency, High Quality Voice Chat.";
isRequired = false; svgIcon = builtins.readFile ./icon.svg;
canBeBackedUp = true; showUrl = false;
backupDescription = "Mumble server data."; isMovable = true;
systemdServices = [ isRequired = false;
"murmur.service" canBeBackedUp = true;
]; backupDescription = "Mumble server data.";
user = "murmur"; systemdServices = [
group = "murmur"; "murmur.service"
folders = [ ];
"/var/lib/murmur" user = "murmur";
]; group = "murmur";
license = [ folders = [
lib.licenses.bsd3 "/var/lib/murmur"
]; ];
homepage = "https://www.mumble.info"; license = [
sourcePage = "https://github.com/mumble-voip/mumble"; lib.licenses.bsd3
supportLevel = "normal"; ];
homepage = "https://www.mumble.info";
sourcePage = "https://github.com/mumble-voip/mumble";
supportLevel = "normal";
};
}; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
sp = config.selfprivacy; sp = config.selfprivacy;
@@ -6,55 +11,65 @@ let
in in
{ {
options.selfprivacy.modules.mumble = { options.selfprivacy.modules.mumble = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable Mumble"; type = lib.types.bool;
}) // { description = "Enable Mumble";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "mumble"; default = "mumble";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Location"; description = "Location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; appName =
appName = (lib.mkOption { (lib.mkOption {
default = "SelfPrivacy Mumble Service"; default = "SelfPrivacy Mumble Service";
type = lib.types.str; type = lib.types.str;
description = "The name of your Mumble server"; description = "The name of your Mumble server";
}) // { })
meta = { // {
type = "string"; meta = {
weight = 1; type = "string";
weight = 1;
};
}; };
}; welcomeText =
welcomeText = (lib.mkOption { (lib.mkOption {
default = "Welcome to my Mumble server!"; default = "Welcome to my Mumble server!";
type = lib.types.str; type = lib.types.str;
description = "Welcome message"; description = "Welcome message";
}) // { })
meta = { // {
type = "string"; meta = {
weight = 2; type = "string";
weight = 2;
};
}; };
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@@ -79,7 +94,9 @@ in
}; };
systemd = { systemd = {
services = { services = {
murmur = { serviceConfig.Slice = "mumble.slice"; }; murmur = {
serviceConfig.Slice = "mumble.slice";
};
murmur-ensure-folder-ownership = { murmur-ensure-folder-ownership = {
description = "Ensure murmur folder ownership"; description = "Ensure murmur folder ownership";
before = [ "murmur.service" ]; before = [ "murmur.service" ];

View File

@@ -13,8 +13,8 @@ in
system.activationScripts.nextcloudSecrets = system.activationScripts.nextcloudSecrets =
lib.trivial.warn lib.trivial.warn
( (
"nextcloud service is disabled, " + "nextcloud service is disabled, "
"${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!" + "${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!"
) )
'' ''
rm -f -v ${db-pass-filepath} rm -f -v ${db-pass-filepath}

View File

@@ -1,6 +1,6 @@
config: rec { config: rec {
sp = config.selfprivacy; sp = config.selfprivacy;
domain= sp.domain; domain = sp.domain;
secrets-filepath = "/etc/selfprivacy/secrets.json"; secrets-filepath = "/etc/selfprivacy/secrets.json";
db-pass-filepath = "/var/lib/nextcloud/db-pass"; db-pass-filepath = "/var/lib/nextcloud/db-pass";
admin-pass-filepath = "/var/lib/nextcloud/admin-pass"; admin-pass-filepath = "/var/lib/nextcloud/admin-pass";

View File

@@ -1,38 +1,45 @@
{ {
description = "PoC SP module for nextcloud"; description = "PoC SP module for nextcloud";
outputs = { self }: { outputs =
nixosModules.default = _: { self }:
{ imports = [ ./module.nix ./cleanup-module.nix ]; }; {
configPathsNeeded = nixosModules.default = _: {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); imports = [
meta = { lib, ... }: { ./module.nix
spModuleSchemaVersion = 1; ./cleanup-module.nix
id = "nextcloud"; ];
name = "Nextcloud";
description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client.";
svgIcon = builtins.readFile ./icon.svg;
isMovable = true;
isRequired = false;
canBeBackedUp = true;
backupDescription = "All the files and other data stored in Nextcloud.";
systemdServices = [
"phpfpm-nextcloud.service"
"redis-nextcloud.service"
];
folders = [
"/var/lib/nextcloud"
];
license = [
lib.licenses.agpl3Plus
];
homepage = "https://nextcloud.com/";
sourcePage = "https://github.com/nextcloud";
supportLevel = "normal";
sso = {
userGroup = "sp.nextcloud.users";
adminGroup = "sp.nextcloud.admins";
}; };
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
meta =
{ lib, ... }:
{
spModuleSchemaVersion = 1;
id = "nextcloud";
name = "Nextcloud";
description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client.";
svgIcon = builtins.readFile ./icon.svg;
isMovable = true;
isRequired = false;
canBeBackedUp = true;
backupDescription = "All the files and other data stored in Nextcloud.";
systemdServices = [
"phpfpm-nextcloud.service"
"redis-nextcloud.service"
];
folders = [
"/var/lib/nextcloud"
];
license = [
lib.licenses.agpl3Plus
];
homepage = "https://nextcloud.com/";
sourcePage = "https://github.com/nextcloud";
supportLevel = "normal";
sso = {
userGroup = "sp.nextcloud.users";
adminGroup = "sp.nextcloud.admins";
};
};
}; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
inherit (import ./common.nix config) inherit (import ./common.nix config)
admin-pass-filepath admin-pass-filepath
@@ -27,391 +32,417 @@ let
usersGroup = "sp.${oauthClientID}.users"; usersGroup = "sp.${oauthClientID}.users";
wildcardGroup = "sp.${oauthClientID}.*"; wildcardGroup = "sp.${oauthClientID}.*";
serviceAccountTokenFP = serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxUserOfService;
auth-passthru.mkServiceAccountTokenFP linuxUserOfService; oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService;
oauthClientSecretFP =
auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService;
updater-page-substitute = updater-page-substitute = pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } ''
pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } '' install -m644 ${./updater.html} -DT $out/index.html
install -m644 ${./updater.html} -DT $out/index.html '';
'';
in in
{ {
options.selfprivacy.modules.nextcloud = with lib; { options.selfprivacy.modules.nextcloud = with lib; {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable Nextcloud"; type = lib.types.bool;
}) // { description = "Enable Nextcloud";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Nextcloud location"; description = "Nextcloud location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "cloud"; default = "cloud";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
}; enableImagemagick =
enableImagemagick = (lib.mkOption { (lib.mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = "Enable ImageMagick"; description = "Enable ImageMagick";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 1; type = "bool";
weight = 1;
};
}; };
}; enableSso =
enableSso = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Enable Single Sign-On"; description = "Enable Single Sign-On";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 2; type = "bool";
weight = 2;
};
}; };
}; enableSambaFeatures =
enableSambaFeatures = (lib.mkOption { (lib.mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Enable support for Samba/CIFS features"; description = "Enable support for Samba/CIFS features";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 3; type = "bool";
weight = 3;
};
}; };
}; debug =
debug = (lib.mkOption { (lib.mkOption {
default = false; default = false;
type = lib.types.bool; type = lib.types.bool;
description = "Enable debug logging"; description = "Enable debug logging";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 4; type = "bool";
weight = 4;
};
}; };
}; disableMaintenanceModeAtStart =
disableMaintenanceModeAtStart = (lib.mkOption { (lib.mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Disable maintenance mode at Nextcloud service startup"; description = "Disable maintenance mode at Nextcloud service startup";
}) // { })
meta = { // {
type = "bool"; meta = {
weight = 5; type = "bool";
weight = 5;
};
}; };
};
}; };
# config = lib.mkIf sp.modules.nextcloud.enable # config = lib.mkIf sp.modules.nextcloud.enable
config = lib.mkIf sp.modules.nextcloud.enable (lib.mkMerge [ config = lib.mkIf sp.modules.nextcloud.enable (
{ lib.mkMerge [
assertions = [ {
{ assertions = [
assertion = cfg.enableSso -> sp.sso.enable; {
message = assertion = cfg.enableSso -> sp.sso.enable;
"SSO cannot be enabled for Nextcloud when SSO is disabled globally."; message = "SSO cannot be enabled for Nextcloud when SSO is disabled globally.";
} }
]; ];
fileSystems = lib.mkIf sp.useBinds { fileSystems = lib.mkIf sp.useBinds {
"/var/lib/nextcloud" = { "/var/lib/nextcloud" = {
device = "/volumes/${cfg.location}/nextcloud"; device = "/volumes/${cfg.location}/nextcloud";
options = [ options = [
"bind" "bind"
"x-systemd.required-by=nextcloud-setup.service" "x-systemd.required-by=nextcloud-setup.service"
"x-systemd.required-by=nextcloud-secrets.service" "x-systemd.required-by=nextcloud-secrets.service"
"x-systemd.before=nextcloud-setup.service" "x-systemd.before=nextcloud-setup.service"
"x-systemd.before=nextcloud-secrets.service" "x-systemd.before=nextcloud-secrets.service"
]; ];
};
};
# for ExecStartPost script to have access to /run/keys/*
users.groups.keys.members =
lib.mkIf is-auth-enabled [ linuxUserOfService ];
# not needed, due to turnOffCertCheck=1 in used_ldap
# users.groups.${config.security.acme.certs.${domain}.group}.members =
# [ config.services.phpfpm.pools.nextcloud.user ];
systemd = {
services = {
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
nextcloud-setup = {
serviceConfig.Slice = "nextcloud.slice";
serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group;
}; };
nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; };
nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice";
nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice";
nextcloud-secrets = {
before = [ "nextcloud-setup.service" ];
requiredBy = [ "nextcloud-setup.service" ];
serviceConfig.Type = "oneshot";
path = with pkgs; [ coreutils jq ];
script = ''
databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath})
adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath})
install -C -m 0440 -o nextcloud -g nextcloud -DT \ # for ExecStartPost script to have access to /run/keys/*
<(printf "%s\n" "$databasePassword") \ users.groups.keys.members = lib.mkIf is-auth-enabled [ linuxUserOfService ];
${db-pass-filepath}
install -C -m 0440 -o nextcloud -g nextcloud -DT \ # not needed, due to turnOffCertCheck=1 in used_ldap
<(printf "%s\n" "$adminPassword") \ # users.groups.${config.security.acme.certs.${domain}.group}.members =
${admin-pass-filepath} # [ config.services.phpfpm.pools.nextcloud.user ];
systemd = {
services = {
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
nextcloud-setup = {
serviceConfig.Slice = "nextcloud.slice";
serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group;
};
nextcloud-cron.serviceConfig.Slice = "nextcloud.slice";
nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice";
nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice";
nextcloud-secrets = {
before = [ "nextcloud-setup.service" ];
requiredBy = [ "nextcloud-setup.service" ];
serviceConfig.Type = "oneshot";
path = with pkgs; [
coreutils
jq
];
script = ''
databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath})
adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath})
install -C -m 0440 -o nextcloud -g nextcloud -DT \
<(printf "%s\n" "$databasePassword") \
${db-pass-filepath}
install -C -m 0440 -o nextcloud -g nextcloud -DT \
<(printf "%s\n" "$adminPassword") \
${admin-pass-filepath}
'';
};
};
slices.nextcloud = {
description = "Nextcloud service slice";
};
};
services.nextcloud = {
enable = true;
package = pkgs.nextcloud30;
inherit hostName;
# Use HTTPS for links
https = true;
# auto-update Nextcloud Apps
autoUpdateApps.enable = true;
# set what time makes sense for you
autoUpdateApps.startAt = "05:00:00";
phpOptions.display_errors = "Off";
phpOptions."opcache.interned_strings_buffer" = "32";
configureRedis = true;
settings =
{
# further forces Nextcloud to use HTTPS
overwriteprotocol = "https";
}
// lib.attrsets.optionalAttrs is-auth-enabled {
loglevel = 0;
# log_type = "file";
social_login_auto_redirect = false;
allow_local_remote_servers = true;
allow_user_to_change_display_name = false;
lost_password_link = "disabled";
allow_multiple_user_backends = false;
updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos.
user_oidc = {
single_logout = true;
use_pkce = true;
auto_provision = true;
soft_auto_provision = true;
disable_account_creation = false;
};
};
config = {
dbtype = "sqlite";
dbuser = "nextcloud";
dbname = "nextcloud";
dbpassFile = db-pass-filepath;
# TODO review whether admin user is needed at all - admin group works
adminpassFile = admin-pass-filepath;
adminuser = "admin";
};
};
services.nginx.virtualHosts.${hostName} = {
useACMEHost = sp.domain;
forceSSL = true;
#locations."/".extraConfig = lib.mkIf is-auth-enabled ''
# # FIXME does not work
# rewrite ^/login$ /apps/user_oidc/login/1 last;
#'';
# show an error instead of a blank page on Nextcloud PHP/FastCGI error
locations."~ \\.php(?:$|/)".extraConfig = ''
error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html;
'';
locations."^~ /updater/" = {
alias = updater-page-substitute + "/";
extraConfig = ''
error_page 410 /index.html;
# otherwise, nginx returns 405 for POST requests to static content
error_page 405 =200 $uri;
''; '';
}; };
}; };
slices.nextcloud = { }
description = "Nextcloud service slice"; # enables samba features when requested
(lib.mkIf cfg.enableSambaFeatures {
# only apply cifs-utils package to this module
services.phpfpm.pools.nextcloud.phpEnv.PATH =
lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
systemd.services.nextcloud-cron.path = [
pkgs.samba
pkgs.cifs-utils
];
})
# the following part is active only when "auth" module is enabled
(lib.mkIf is-auth-enabled {
systemd.services.nextcloud-setup = {
serviceConfig = {
Restart = "on-failure";
RestartSec = "60";
};
path = [ pkgs.jq ];
script = lib.mkMerge [
(lib.strings.optionalString cfg.disableMaintenanceModeAtStart (
lib.mkBefore "${occ} maintenance:mode --no-interaction --off"
))
''
set -o errexit
set -o nounset
${lib.strings.optionalString cfg.debug "set -o xtrace"}
${occ} app:disable logreader
${occ} app:install user_ldap || :
${occ} app:enable user_ldap
# The following code tries to match an existing config or creates a new one.
# The criteria for matching is the ldapHost value.
# remove broken link after previous nextcloud (un)installation
[[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \
rm -v "${override-config-fp}"
ALL_CONFIG="$(${occ} ldap:show-config --output=json)"
MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")"
if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then
CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")"
else
CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)"
fi
echo "Using configId $CONFIG_ID"
# The following CLI commands follow
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way
# StartTLS is not supported in Kanidm due to security risks, whereas
# user_ldap doesn't support SASL. Importing certificate doesn't
# help:
# ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem"
${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1'
${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token'
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})"
${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail'
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \
'(&(class=group)(${wildcardGroup})'
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \
'(&(class=group)(${wildcardGroup}))'
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \
# 'groupOfUniqueNames'
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \
# 'uniqueMember'
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \
'(&(class=person)(memberof=${usersGroup})(uid=%uid))'
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \
'uid'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \
'displayname'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \
'(&(class=person)(memberof=${usersGroup})(name=%s))'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \
'1'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \
'person'
${occ} ldap:test-config -- "$CONFIG_ID"
# delete all configs except "$CONFIG_ID"
for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do
echo "Deactivating $configid"
${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0'
echo "Deactivated $configid"
echo "Deleting $configid"
${occ} ldap:delete-config "$configid"
echo "Deleted $configid"
done
${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1'
############################################################################
# OIDC app
############################################################################
${occ} app:install user_oidc || :
${occ} app:enable user_oidc
${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \
--clientid="${oauthClientID}" \
--clientsecret="$(<${oauthClientSecretFP})" \
--discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \
--unique-uid=0 \
--scope="email openid profile" \
--mapping-uid=preferred_username \
--no-interaction \
--mapping-groups=groups \
--group-provisioning=1 \
-vvv
''
(lib.optionalString deleteNextcloudAdmin ''
if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then
${occ} user:delete admin
touch /var/lib/nextcloud/.admin-user-deleted
fi
'')
];
}; };
}; selfprivacy.auth.clients."${oauthClientID}" = {
services.nextcloud = { inherit adminsGroup usersGroup;
enable = true; imageFile = ./icon.svg;
package = pkgs.nextcloud30; displayName = "Nextcloud";
inherit hostName; subdomain = cfg.subdomain;
isTokenNeeded = true;
# Use HTTPS for links originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code";
https = true; originLanding = "https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1";
useShortPreferredUsername = true;
# auto-update Nextcloud Apps clientSystemdUnits = [
autoUpdateApps.enable = true; "nextcloud-setup.service"
# set what time makes sense for you "phpfpm-nextcloud.service"
autoUpdateApps.startAt = "05:00:00"; ];
enablePkce = true;
phpOptions.display_errors = "Off"; linuxUserOfClient = linuxUserOfService;
phpOptions."opcache.interned_strings_buffer" = "32"; linuxGroupOfClient = linuxGroupOfService;
scopeMaps.${usersGroup} = [
configureRedis = true; "email"
"openid"
settings = { "profile"
# further forces Nextcloud to use HTTPS ];
overwriteprotocol = "https"; claimMaps.groups = {
} // lib.attrsets.optionalAttrs is-auth-enabled { joinType = "array";
loglevel = 0; valuesByGroup.${adminsGroup} = [ "admin" ];
# log_type = "file";
social_login_auto_redirect = false;
allow_local_remote_servers = true;
allow_user_to_change_display_name = false;
lost_password_link = "disabled";
allow_multiple_user_backends = false;
updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos.
user_oidc = {
single_logout = true;
use_pkce = true;
auto_provision = true;
soft_auto_provision = true;
disable_account_creation = false;
}; };
}; };
})
config = { (lib.mkIf (!is-auth-enabled) {
dbtype = "sqlite"; systemd.services.nextcloud-setup = {
dbuser = "nextcloud"; script = ''
dbname = "nextcloud"; ${occ} app:disable logreader
dbpassFile = db-pass-filepath; ${occ} app:disable user_oidc
# TODO review whether admin user is needed at all - admin group works
adminpassFile = admin-pass-filepath;
adminuser = "admin";
};
};
services.nginx.virtualHosts.${hostName} = {
useACMEHost = sp.domain;
forceSSL = true;
#locations."/".extraConfig = lib.mkIf is-auth-enabled ''
# # FIXME does not work
# rewrite ^/login$ /apps/user_oidc/login/1 last;
#'';
# show an error instead of a blank page on Nextcloud PHP/FastCGI error
locations."~ \\.php(?:$|/)".extraConfig = ''
error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html;
'';
locations."^~ /updater/" = {
alias = updater-page-substitute + "/";
extraConfig = ''
error_page 410 /index.html;
# otherwise, nginx returns 405 for POST requests to static content
error_page 405 =200 $uri;
''; '';
}; };
}; })
} ]
# enables samba features when requested );
(lib.mkIf cfg.enableSambaFeatures {
# only apply cifs-utils package to this module
services.phpfpm.pools.nextcloud.phpEnv.PATH =
lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
systemd.services.nextcloud-cron.path = [ pkgs.samba pkgs.cifs-utils ];
})
# the following part is active only when "auth" module is enabled
(lib.mkIf is-auth-enabled {
systemd.services.nextcloud-setup = {
serviceConfig = {
Restart = "on-failure";
RestartSec = "60";
};
path = [ pkgs.jq ];
script = lib.mkMerge [
(lib.strings.optionalString cfg.disableMaintenanceModeAtStart (
lib.mkBefore "${occ} maintenance:mode --no-interaction --off"
))
''
set -o errexit
set -o nounset
${lib.strings.optionalString cfg.debug "set -o xtrace"}
${occ} app:disable logreader
${occ} app:install user_ldap || :
${occ} app:enable user_ldap
# The following code tries to match an existing config or creates a new one.
# The criteria for matching is the ldapHost value.
# remove broken link after previous nextcloud (un)installation
[[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \
rm -v "${override-config-fp}"
ALL_CONFIG="$(${occ} ldap:show-config --output=json)"
MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")"
if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then
CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")"
else
CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)"
fi
echo "Using configId $CONFIG_ID"
# The following CLI commands follow
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way
# StartTLS is not supported in Kanidm due to security risks, whereas
# user_ldap doesn't support SASL. Importing certificate doesn't
# help:
# ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem"
${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1'
${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token'
${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})"
${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}'
${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail'
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \
'(&(class=group)(${wildcardGroup})'
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \
'(&(class=group)(${wildcardGroup}))'
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \
# 'groupOfUniqueNames'
# ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \
# 'uniqueMember'
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \
'(&(class=person)(memberof=${usersGroup})(uid=%uid))'
${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \
'uid'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \
'displayname'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \
'(&(class=person)(memberof=${usersGroup})(name=%s))'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \
'1'
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \
'person'
${occ} ldap:test-config -- "$CONFIG_ID"
# delete all configs except "$CONFIG_ID"
for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do
echo "Deactivating $configid"
${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0'
echo "Deactivated $configid"
echo "Deleting $configid"
${occ} ldap:delete-config "$configid"
echo "Deleted $configid"
done
${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1'
############################################################################
# OIDC app
############################################################################
${occ} app:install user_oidc || :
${occ} app:enable user_oidc
${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \
--clientid="${oauthClientID}" \
--clientsecret="$(<${oauthClientSecretFP})" \
--discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \
--unique-uid=0 \
--scope="email openid profile" \
--mapping-uid=preferred_username \
--no-interaction \
--mapping-groups=groups \
--group-provisioning=1 \
-vvv
''
(lib.optionalString deleteNextcloudAdmin ''
if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then
${occ} user:delete admin
touch /var/lib/nextcloud/.admin-user-deleted
fi
'')
];
};
selfprivacy.auth.clients."${oauthClientID}" = {
inherit adminsGroup usersGroup;
imageFile = ./icon.svg;
displayName = "Nextcloud";
subdomain = cfg.subdomain;
isTokenNeeded = true;
originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code";
originLanding =
"https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1";
useShortPreferredUsername = true;
clientSystemdUnits =
[ "nextcloud-setup.service" "phpfpm-nextcloud.service" ];
enablePkce = true;
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
scopeMaps.${usersGroup} = [ "email" "openid" "profile" ];
claimMaps.groups = {
joinType = "array";
valuesByGroup.${adminsGroup} = [ "admin" ];
};
};
})
(lib.mkIf (! is-auth-enabled) {
systemd.services.nextcloud-setup = {
script = ''
${occ} app:disable logreader
${occ} app:disable user_oidc
'';
};
})
]);
} }

View File

@@ -1,29 +1,32 @@
{ {
description = "PoC SP module for OpenConnect VPN server (ocserv)"; description = "PoC SP module for OpenConnect VPN server (ocserv)";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "ocserv"; { lib, ... }:
name = "OpenConnect VPN"; {
description = "OpenConnect VPN to connect your devices and access the internet."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "ocserv";
isMovable = false; name = "OpenConnect VPN";
isRequired = false; description = "OpenConnect VPN to connect your devices and access the internet.";
canBeBackedUp = false; svgIcon = builtins.readFile ./icon.svg;
backupDescription = "Backups are not available for OpenConnect VPN."; isMovable = false;
systemdServices = [ isRequired = false;
"ocserv.service" canBeBackedUp = false;
]; backupDescription = "Backups are not available for OpenConnect VPN.";
license = [ systemdServices = [
lib.licenses.gpl2Plus "ocserv.service"
]; ];
homepage = "https://gitlab.com/openconnect/ocserv"; license = [
sourcePage = "https://gitlab.com/openconnect/ocserv"; lib.licenses.gpl2Plus
supportLevel = "deprecated"; ];
homepage = "https://gitlab.com/openconnect/ocserv";
sourcePage = "https://gitlab.com/openconnect/ocserv";
supportLevel = "deprecated";
};
}; };
};
} }

View File

@@ -7,15 +7,17 @@ let
in in
{ {
options.selfprivacy.modules.ocserv = { options.selfprivacy.modules.ocserv = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable"; type = lib.types.bool;
}) // { description = "Enable";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@@ -66,7 +68,10 @@ in
systemd = { systemd = {
services = { services = {
ocserv = { ocserv = {
unitConfig.ConditionPathExists = [ cert key ]; unitConfig.ConditionPathExists = [
cert
key
];
serviceConfig.Slice = "ocserv.slice"; serviceConfig.Slice = "ocserv.slice";
}; };
}; };

View File

@@ -5,13 +5,8 @@ in
# FIXME do we really want to delete passwords on module deactivation!? # FIXME do we really want to delete passwords on module deactivation!?
{ {
config = lib.mkIf (!sp.modules.pleroma.enable) { config = lib.mkIf (!sp.modules.pleroma.enable) {
system.activationScripts.pleroma = system.activationScripts.pleroma = lib.trivial.warn ("pleroma service is disabled, ${secrets-exs} will be removed!") ''
lib.trivial.warn rm -f -v ${secrets-exs}
( '';
"pleroma service is disabled, ${secrets-exs} will be removed!"
)
''
rm -f -v ${secrets-exs}
'';
}; };
} }

View File

@@ -1,5 +1,4 @@
config: config: {
{
sp = config.selfprivacy; sp = config.selfprivacy;
secrets-exs = "/var/lib/pleroma/secrets.exs"; secrets-exs = "/var/lib/pleroma/secrets.exs";
} }

View File

@@ -1,35 +1,38 @@
{ {
description = "PoC SP module for Pleroma lightweight fediverse server"; description = "PoC SP module for Pleroma lightweight fediverse server";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "pleroma"; { lib, ... }:
name = "Pleroma"; {
description = "Pleroma is a microblogging service that offers a web interface and a desktop client."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "pleroma";
isMovable = true; name = "Pleroma";
isRequired = false; description = "Pleroma is a microblogging service that offers a web interface and a desktop client.";
canBeBackedUp = true; svgIcon = builtins.readFile ./icon.svg;
backupDescription = "Your Pleroma accounts, posts and media."; isMovable = true;
systemdServices = [ isRequired = false;
"pleroma.service" canBeBackedUp = true;
]; backupDescription = "Your Pleroma accounts, posts and media.";
folders = [ systemdServices = [
"/var/lib/pleroma" "pleroma.service"
]; ];
postgreDatabases = [ folders = [
"pleroma" "/var/lib/pleroma"
]; ];
license = [ postgreDatabases = [
lib.licenses.agpl3Only "pleroma"
]; ];
homepage = "https://pleroma.social/"; license = [
sourcePage = "https://git.pleroma.social/pleroma/pleroma"; lib.licenses.agpl3Only
supportLevel = "deprecated"; ];
homepage = "https://pleroma.social/";
sourcePage = "https://git.pleroma.social/pleroma/pleroma";
supportLevel = "deprecated";
};
}; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
secrets-filepath = "/etc/selfprivacy/secrets.json"; secrets-filepath = "/etc/selfprivacy/secrets.json";
cfg = config.selfprivacy.modules.pleroma; cfg = config.selfprivacy.modules.pleroma;
@@ -6,35 +11,41 @@ let
in in
{ {
options.selfprivacy.modules.pleroma = { options.selfprivacy.modules.pleroma = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable"; type = lib.types.bool;
}) // { description = "Enable";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Location"; description = "Location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; subdomain =
subdomain = (lib.mkOption { (lib.mkOption {
default = "social"; default = "social";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain"; description = "Subdomain";
}) // { })
meta = { // {
widget = "subdomain"; meta = {
type = "string"; widget = "subdomain";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; type = "string";
weight = 0; regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
}; };
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
fileSystems = lib.mkIf sp.useBinds { fileSystems = lib.mkIf sp.useBinds {
@@ -55,10 +66,9 @@ in
user = "pleroma"; user = "pleroma";
group = "pleroma"; group = "pleroma";
configs = [ configs = [
(builtins.replaceStrings (builtins.replaceStrings [ "$DOMAIN" "$LUSER" ] [ sp.domain sp.username ] (
[ "$DOMAIN" "$LUSER" ] builtins.readFile ./config.exs.in
[ sp.domain sp.username ] ))
(builtins.readFile ./config.exs.in))
]; ];
}; };
postgresql = { postgresql = {
@@ -94,7 +104,10 @@ in
before = [ "pleroma.service" ]; before = [ "pleroma.service" ];
requiredBy = [ "pleroma.service" ]; requiredBy = [ "pleroma.service" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
path = with pkgs; [ coreutils jq ]; path = with pkgs; [
coreutils
jq
];
script = '' script = ''
set -o nounset set -o nounset

View File

@@ -1,36 +1,39 @@
{ {
description = "Roundcube is a web-based email client."; description = "Roundcube is a web-based email client.";
outputs = { self }: { outputs =
nixosModules.default = import ./module.nix; { self }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix;
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "roundcube"; { lib, ... }:
name = "Roundcube"; {
description = "Roundcube is an open source webmail software."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "roundcube";
isMovable = false; name = "Roundcube";
isRequired = false; description = "Roundcube is an open source webmail software.";
canBeBackedUp = true; svgIcon = builtins.readFile ./icon.svg;
backupDescription = "Users' settings."; isMovable = false;
postgreDatabases = [ isRequired = false;
"roundcube" canBeBackedUp = true;
]; backupDescription = "Users' settings.";
systemdServices = [ postgreDatabases = [
"phpfpm-roundcube.service" "roundcube"
]; ];
license = [ systemdServices = [
lib.licenses.gpl3 "phpfpm-roundcube.service"
]; ];
homepage = "https://roundcube.net/"; license = [
sourcePage = "https://github.com/roundcube/roundcubemail"; lib.licenses.gpl3
supportLevel = "normal"; ];
sso = { homepage = "https://roundcube.net/";
userGroup = "sp.roundcube.users"; sourcePage = "https://github.com/roundcube/roundcubemail";
adminGroup = "sp.roundcube.admins"; supportLevel = "normal";
}; sso = {
userGroup = "sp.roundcube.users";
adminGroup = "sp.roundcube.admins";
};
};
}; };
};
} }

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
cfg = config.selfprivacy.modules.roundcube; cfg = config.selfprivacy.modules.roundcube;
@@ -16,129 +21,136 @@ let
usersGroup = "sp.${sp-module-name}.users"; usersGroup = "sp.${sp-module-name}.users";
oauth-donor = config.selfprivacy.passthru.mailserver; oauth-donor = config.selfprivacy.passthru.mailserver;
oauthClientSecretFP = oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
# copy client secret from mailserver # copy client secret from mailserver
kanidmExecStartPreScriptRoot = pkgs.writeShellScript kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" ''
"${sp-module-name}-kanidm-ExecStartPre-root-script.sh" install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
'' '';
install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
'';
in in
{ {
options.selfprivacy.modules.roundcube = { options.selfprivacy.modules.roundcube = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable"; type = lib.types.bool;
}) // { description = "Enable";
meta = { })
type = "enable"; // {
}; meta = {
}; type = "enable";
subdomain = (lib.mkOption {
default = "roundcube";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain";
}) // {
meta = {
widget = "subdomain";
type = "string";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
};
enableSso = (lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable Single Sign-On";
}) // {
meta = {
type = "bool";
weight = 1;
};
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
assertions = [
{
assertion = cfg.enableSso -> config.selfprivacy.sso.enable;
message =
"SSO cannot be enabled for Roundcube when SSO is disabled globally.";
}
];
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = "${cfg.subdomain}.${config.selfprivacy.domain}";
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = {
forceSSL = true;
useACMEHost = domain;
enableACME = false;
};
systemd.slices.roundcube.description = "Roundcube service slice";
# Roundcube depends on Dovecot and its OAuth2 client secret.
systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ];
}
# the following part is active only when "auth" module is enabled
(lib.mkIf is-auth-enabled {
services.roundcube.extraConfig = lib.mkAfter ''
$config['oauth_provider'] = 'generic';
$config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}';
$config['oauth_client_id'] = '${oauth-donor.oauth-client-id}';
$config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}');
$config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
$config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token';
$config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo';
$config['oauth_scope'] = 'email profile openid';
$config['oauth_auth_parameters'] = [];
$config['oauth_identity_fields'] = ['email'];
$config['oauth_login_redirect'] = true;
$config['auto_create_user'] = true;
'';
systemd.services.roundcube = {
after = [ "dovecot2.service" ];
requires = [ "dovecot2.service" ];
};
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
("-+" + kanidmExecStartPreScriptRoot)
];
selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = {
inherit adminsGroup usersGroup;
imageFile = ./icon.svg;
displayName = "Roundcube";
subdomain = cfg.subdomain;
isTokenNeeded = false;
isMailserver = true;
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
originLanding = "https://${cfg.subdomain}.${domain}/";
useShortPreferredUsername = false;
clientSystemdUnits = [ "dovecot2.service" "phpfpm-roundcube.service" ];
enablePkce = false;
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
scopeMaps = {
"${usersGroup}" = [
"email"
"openid"
"profile"
];
}; };
}; };
}) subdomain =
]); (lib.mkOption {
default = "roundcube";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain";
})
// {
meta = {
widget = "subdomain";
type = "string";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
};
enableSso =
(lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable Single Sign-On";
})
// {
meta = {
type = "bool";
weight = 1;
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
{
assertion = cfg.enableSso -> config.selfprivacy.sso.enable;
message = "SSO cannot be enabled for Roundcube when SSO is disabled globally.";
}
];
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = "${cfg.subdomain}.${config.selfprivacy.domain}";
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = {
forceSSL = true;
useACMEHost = domain;
enableACME = false;
};
systemd.slices.roundcube.description = "Roundcube service slice";
# Roundcube depends on Dovecot and its OAuth2 client secret.
systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ];
}
# the following part is active only when "auth" module is enabled
(lib.mkIf is-auth-enabled {
services.roundcube.extraConfig = lib.mkAfter ''
$config['oauth_provider'] = 'generic';
$config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}';
$config['oauth_client_id'] = '${oauth-donor.oauth-client-id}';
$config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}');
$config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
$config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token';
$config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo';
$config['oauth_scope'] = 'email profile openid';
$config['oauth_auth_parameters'] = [];
$config['oauth_identity_fields'] = ['email'];
$config['oauth_login_redirect'] = true;
$config['auto_create_user'] = true;
'';
systemd.services.roundcube = {
after = [ "dovecot2.service" ];
requires = [ "dovecot2.service" ];
};
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
("-+" + kanidmExecStartPreScriptRoot)
];
selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = {
inherit adminsGroup usersGroup;
imageFile = ./icon.svg;
displayName = "Roundcube";
subdomain = cfg.subdomain;
isTokenNeeded = false;
isMailserver = true;
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
originLanding = "https://${cfg.subdomain}.${domain}/";
useShortPreferredUsername = false;
clientSystemdUnits = [
"dovecot2.service"
"phpfpm-roundcube.service"
];
enablePkce = false;
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
scopeMaps = {
"${usersGroup}" = [
"email"
"openid"
"profile"
];
};
};
})
]
);
} }

View File

@@ -1,8 +1,14 @@
{ mailserver-service-account-name {
, mailserver-service-account-token-name mailserver-service-account-name,
, mailserver-service-account-token-fp mailserver-service-account-token-name,
mailserver-service-account-token-fp,
}: }:
{ config, lib, pkgs, ... }@nixos-args: {
config,
lib,
pkgs,
...
}@nixos-args:
let let
inherit (import ./common.nix nixos-args) inherit (import ./common.nix nixos-args)
appendSetting appendSetting
@@ -17,67 +23,70 @@ let
keysPath = auth-passthru.keys-path; keysPath = auth-passthru.keys-path;
# create service account token, needed for LDAP # create service account token, needed for LDAP
kanidmExecStartPostScript = pkgs.writeShellScript kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" ''
"mailserver-kanidm-ExecStartPost-script.sh" export HOME=$RUNTIME_DIRECTORY/client_home
'' readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
export HOME=$RUNTIME_DIRECTORY/client_home
readonly KANIDM="${pkgs.kanidm}/bin/kanidm"
# get Kanidm service account for mailserver # get Kanidm service account for mailserver
KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")"
echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
if [ -n "$KANIDM_SERVICE_ACCOUNT" ] if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
then then
echo "kanidm service account \"${mailserver-service-account-name}\" is found" echo "kanidm service account \"${mailserver-service-account-name}\" is found"
else else
echo "kanidm service account \"${mailserver-service-account-name}\" is not found" echo "kanidm service account \"${mailserver-service-account-name}\" is not found"
echo "creating new kanidm service account \"${mailserver-service-account-name}\"" echo "creating new kanidm service account \"${mailserver-service-account-name}\""
if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin
then then
"kanidm service account \"${mailserver-service-account-name}\" created" "kanidm service account \"${mailserver-service-account-name}\" created"
else else
echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\""
exit 1 exit 1
fi fi
fi fi
# add Kanidm service account to `idm_mail_servers` group # add Kanidm service account to `idm_mail_servers` group
$KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name}
# create a new read-only token for mailserver # create a new read-only token for mailserver
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)"
then then
echo "error: kanidm CLI returns an error when trying to generate service-account api-token" echo "error: kanidm CLI returns an error when trying to generate service-account api-token"
exit 1 exit 1
fi fi
if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)"
then then
echo "error: cannot get service-account API token from JSON" echo "error: cannot get service-account API token from JSON"
exit 1 exit 1
fi fi
if ! install --mode=640 \ if ! install --mode=640 \
<(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \
${mailserver-service-account-token-fp} ${mailserver-service-account-token-fp}
then then
echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" echo "error: cannot write token to \"${mailserver-service-account-token-fp}\""
exit 1 exit 1
fi fi
''; '';
ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext"; ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext";
mkLdapSearchScope = scope: ( mkLdapSearchScope =
if scope == "sub" then "subtree" scope:
else if scope == "one" then "onelevel" (
else scope if scope == "sub" then
); "subtree"
else if scope == "one" then
"onelevel"
else
scope
);
dovecot-ldap-config = pkgs.writeTextFile { dovecot-ldap-config = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template"; name = "dovecot-ldap.conf.ext.template";
text = '' text = ''
ldap_version = 3 ldap_version = 3
uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris} uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris}
${lib.optionalString config.mailserver.ldap.startTls '' ${lib.optionalString config.mailserver.ldap.startTls ''
tls = yes tls = yes
''} ''}
tls_require_cert = hard tls_require_cert = hard
tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile}
@@ -87,7 +96,7 @@ let
base = ${config.mailserver.ldap.searchBase} base = ${config.mailserver.ldap.searchBase}
scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope} scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope}
${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) '' ${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) ''
user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} user_attrs = ${config.mailserver.ldap.dovecot.userAttrs}
''} ''}
user_filter = ${config.mailserver.ldap.dovecot.userFilter} user_filter = ${config.mailserver.ldap.dovecot.userFilter}
''; '';
@@ -101,10 +110,8 @@ let
destination = ldapConfFile; destination = ldapConfFile;
}; };
oauth-client-id = "mailserver"; oauth-client-id = "mailserver";
oauth-client-secret-fp = oauth-client-secret-fp = "${keysPath}/${group}/kanidm-oauth-client-secret";
"${keysPath}/${group}/kanidm-oauth-client-secret"; oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
oauth-secret-ExecStartPreScript = pkgs.writeShellScript
"${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
set -o xtrace set -o xtrace
[ -f "${oauth-client-secret-fp}" ] || \ [ -f "${oauth-client-secret-fp}" ] || \
"${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}" "${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}"
@@ -122,8 +129,8 @@ let
openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id} openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id}
debug = "no" debug = "no"
''; '';
prefix = ''introspection_url = "'' + prefix =
(auth-passthru.oauth2-introspection-url-prefix oauth-client-id); ''introspection_url = "'' + (auth-passthru.oauth2-introspection-url-prefix oauth-client-id);
suffix = auth-passthru.oauth2-introspection-url-postfix + ''"''; suffix = auth-passthru.oauth2-introspection-url-postfix + ''"'';
passwordFile = oauth-client-secret-fp; passwordFile = oauth-client-secret-fp;
destination = dovecot-oauth2-conf-fp; destination = dovecot-oauth2-conf-fp;

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }@nixos-args: {
config,
lib,
pkgs,
...
}@nixos-args:
let let
inherit (import ./common.nix nixos-args) inherit (import ./common.nix nixos-args)
appendSetting appendSetting
@@ -9,8 +14,7 @@ let
cfg = config.mailserver; cfg = config.mailserver;
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
submissionOptions.smtpd_sender_login_maps = submissionOptions.smtpd_sender_login_maps = lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}";
lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}";
commonLdapConfig = '' commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris} server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
start_tls = ${if cfg.ldap.startTls then "yes" else "no"} start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
@@ -61,8 +65,10 @@ in
${appendPwdInVirtualMailboxMap} ${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap} ${appendPwdInSenderLoginMap}
''; '';
restartTriggers = restartTriggers = [
[ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; appendPwdInVirtualMailboxMap
appendPwdInSenderLoginMap
];
wants = [ auth-passthru.oauth2-systemd-service ]; wants = [ auth-passthru.oauth2-systemd-service ];
after = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ];
}; };

View File

@@ -4,11 +4,17 @@ rec {
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
group = "dovecot2"; group = "dovecot2";
is-auth-enabled = is-auth-enabled =
config.selfprivacy.modules.simple-nixos-mailserver.enableSso config.selfprivacy.modules.simple-nixos-mailserver.enableSso && config.selfprivacy.sso.enable;
&& config.selfprivacy.sso.enable;
appendSetting = appendSetting =
{ name, file, prefix, suffix ? "", passwordFile, destination }: {
name,
file,
prefix,
suffix ? "",
passwordFile,
destination,
}:
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}
set -euo pipefail set -euo pipefail

View File

@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }@nixos-args: {
config,
lib,
pkgs,
...
}@nixos-args:
let let
sp = config.selfprivacy; sp = config.selfprivacy;
@@ -11,26 +16,22 @@ let
mailserver-service-account = { mailserver-service-account = {
mailserver-service-account-name = "sp.mailserver.service-account"; mailserver-service-account-name = "sp.mailserver.service-account";
mailserver-service-account-token-name = "mailserver-service-account-token"; mailserver-service-account-token-name = "mailserver-service-account-token";
mailserver-service-account-token-fp = mailserver-service-account-token-fp = "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
"/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
}; };
in in
lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ lib.mkIf sp.modules.simple-nixos-mailserver.enable (
{ lib.mkMerge [
assertions = [ {
{ assertions = [
assertion = {
config.selfprivacy.modules.simple-nixos-mailserver.enableSso assertion =
-> config.selfprivacy.sso.enable; config.selfprivacy.modules.simple-nixos-mailserver.enableSso -> config.selfprivacy.sso.enable;
message = message = "SSO cannot be enabled for Mailserver when SSO is disabled globally.";
"SSO cannot be enabled for Mailserver when SSO is disabled globally."; }
} ];
]; fileSystems = lib.mkIf sp.useBinds {
fileSystems = lib.mkIf sp.useBinds
{
"/var/vmail" = { "/var/vmail" = {
device = device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail";
"/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail";
options = [ options = [
"bind" "bind"
"x-systemd.required-by=postfix.service" "x-systemd.required-by=postfix.service"
@@ -38,8 +39,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
]; ];
}; };
"/var/sieve" = { "/var/sieve" = {
device = device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve";
"/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve";
options = [ options = [
"bind" "bind"
"x-systemd.required-by=dovecot2.service" "x-systemd.required-by=dovecot2.service"
@@ -48,114 +48,120 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
}; };
}; };
users.users = { users.users = {
virtualMail = { virtualMail = {
isNormalUser = false; isNormalUser = false;
};
};
users.groups.acmereceivers.members = [ "dovecot2" "postfix" "virtualMail" ];
mailserver = {
enable = true;
fqdn = sp.domain;
domains = [ sp.domain ];
localDnsResolver = false;
# A list of all login accounts. To create the password hashes, use
# mkpasswd -m sha-512 "super secret password"
loginAccounts = ({
"${sp.username}@${sp.domain}" = {
hashedPassword = sp.hashedMasterPassword;
sieveScript = ''
require ["fileinto", "mailbox"];
if header :contains "Chat-Version" "1.0"
{
fileinto :create "DeltaChat";
stop;
}
'';
}; };
} // builtins.listToAttrs (builtins.map
(user: {
name = "${user.username}@${sp.domain}";
value = {
hashedPassword = user.hashedPassword;
sieveScript = ''
require ["fileinto", "mailbox"];
if header :contains "Chat-Version" "1.0"
{
fileinto :create "DeltaChat";
stop;
}
'';
};
})
sp.users));
extraVirtualAliases = {
"admin@${sp.domain}" = "${sp.username}@${sp.domain}";
}; };
certificateScheme = "manual"; users.groups.acmereceivers.members = [
certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem"; "dovecot2"
keyFile = "/var/lib/acme/root-${sp.domain}/key.pem"; "postfix"
"virtualMail"
];
# Enable IMAP and POP3 mailserver = {
enableImap = true; enable = true;
enableImapSsl = true; fqdn = sp.domain;
enablePop3 = false; domains = [ sp.domain ];
enablePop3Ssl = false; localDnsResolver = false;
dkimSelector = "selector";
# Enable the ManageSieve protocol # A list of all login accounts. To create the password hashes, use
enableManageSieve = true; # mkpasswd -m sha-512 "super secret password"
loginAccounts = (
{
"${sp.username}@${sp.domain}" = {
hashedPassword = sp.hashedMasterPassword;
sieveScript = ''
require ["fileinto", "mailbox"];
if header :contains "Chat-Version" "1.0"
{
fileinto :create "DeltaChat";
stop;
}
'';
};
}
// builtins.listToAttrs (
builtins.map (user: {
name = "${user.username}@${sp.domain}";
value = {
hashedPassword = user.hashedPassword;
sieveScript = ''
require ["fileinto", "mailbox"];
if header :contains "Chat-Version" "1.0"
{
fileinto :create "DeltaChat";
stop;
}
'';
};
}) sp.users
)
);
virusScanning = false; extraVirtualAliases = {
"admin@${sp.domain}" = "${sp.username}@${sp.domain}";
};
mailDirectory = "/var/vmail"; certificateScheme = "manual";
}; certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem";
keyFile = "/var/lib/acme/root-${sp.domain}/key.pem";
systemd = { # Enable IMAP and POP3
services = { enableImap = true;
dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice"; enableImapSsl = true;
postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice"; enablePop3 = false;
rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; enablePop3Ssl = false;
redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; dkimSelector = "selector";
opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice";
# Enable the ManageSieve protocol
enableManageSieve = true;
virusScanning = false;
mailDirectory = "/var/vmail";
}; };
slices."simple_nixos_mailserver" = {
name = "simple_nixos_mailserver.slice"; systemd = {
description = "Simple NixOS Mailserver service slice"; services = {
dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice";
postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice";
rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice";
opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice";
};
slices."simple_nixos_mailserver" = {
name = "simple_nixos_mailserver.slice";
description = "Simple NixOS Mailserver service slice";
};
}; };
}; }
} # the following parts are active only when "auth" module is enabled
# the following parts are active only when "auth" module is enabled (lib.mkIf is-auth-enabled {
(lib.mkIf is-auth-enabled { mailserver = {
mailserver = { extraVirtualAliases = lib.mkForce { };
extraVirtualAliases = lib.mkForce { }; loginAccounts = lib.mkForce { };
loginAccounts = lib.mkForce { }; # LDAP is needed for Postfix to query Kanidm about email address ownership.
# LDAP is needed for Postfix to query Kanidm about email address ownership. # LDAP is needed for Dovecot also.
# LDAP is needed for Dovecot also. ldap = {
ldap = { # false; otherwise, simple-nixos-mailserver enables auth via LDAP
# false; otherwise, simple-nixos-mailserver enables auth via LDAP enable = false;
enable = false;
# bind.dn = "uid=mail,ou=persons," + ldap_base_dn; # bind.dn = "uid=mail,ou=persons," + ldap_base_dn;
bind.dn = "dn=token"; bind.dn = "dn=token";
# TODO change in this file should trigger system restart dovecot # TODO change in this file should trigger system restart dovecot
bind.passwordFile = bind.passwordFile = mailserver-service-account.mailserver-service-account-token-fp;
mailserver-service-account.mailserver-service-account-token-fp;
# searchBase = "ou=persons," + ldap_base_dn; # searchBase = "ou=persons," + ldap_base_dn;
searchBase = auth-passthru.ldap-base-dn; # TODO refine this searchBase = auth-passthru.ldap-base-dn; # TODO refine this
# NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS)
uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ];
};
}; };
}; })
}) (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix mailserver-service-account nixos-args))
(lib.mkIf is-auth-enabled (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args))
(import ./auth-dovecot.nix mailserver-service-account nixos-args)) ]
(lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args)) )
])

View File

@@ -1,43 +1,45 @@
{ {
description = "PoC SP module for the simple-nixos-mailserver"; description = "PoC SP module for the simple-nixos-mailserver";
inputs.mailserver.url = inputs.mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
gitlab:simple-nixos-mailserver/nixos-mailserver;
outputs = { self, mailserver }: { outputs =
nixosModules.default = _: { { self, mailserver }:
imports = [ {
mailserver.nixosModules.default nixosModules.default = _: {
./options.nix imports = [
./config.nix mailserver.nixosModules.default
]; ./options.nix
}; ./config.nix
configPathsNeeded = ];
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); };
meta = { lib, ... }: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "simple-nixos-mailserver"; { lib, ... }:
name = "Mail Server"; {
description = "E-Mail for company and family."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "simple-nixos-mailserver";
isMovable = true; name = "Mail Server";
isRequired = true; description = "E-Mail for company and family.";
canBeBackedUp = true; svgIcon = builtins.readFile ./icon.svg;
backupDescription = "Mail boxes and filters."; isMovable = true;
systemdServices = [ isRequired = true;
"dovecot2.service" canBeBackedUp = true;
"postfix.service" backupDescription = "Mail boxes and filters.";
]; systemdServices = [
user = "virtualMail"; "dovecot2.service"
folders = [ "postfix.service"
"/var/vmail" ];
"/var/sieve" user = "virtualMail";
]; folders = [
supportLevel = "normal"; "/var/vmail"
}; "/var/sieve"
];
supportLevel = "normal";
};
# TODO generate json docs from module? something like: # TODO generate json docs from module? something like:
# nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON' # nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON'
# (doesn't work because of `assertions`) # (doesn't work because of `assertions`)
}; };
} }

View File

@@ -1,31 +1,37 @@
{ lib, ... }: { lib, ... }:
{ {
options.selfprivacy.modules.simple-nixos-mailserver = { options.selfprivacy.modules.simple-nixos-mailserver = {
enable = (lib.mkOption { enable =
default = false; (lib.mkOption {
type = lib.types.bool; default = false;
description = "Enable mail server"; type = lib.types.bool;
}) // { description = "Enable mail server";
meta = { })
type = "enable"; // {
meta = {
type = "enable";
};
}; };
}; location =
location = (lib.mkOption { (lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "Location"; description = "Location";
}) // { })
meta = { // {
type = "location"; meta = {
type = "location";
};
}; };
}; enableSso =
enableSso = (lib.mkOption { (lib.mkOption {
default = true; default = true;
type = lib.types.bool; type = lib.types.bool;
description = "Enable SSO for mail server"; description = "Enable SSO for mail server";
}) // { })
meta = { // {
type = "enable"; meta = {
type = "enable";
};
}; };
};
}; };
} }

View File

@@ -5,37 +5,40 @@
nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11"; nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11";
}; };
outputs = {nixpkgs-24-11, ...}: { outputs =
nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux; { nixpkgs-24-11, ... }:
configPathsNeeded = {
builtins.fromJSON (builtins.readFile ./config-paths-needed.json); nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux;
meta = {lib, ...}: { configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
spModuleSchemaVersion = 1; meta =
id = "vikunja"; { lib, ... }:
name = "Vikunja"; {
description = "Vikunja, the fluffy, open-source, self-hostable to-do app."; spModuleSchemaVersion = 1;
svgIcon = builtins.readFile ./icon.svg; id = "vikunja";
isMovable = true; name = "Vikunja";
isRequired = false; description = "Vikunja, the fluffy, open-source, self-hostable to-do app.";
backupDescription = "Tasks and attachments."; svgIcon = builtins.readFile ./icon.svg;
systemdServices = [ isMovable = true;
"vikunja.service" isRequired = false;
]; backupDescription = "Tasks and attachments.";
folders = [ systemdServices = [
"/var/lib/vikunja" "vikunja.service"
]; ];
postgreDatabases = [ folders = [
"vikunja" "/var/lib/vikunja"
]; ];
license = [ postgreDatabases = [
lib.licenses.agpl3Plus "vikunja"
]; ];
homepage = "https://vikunja.io"; license = [
sourcePage = "https://github.com/go-vikunja/vikunja"; lib.licenses.agpl3Plus
supportLevel = "normal"; ];
sso = { homepage = "https://vikunja.io";
userGroup = "sp.vikunja.users"; sourcePage = "https://github.com/go-vikunja/vikunja";
}; supportLevel = "normal";
sso = {
userGroup = "sp.vikunja.users";
};
};
}; };
};
} }

View File

@@ -1,8 +1,10 @@
latestPkgs: { latestPkgs:
{
config, config,
lib, lib,
... ...
}: let }:
let
sp = config.selfprivacy; sp = config.selfprivacy;
cfg = sp.modules.vikunja; cfg = sp.modules.vikunja;
oauthClientID = "vikunja"; oauthClientID = "vikunja";
@@ -13,18 +15,16 @@ latestPkgs: {
# SelfPrivacy uses SP Module ID to identify the group! # SelfPrivacy uses SP Module ID to identify the group!
usersGroup = "sp.vikunja.users"; usersGroup = "sp.vikunja.users";
oauthClientSecretFP = oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: { vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: {
doCheck = false; # Tests are slow. doCheck = false; # Tests are slow.
patches = patches = (old.patches or [ ]) ++ [
(old.patches or []) ./load-client-secret-from-env.patch
++ [ ];
./load-client-secret-from-env.patch
];
}); });
in { in
{
options.selfprivacy.modules.vikunja = { options.selfprivacy.modules.vikunja = {
enable = enable =
(lib.mkOption { (lib.mkOption {
@@ -63,157 +63,168 @@ in {
}; };
}; };
config = config = lib.mkIf cfg.enable {
lib.mkIf cfg.enable assertions = [
{ {
assertions = [ assertion = sp.sso.enable;
{ message = "Vikunja cannot be enabled when SSO is disabled.";
assertion = sp.sso.enable; }
message = "Vikunja cannot be enabled when SSO is disabled."; ];
}
];
fileSystems = lib.mkIf sp.useBinds { fileSystems = lib.mkIf sp.useBinds {
"/var/lib/vikunja" = { "/var/lib/vikunja" = {
device = "/volumes/${cfg.location}/vikunja"; device = "/volumes/${cfg.location}/vikunja";
options = ["bind"]; options = [ "bind" ];
};
};
users = {
users.vikunja = {
isSystemUser = true;
group = "vikunja";
};
groups.vikunja = {};
};
services.postgresql = {
ensureDatabases = ["vikunja"];
ensureUsers = [
{
name = "vikunja";
ensureDBOwnership = true;
}
];
};
services.vikunja = {
enable = true;
package = vikunjaPackage;
frontendScheme = "https";
frontendHostname = "${cfg.subdomain}.${sp.domain}";
port = 4835;
database = {
type = "postgres";
host = "/run/postgresql";
};
settings = {
service = {
enableregistration = false;
enabletotp = false;
enableuserdeletion = true;
};
auth = {
local.enabled = false;
openid = {
enabled = true;
providers = [
{
name = oauth2-provider-name;
authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
clientid = oauthClientID;
clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable.
scope = "openid profile email";
}
];
};
};
};
};
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
useACMEHost = sp.domain;
forceSSL = true;
extraConfig = ''
add_header Strict-Transport-Security $hsts_header;
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
add_header 'Referrer-Policy' 'origin-when-cross-origin';
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
'';
locations = {
"/" = {
proxyPass = "http://127.0.0.1:4835";
};
};
};
systemd = {
services.vikunja = {
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja";
serviceConfig = {
Slice = "vikunja.slice";
LoadCredential = "oauth2-secret:${oauthClientSecretFP}";
DynamicUser = lib.mkForce false;
User = "vikunja";
Group = "vikunja";
AmbientCapabilities = [""];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
RemoveIPC = true;
SystemCallFilter = ["@system-service" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"];
};
environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret";
};
slices.vikunja = {
description = "Vikunja service slice";
};
};
selfprivacy.auth.clients.${oauthClientID} = {
inherit usersGroup;
subdomain = cfg.subdomain;
isTokenNeeded = false;
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}";
clientSystemdUnits = ["vikunja.service"];
enablePkce = false;
linuxUserOfClient = "vikunja";
linuxGroupOfClient = "vikunja";
}; };
}; };
users = {
users.vikunja = {
isSystemUser = true;
group = "vikunja";
};
groups.vikunja = { };
};
services.postgresql = {
ensureDatabases = [ "vikunja" ];
ensureUsers = [
{
name = "vikunja";
ensureDBOwnership = true;
}
];
};
services.vikunja = {
enable = true;
package = vikunjaPackage;
frontendScheme = "https";
frontendHostname = "${cfg.subdomain}.${sp.domain}";
port = 4835;
database = {
type = "postgres";
host = "/run/postgresql";
};
settings = {
service = {
enableregistration = false;
enabletotp = false;
enableuserdeletion = true;
};
auth = {
local.enabled = false;
openid = {
enabled = true;
providers = [
{
name = oauth2-provider-name;
authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
clientid = oauthClientID;
clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable.
scope = "openid profile email";
}
];
};
};
};
};
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
useACMEHost = sp.domain;
forceSSL = true;
extraConfig = ''
add_header Strict-Transport-Security $hsts_header;
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
add_header 'Referrer-Policy' 'origin-when-cross-origin';
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
'';
locations = {
"/" = {
proxyPass = "http://127.0.0.1:4835";
};
};
};
systemd = {
services.vikunja = {
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja";
serviceConfig = {
Slice = "vikunja.slice";
LoadCredential = "oauth2-secret:${oauthClientSecretFP}";
DynamicUser = lib.mkForce false;
User = "vikunja";
Group = "vikunja";
AmbientCapabilities = [ "" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
RemoveIPC = true;
SystemCallFilter = [
"@system-service"
"~@cpu-emulation"
"~@debug"
"~@keyring"
"~@memlock"
"~@obsolete"
"~@privileged"
"~@setuid"
];
};
environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret";
};
slices.vikunja = {
description = "Vikunja service slice";
};
};
selfprivacy.auth.clients.${oauthClientID} = {
inherit usersGroup;
subdomain = cfg.subdomain;
isTokenNeeded = false;
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}";
clientSystemdUnits = [ "vikunja.service" ];
enablePkce = false;
linuxUserOfClient = "vikunja";
linuxGroupOfClient = "vikunja";
};
};
} }

View File

@@ -6,21 +6,23 @@ in
users = { users = {
mutableUsers = false; mutableUsers = false;
allowNoPasswordLogin = true; allowNoPasswordLogin = true;
users = { users =
"${cfg.username}" = { {
isNormalUser = true; "${cfg.username}" = {
hashedPassword = cfg.hashedMasterPassword;
openssh.authorizedKeys.keys = cfg.sshKeys;
};
} // builtins.listToAttrs (builtins.map
(user: {
name = "${user.username}";
value = {
isNormalUser = true; isNormalUser = true;
hashedPassword = user.hashedPassword; hashedPassword = cfg.hashedMasterPassword;
openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]); openssh.authorizedKeys.keys = cfg.sshKeys;
}; };
}) }
cfg.users); // builtins.listToAttrs (
builtins.map (user: {
name = "${user.username}";
value = {
isNormalUser = true;
hashedPassword = user.hashedPassword;
openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]);
};
}) cfg.users
);
}; };
} }

View File

@@ -3,13 +3,13 @@ let
cfg = config.selfprivacy; cfg = config.selfprivacy;
in in
{ {
fileSystems = builtins.listToAttrs (builtins.map fileSystems = builtins.listToAttrs (
(volume: { builtins.map (volume: {
name = "${volume.mountPoint}"; name = "${volume.mountPoint}";
value = { value = {
device = "${volume.device}"; device = "${volume.device}";
fsType = "${volume.fsType}"; fsType = "${volume.fsType}";
}; };
}) }) cfg.volumes
cfg.volumes); );
} }