Compare commits

..

3 commits

Author SHA1 Message Date
Patrick 37889dbdd3
feat: kanidm provision
feat: move gitea to forgejo
feat: fix streamdeck
2024-03-12 18:19:52 +01:00
Patrick cb72a7a224
fix: printer samba access rules 2024-03-12 15:15:37 +01:00
Patrick dacc8ad9e0
feat: scan to samba 2024-03-09 20:32:09 +01:00
22 changed files with 1037 additions and 175 deletions

View file

@ -8,7 +8,7 @@
...
}: let
adguardhomedomain = "adguardhome.${config.secrets.secrets.global.domains.web}";
giteadomain = "git.${config.secrets.secrets.global.domains.web}";
forgejoDomain = "git.${config.secrets.secrets.global.domains.web}";
immichdomain = "immich.${config.secrets.secrets.global.domains.web}";
nextclouddomain = "nc.${config.secrets.secrets.global.domains.web}";
ollamadomain = "ollama.${config.secrets.secrets.global.domains.web}";
@ -44,19 +44,19 @@ in {
'';
};
upstreams.gitea = {
servers."${ipOf "gitea"}:3000" = {};
upstreams.forgejo = {
servers."${ipOf "forgejo"}:3000" = {};
extraConfig = ''
zone gitea 64k ;
zone forgejo 64k ;
keepalive 5 ;
'';
};
virtualHosts.${giteadomain} = {
virtualHosts.${forgejoDomain} = {
forceSSL = true;
useACMEHost = "web";
locations."/" = {
proxyPass = "http://gitea";
proxyPass = "http://forgejo";
proxyWebsockets = true;
};
extraConfig = ''
@ -326,7 +326,7 @@ in {
// mkContainer "paperless" {
enableSharedPaperless = true;
}
// mkContainer "gitea" {
// mkContainer "forgejo" {
enablePanzer = true;
}
// mkMicrovm "immich" {

View file

@ -0,0 +1,15 @@
age-encryption.org/v1
-> X25519 gc7a2OUDl5ZLAWsWvg+Vq8OjjQ8gybzm6FFqUWCelWE
BMYy6ekQeRNwNyUMZeSYH2ff5+1dfy+2A885JR5XRXo
-> piv-p256 XTQkUA A0N6GlqsddLw7SHSBKQ8XdYJD/q42y9DY7A4DnAq8cHQ
syaYAv5cXNcXZRs+Nzwj14Eai0EHGjtu3o3IBt1dg5k
-> piv-p256 ZFgiIw A7zvUk0WKnNZG8fHeTTOqNAR7Gf1SVvEVINMOGH6fetZ
6R2zFSXZXDFRhGg8+AFBQvTKWbDPZIYgkDY19cxmqi4
-> piv-p256 5vmPtQ AoQwhdsUwjX8Dn63egOz92T+XNt5/MMxsL5T+7o1Jrvz
TnN51QUPA1wcHBCR6KYT0xFH5WVT+FAI/QhMTW3ajGE
-> piv-p256 ZFgiIw Azh9/Kn00w3JVQvlarDwwrXRnOGeTHYHtEF2gK6i2qW+
gIp88ToCDc80WlJikNyKw8up+GWWexzMoaQVuLBRIfQ
-> Zr~B*Za-grease
20yMZ5Gv9nTK1qQVWe7lSAspqwdpUGVnX4r8M8UNvaQGNbSDFghj
--- BleVfNvY9wSlTcM42gt7rKRxmDG4LUlWHHi3vWgz6Og
ð#e¢ó`¤½Í\ß|<7C>¤ã5­ Ø&\<5C>®0… #áºHZrnF¬“b<"¶mùšSP²úýãébLµ¿Õ†fVs.ù–¶?<3F>Üåän<>#\0ðÌw/Ïì.áh}w…S¥"Eé*Ö¶<>ãSÂ>׿huÜn)맃¿ÓÝEãRðpBÀG<>àãà#!n4¡¥¾CKVÐöÇ?

View file

@ -1,16 +0,0 @@
age-encryption.org/v1
-> X25519 nbSYyNcboZYI9JVKkImhppQToS3XQxjiriBS0TAQrE4
/g8LhlSY1YxQQo47FuzzXNPTVsnBgC+TRh9BBq3WVLI
-> piv-p256 XTQkUA AyItco2APIdcG+okYszuK5aqGOlkfD7at+sG53awGFAQ
KTD5Nr2u6U6oVEYDNDLO8U3zVMLWypHHS0Y+9Q36TGM
-> piv-p256 ZFgiIw AhZRrPWodlNzQAMgFZDOiamtdsWE8vtjGXNpbGsCLnpY
MmHIE4jSpYZFseJpG2Tdj+CnqjIkk/dSUoOJaRoiWe0
-> piv-p256 5vmPtQ AvgHDtaekIaAbj7gmUJP0xWwOpESKSgQAUd4voq2ee9c
XIFV78uWrOgJrQ7cpYmIrMsGpkji1GqvSuAIKRq+Tt0
-> piv-p256 ZFgiIw ApCcEoQQOcikT1hANSjOmzLiL6tNhk2vJ96HDNtJuMpd
geXW5whCUN4Hr5zvko5B94aHhirsmUXpoMBGKrZtZGA
-> '@Uo0`]-grease 5:un}:j
8NEXuaIP6w
--- jQD1YtAJ15d+eIy6nPqHHUMTLJtgAg/+5sqQbw0Q0Ko
ë„sσŸ„ú=]Ý%ʹîÊL ÞÛÀËlÈáÑieMá—þŒØ\Y²CoÁˆ-sœ†«4M
$\¡ÿwµ,šM‡g´tõñ  •#V<>p

View file

@ -0,0 +1,16 @@
age-encryption.org/v1
-> X25519 vc309RP8AQvszDq+D1f4f5dWrqOpjt2lKmci9507CUQ
TKV60m9WQendmKOKVJuM6JvvQPb7dsAO8+Tn3NNjdt4
-> piv-p256 XTQkUA A/zSSbrKLxl31vDGaKfSTqJfX/I0ESYjApL1FIcwbRT/
f7tAcOo7eDCACUjO++5Ca8UYsEMyfEosMEhoRyZlv2s
-> piv-p256 ZFgiIw AveXSkbOt33wUXtCc1IDmssYIJfUOSVm/zamehQDBaN6
Cq7pLQL5ED3lSZrive0vyoipe1PN4FpbNAKDYIHWE1U
-> piv-p256 5vmPtQ Atez78iMxWLAq114XO7wwU1JiSTU50wfohvJ0LQUL5JI
oHv2dlB91XwPpL8OXFZyCE4CEGe7H+OKkef3m178a/w
-> piv-p256 ZFgiIw AjTxVdEB+Wck0N07i2nvkWtzH13Vfznl7TqS6kqwvGet
gN2xJG0OOfqtimsf9qwoYk8fB4A5GQILt6kWScZ+ono
-> WR-grease ]x3SA4 w6 7\t D2b
4QgiTLjrIaQBKUc3cNs0+Hyqo9ccxdAstepUX1Ps4HLqGBNrIUkM2Q
--- CylSm6+Ki9f+ilXS7UTvDxDlLNLLvRaJNOw9kHt6TVk
(<28>ó˜Fg*î†7ÝÒ"¢‡<E280A1>ä[šÌ¸…¼f­<66>H®¶}¿Æ²Ìì†c*—l§JR˜cÁŽ}5ò™¬ Ül
§·ç}½ãáŸ"S™„ØÏ

View file

@ -21,7 +21,7 @@
nextcloud = uidGid 213;
redis-nextcloud = uidGid 214;
radicale = uidGid 215;
gitea = uidGid 215;
forgejo = uidGid 215;
vaultwarden = uidGid 215;
redis-paperless = uidGid 216;
microvm = uidGid 217;
@ -40,5 +40,6 @@
helen = uidGid 2001;
ggr = uidGid 2002;
family = uidGid 2003;
printer = uidGid 2005;
};
}

854
modules/kanidm.nix Normal file
View file

@ -0,0 +1,854 @@
{
config,
lib,
options,
pkgs,
...
}: let
inherit
(lib)
any
attrNames
attrValues
concatLines
concatLists
converge
filter
filterAttrs
filterAttrsRecursive
flip
foldl'
getExe
hasInfix
hasPrefix
isStorePath
last
mapAttrsToList
mdDoc
mkEnableOption
mkForce
mkIf
mkMerge
mkOption
mkPackageOption
optional
optionalString
splitString
subtractLists
types
unique
;
cfg = config.services.kanidm;
settingsFormat = pkgs.formats.toml {};
# Remove null values, so we can document optional values that don't end up in the generated TOML file.
filterConfig = converge (filterAttrsRecursive (_: v: v != null));
serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
certPaths = builtins.map builtins.dirOf [cfg.serverSettings.tls_chain cfg.serverSettings.tls_key];
# Merge bind mount paths and remove paths where a prefix is already mounted.
# This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
# paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
hasPrefixInList = list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
mergePaths = foldl' (merged: newPath: let
# 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;
# If a prefix of the new path is already in the list, do not add it
filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath;
in
filteredPaths ++ filteredNew) [];
defaultServiceConfig = {
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
];
CapabilityBoundingSet = [];
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
# Implies ProtectSystem=strict, which re-mounts all paths
# DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
# Would re-mount paths ignored by temporary root
#ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = ["@system-service" "~@privileged @resources @setuid @keyring"];
# Does not work well with the temporary root
#UMask = "0066";
};
mkPresentOption = what:
mkOption {
description = mdDoc "Whether to ensure that this ${what} is present or absent.";
type = types.bool;
default = true;
};
filterPresent = filterAttrs (_: v: v.present);
provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON {
inherit (cfg.provision) groups persons systems;
});
serverPort =
# ipv6:
if hasInfix "]:" cfg.serverSettings.bindaddress
then last (splitString "]:" cfg.serverSettings.bindaddress)
else
# ipv4:
if hasInfix "." cfg.serverSettings.bindaddress
then last (splitString ":" cfg.serverSettings.bindaddress)
# default is 8443
else "8443";
# Only recover the admin account if a password should explicitly be provisioned
# for the account. Otherwise it is not needed for provisioning.
maybeRecoverAdmin = optionalString (cfg.provision.adminPasswordFile != null) ''
KANIDM_ADMIN_PASSWORD=$(< ${cfg.provision.adminPasswordFile})
# We always reset the admin account password if a desired password was specified.
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} admin --from-environment >/dev/null; then
echo "Failed to recover admin account" >&2
exit 1
fi
'';
# Recover the idm_admin account. If a password should explicitly be provisioned
# for the account we set it, otherwise we generate a new one because it is required
# for provisioning.
recoverIdmAdmin =
if cfg.provision.idmAdminPasswordFile != null
then ''
KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
# We always reset the idm_admin account password if a desired password was specified.
if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
echo "Failed to recover idm_admin account" >&2
exit 1
fi
''
else ''
# Recover idm_admin account
if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to recover admin account" >&2
exit 1
fi
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${getExe pkgs.jq} -r .password); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
exit 1
fi
'';
postStartScript = pkgs.writeShellScript "post-start" ''
set -euo pipefail
# Wait for the kanidm server to come online
count=0
while ! test -e /run/kanidmd/sock; do
sleep 0.1
if [[ "$count" -eq 600 ]]; then
echo "Tried for 60 seconds, giving up..."
exit 1
fi
if [[ ! -d "/proc/$MAINPID" ]]; then
echo "Main server died, giving up..."
exit 1
fi
count=$((count++))
done
${recoverIdmAdmin}
${maybeRecoverAdmin}
KANIDM_PROVISION_IDM_ADMIN_TOKEN=$KANIDM_IDM_ADMIN_PASSWORD \
${getExe pkgs.kanidm-provision} --url "${cfg.provision.instanceUrl}" --state ${provisionStateJson} ${optionalString cfg.provision.acceptInvalidCerts "--accept-invalid-certs"}
'';
in {
options.services.kanidm = {
enableClient = mkEnableOption (mdDoc "the Kanidm client");
enableServer = mkEnableOption (mdDoc "the Kanidm server");
enablePam = mkEnableOption (mdDoc "the Kanidm PAM and NSS integration");
package = mkPackageOption pkgs "kanidm" {};
serverSettings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
bindaddress = mkOption {
description = mdDoc "Address/port combination the webserver binds to.";
example = "[::1]:8443";
type = types.str;
};
# Should be optional but toml does not accept null
ldapbindaddress = mkOption {
description = mdDoc ''
Address and port the LDAP server is bound to. Setting this to `null` disables the LDAP interface.
'';
example = "[::1]:636";
default = null;
type = types.nullOr types.str;
};
origin = mkOption {
description = mdDoc "The origin of your Kanidm instance. Must have https as protocol.";
example = "https://idm.example.org";
type = types.strMatching "^https://.*";
};
domain = mkOption {
description = mdDoc ''
The `domain` that Kanidm manages. Must be below or equal to the domain
specified in `serverSettings.origin`.
This can be left at `null`, only if your instance has the role `ReadOnlyReplica`.
While it is possible to change the domain later on, it requires extra steps!
Please consider the warnings and execute the steps described
[in the documentation](https://kanidm.github.io/kanidm/stable/administrivia.html#rename-the-domain).
'';
example = "example.org";
default = null;
type = types.nullOr types.str;
};
db_path = mkOption {
description = mdDoc "Path to Kanidm database.";
default = "/var/lib/kanidm/kanidm.db";
readOnly = true;
type = types.path;
};
tls_chain = mkOption {
description = mdDoc "TLS chain in pem format.";
type = types.path;
};
tls_key = mkOption {
description = mdDoc "TLS key in pem format.";
type = types.path;
};
log_level = mkOption {
description = mdDoc "Log level of the server.";
default = "info";
type = types.enum ["info" "debug" "trace"];
};
role = mkOption {
description = mdDoc "The role of this server. This affects the replication relationship and thereby available features.";
default = "WriteReplica";
type = types.enum ["WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica"];
};
online_backup = {
path = mkOption {
description = mdDoc "Path to the output directory for backups.";
type = types.path;
default = "/var/lib/kanidm/backups";
};
schedule = mkOption {
description = mdDoc "The schedule for backups in cron format.";
type = types.str;
default = "00 22 * * *";
};
versions = mkOption {
description = mdDoc ''
Number of backups to keep.
The default is set to `0`, in order to disable backups by default.
'';
type = types.ints.unsigned;
default = 0;
example = 7;
};
};
};
};
default = {};
description = mdDoc ''
Settings for Kanidm, see
[the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
for possible values.
'';
};
clientSettings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options.uri = mkOption {
description = mdDoc "Address of the Kanidm server.";
example = "http://127.0.0.1:8080";
type = types.str;
};
};
description = mdDoc ''
Configure Kanidm clients, needed for the PAM daemon. See
[the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
for possible values.
'';
};
unixSettings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
pam_allowed_login_groups = mkOption {
description = mdDoc "Kanidm groups that are allowed to login using PAM.";
example = "my_pam_group";
type = types.listOf types.str;
};
hsm_pin_path = mkOption {
description = mdDoc "Path to a HSM pin.";
default = "/var/cache/kanidm-unixd/hsm-pin";
type = types.path;
};
};
};
description = mdDoc ''
Configure Kanidm unix daemon.
See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
for possible values.
'';
};
provision = {
enable = mkEnableOption "provisioning of groups, users and oauth2 resource servers";
instanceUrl = mkOption {
description = "The instance url to which the provisioning tool should connect.";
default = "https://localhost:${serverPort}";
defaultText = ''"https://localhost:<port from serverSettings.bindaddress>"'';
type = types.str;
};
acceptInvalidCerts = mkOption {
description = ''
Whether to allow invalid certificates when provisioning the target instance.
By default this is only allowed when the instanceUrl is localhost. This is
dangerous when used with an external URL.
'';
type = types.bool;
default = hasPrefix "https://localhost:" cfg.provision.instanceUrl;
defaultText = ''hasPrefix "https://localhost:" cfg.provision.instanceUrl'';
};
adminPasswordFile = mkOption {
description = "Path to a file containing the admin password for kanidm. Do NOT use a file from the nix store here!";
example = "/run/secrets/kanidm-admin-password";
default = null;
type = types.nullOr types.path;
};
idmAdminPasswordFile = mkOption {
description = ''
Path to a file containing the idm admin password for kanidm. Do NOT use a file from the nix store here!
If this is not given but provisioning is enabled, the idm_admin password will be reset on each restart.
'';
example = "/run/secrets/kanidm-idm-admin-password";
default = null;
type = types.nullOr types.path;
};
autoRemove = mkOption {
description = ''
Determines whether deleting an entity in this provisioning config should automatically
cause them to be removed from kanidm, too. This works because the provisioning tool tracks
all entities it has ever created. If this is set to false, you need to explicitly specify
`present = false` to delete an entity.
'';
type = types.bool;
default = true;
};
groups = mkOption {
description = "Provisioning of kanidm groups";
default = {};
type = types.attrsOf (types.submodule (groupSubmod: {
options = {
present = mkPresentOption "group";
members = mkOption {
description = "List of kanidm entities (persons, groups, ...) which are part of this group.";
type = types.listOf types.str;
apply = unique;
default = [];
};
};
config.members = concatLists (flip mapAttrsToList cfg.provision.persons (
person: personCfg:
optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person
));
}));
};
persons = mkOption {
description = "Provisioning of kanidm persons";
default = {};
type = types.attrsOf (types.submodule {
options = {
present = mkPresentOption "person";
displayName = mkOption {
description = "Display name";
type = types.str;
example = "My User";
};
legalName = mkOption {
description = "Full legal name";
type = types.nullOr types.str;
example = "Jane Doe";
default = null;
};
mailAddresses = mkOption {
description = "Mail addresses. First given address is considered the primary address.";
type = types.listOf types.str;
example = ["jane.doe@example.com"];
default = [];
};
groups = mkOption {
description = "List of groups this person should belong to.";
type = types.listOf types.str;
apply = unique;
default = [];
};
};
});
};
systems.oauth2 = mkOption {
description = "Provisioning of oauth2 resource servers";
default = {};
type = types.attrsOf (types.submodule {
options = {
present = mkPresentOption "oauth2 resource server";
displayName = mkOption {
description = "Display name";
type = types.str;
example = "Some Service";
};
originUrl = mkOption {
description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
type = types.strMatching ".*://.*/$";
example = "https://someservice.example.com/";
};
originLanding = mkOption {
description = "When redirecting from the Kanidm Apps Listing page, some linked applications may need to land on a specific page to trigger oauth2/oidc interactions.";
type = types.nullOr types.str;
default = null;
example = "https://someservice.example.com/home";
};
basicSecretFile = mkOption {
description = ''
The basic secret to use for this service. If null, the random secret generated
by kanidm will not be touched. Do NOT use a path from the nix store here!
'';
type = types.nullOr types.path;
example = "/run/secrets/some-oauth2-basic-secret";
default = null;
};
allowInsecureClientDisablePkce = mkOption {
description = ''
Disable PKCE on this oauth2 resource server to work around insecure clients
that may not support it. You should request the client to enable PKCE!
'';
type = types.bool;
default = false;
};
preferShortUsername = mkOption {
description = "Use 'name' instead of 'spn' in the preferred_username claim";
type = types.bool;
default = false;
};
scopeMaps = mkOption {
description = ''
Maps kanidm groups to returned oauth scopes.
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
'';
type = types.attrsOf (types.listOf types.str);
default = {};
};
supplementaryScopeMaps = mkOption {
description = ''
Maps kanidm groups to additionally returned oauth scopes.
See [Scope Relations](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#scope-relationships) for more information.
'';
type = types.attrsOf (types.listOf types.str);
default = {};
};
removeOrphanedClaimMaps = mkOption {
description = "Whether claim maps not specified here but present in kanidm should be removed from kanidm.";
type = types.bool;
default = true;
};
claimMaps = mkOption {
description = ''
Adds additional claims (and values) based on which kanidm groups an authenticating party belongs to.
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
'';
default = {};
type = types.attrsOf (types.submodule {
options = {
joinType = mkOption {
description = ''
Determines how multiple values are joined to create the claim value.
See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
'';
type = types.enum ["array" "csv" "ssv"];
default = "array";
};
valuesByGroup = mkOption {
description = "Maps kanidm groups to values for the claim.";
default = {};
type = types.attrsOf (types.listOf types.str);
};
};
});
};
};
});
};
};
};
config = mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
assertions = let
entityList = type: attrs: flip mapAttrsToList (filterPresent attrs) (name: _: {inherit type name;});
entities =
entityList "group" cfg.provision.groups
++ entityList "person" cfg.provision.persons
++ entityList "oauth2" cfg.provision.systems.oauth2;
# Accumulate entities by name. Track corresponding entity types for later duplicate check.
entitiesByName =
foldl' (
acc: {
type,
name,
}:
acc
// {
${name} = (acc.${name} or []) ++ [type];
}
) {}
entities;
assertGroupsKnown = opt: groups: let
knownGroups = attrNames (filterPresent cfg.provision.groups);
unknownGroups = subtractLists knownGroups groups;
in {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [];
message = "${opt} refers to unknown groups: ${toString unknownGroups}";
};
assertEntitiesKnown = opt: entities: let
unknownEntities = subtractLists (attrNames entitiesByName) entities;
in {
assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [];
message = "${opt} refers to unknown entities: ${toString unknownEntities}";
};
in
[
{
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!isStorePath cfg.serverSettings.tls_chain);
message = ''
<option>services.kanidm.serverSettings.tls_chain</option> points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
{
assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!isStorePath cfg.serverSettings.tls_key);
message = ''
<option>services.kanidm.serverSettings.tls_key</option> points to
a file in the Nix store. You should use a quoted absolute path to
prevent this.
'';
}
{
assertion = !cfg.enableClient || options.services.kanidm.clientSettings.isDefined;
message = ''
<option>services.kanidm.clientSettings</option> needs to be configured
if the client is enabled.
'';
}
{
assertion = !cfg.enablePam || options.services.kanidm.clientSettings.isDefined;
message = ''
<option>services.kanidm.clientSettings</option> needs to be configured
for the PAM daemon to connect to the Kanidm server.
'';
}
{
assertion =
!cfg.enableServer
|| (cfg.serverSettings.domain
== null
-> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
message = ''
<option>services.kanidm.serverSettings.domain</option> can only be set if this instance
is not a ReadOnlyReplica. Otherwise the db would inherit it from
the instance it follows.
'';
}
{
assertion = cfg.provision.enable -> cfg.enableServer;
message = "<option>services.kanidm.provision</option> requires <option>services.kanidm.enableServer</option> to be true";
}
# If any secret is provisioned, the kanidm package must have some required patches applied to it
{
assertion =
(cfg.provision.enable
&& (
cfg.provision.adminPasswordFile
!= null
|| cfg.provision.idmAdminPasswordFile != null
|| any (x: x.basicSecretFile != null) (attrValues (filterPresent cfg.provision.systems.oauth2))
))
-> cfg.package.enableSecretProvisioning;
message = ''
Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches.
You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
'';
}
# Entity names must be globally unique:
(let
# Filter all names that occurred in more than one entity type.
duplicateNames = filterAttrs (_: v: builtins.length v > 1) entitiesByName;
in {
assertion = cfg.provision.enable -> duplicateNames == {};
message = ''
services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
${concatLines (mapAttrsToList (name: xs: " - '${name}' used as: ${toString xs}") duplicateNames)}'';
})
]
++ flip mapAttrsToList (filterPresent cfg.provision.persons) (
person: personCfg:
assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
)
++ flip mapAttrsToList (filterPresent cfg.provision.groups) (
group: groupCfg:
assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
)
++ concatLists (flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
oauth2: oauth2Cfg:
[
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (attrNames oauth2Cfg.scopeMaps))
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (attrNames oauth2Cfg.supplementaryScopeMaps))
]
++ concatLists (flip mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [
(assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (attrNames claimCfg.valuesByGroup))
# At least one group must map to a value in each claim map
{
assertion = (cfg.provision.enable && cfg.enableServer) -> any (xs: xs != []) (attrValues claimCfg.valuesByGroup);
message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
}
]))
));
environment.systemPackages = mkIf cfg.enableClient [cfg.package];
systemd.tmpfiles.settings."10-kanidm" = {
${cfg.serverSettings.online_backup.path}.d = {
mode = "0700";
user = "kanidm";
group = "kanidm";
};
};
systemd.services.kanidm = mkIf cfg.enableServer {
description = "kanidm identity management daemon";
wantedBy = ["multi-user.target"];
after = ["network.target"];
serviceConfig = mkMerge [
# Merge paths and ignore existing prefixes needs to sidestep mkMerge
(defaultServiceConfig
// {
BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
})
{
StateDirectory = "kanidm";
StateDirectoryMode = "0700";
RuntimeDirectory = "kanidmd";
ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
ExecStartPost = mkIf cfg.provision.enable postStartScript;
User = "kanidm";
Group = "kanidm";
BindPaths = [
# To create the socket
"/run/kanidmd:/run/kanidmd"
# To store backups
cfg.serverSettings.online_backup.path
];
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
# This would otherwise override the CAP_NET_BIND_SERVICE capability.
PrivateUsers = mkForce false;
# Port needs to be exposed to the host network
PrivateNetwork = mkForce false;
RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"];
TemporaryFileSystem = "/:ro";
}
];
environment.RUST_LOG = "info";
};
systemd.services.kanidm-unixd = mkIf cfg.enablePam {
description = "Kanidm PAM daemon";
wantedBy = ["multi-user.target"];
after = ["network.target"];
restartTriggers = [unixConfigFile clientConfigFile];
serviceConfig = mkMerge [
defaultServiceConfig
{
CacheDirectory = "kanidm-unixd";
CacheDirectoryMode = "0700";
RuntimeDirectory = "kanidm-unixd";
ExecStart = "${cfg.package}/bin/kanidm_unixd";
User = "kanidm-unixd";
Group = "kanidm-unixd";
BindReadOnlyPaths = [
"-/etc/kanidm"
"-/etc/static/kanidm"
"-/etc/ssl"
"-/etc/static/ssl"
"-/etc/passwd"
"-/etc/group"
];
BindPaths = [
# To create the socket
"/run/kanidm-unixd:/var/run/kanidm-unixd"
];
# Needs to connect to kanidmd
PrivateNetwork = mkForce false;
RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"];
TemporaryFileSystem = "/:ro";
}
];
environment.RUST_LOG = "info";
};
systemd.services.kanidm-unixd-tasks = mkIf cfg.enablePam {
description = "Kanidm PAM home management daemon";
wantedBy = ["multi-user.target"];
after = ["network.target" "kanidm-unixd.service"];
partOf = ["kanidm-unixd.service"];
restartTriggers = [unixConfigFile clientConfigFile];
serviceConfig = {
ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
"-/etc/kanidm"
"-/etc/static/kanidm"
];
BindPaths = [
# To manage home directories
"/home"
# To connect to kanidm-unixd
"/run/kanidm-unixd:/var/run/kanidm-unixd"
];
# CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
CapabilityBoundingSet = ["CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH"];
IPAddressDeny = "any";
# Need access to users
PrivateUsers = false;
# Need access to home directories
ProtectHome = false;
RestrictAddressFamilies = ["AF_UNIX"];
TemporaryFileSystem = "/:ro";
Restart = "on-failure";
};
environment.RUST_LOG = "info";
};
# These paths are hardcoded
environment.etc = mkMerge [
(mkIf cfg.enableServer {
"kanidm/server.toml".source = serverConfigFile;
})
(mkIf options.services.kanidm.clientSettings.isDefined {
"kanidm/config".source = clientConfigFile;
})
(mkIf cfg.enablePam {
"kanidm/unixd".source = unixConfigFile;
})
];
system.nssModules = mkIf cfg.enablePam [cfg.package];
system.nssDatabases.group = optional cfg.enablePam "kanidm";
system.nssDatabases.passwd = optional cfg.enablePam "kanidm";
users.groups = mkMerge [
(mkIf cfg.enableServer {
kanidm = {};
})
(mkIf cfg.enablePam {
kanidm-unixd = {};
})
];
users.users = mkMerge [
(mkIf cfg.enableServer {
kanidm = {
description = "Kanidm server";
isSystemUser = true;
group = "kanidm";
packages = [cfg.package];
};
})
(mkIf cfg.enablePam {
kanidm-unixd = {
description = "Kanidm PAM daemon";
isSystemUser = true;
group = "kanidm-unixd";
};
})
];
};
meta.maintainers = with lib.maintainers; [erictapen Flakebi oddlama];
meta.buildDocsInSandbox = false;
}

View file

@ -14,7 +14,15 @@
settingsFormat = pkgs.formats.json {};
in {
home-manager.sharedModules = [
({config, ...}: {
({config, ...}: let
cfg = settingsFormat.generate "config.json" {
streamdeck_ui_version = 1;
state = config.programs.streamdeck-ui.settings;
};
preStart = pkgs.writeShellScript "streamdeck-setup-config" ''
cp "${cfg}" "$XDG_RUNTIME_DIR/streamdeck/config.json"
'';
in {
options.programs.streamdeck-ui = {
enable = mkEnableOption "streamdeck-ui";
package = mkPackageOption pkgs "streamdeck-ui" {};
@ -36,20 +44,14 @@ in {
Service = {
Type = "exec";
ExecStart = "${pkgs.streamdeck-ui}/bin/streamdeck --no-ui";
Environment = "STREAMDECK_UI_CONFIG=${config.xdg.configHome}/streamdeck-ui/config.json";
ExecStartPre = preStart;
Environment = ''STREAMDECK_UI_CONFIG=%t/streamdeck/config.json'';
RuntimeDirectory = "streamdeck";
};
Install.WantedBy = ["graphical-session.target"];
};
};
};
xdg.configFile.streamdeck-ui = {
target = "streamdeck-ui/config.json";
source = settingsFormat.generate "config.json" {
streamdeck_ui_version = 1;
state = config.programs.streamdeck-ui.settings;
};
};
};
})
];

View file

@ -1,104 +0,0 @@
{
config,
pkgs,
...
}: let
stateDir = "/var/lib/authelia-main";
in {
age.secrets.jwtSecretFile = {
generator.script = "alnum";
mode = "440";
inherit (config.services.authelia.instances.main) group;
};
age.secrets.sessionSecretFile = {
generator.script = "alnum";
mode = "440";
inherit (config.services.authelia.instances.main) group;
};
age.secrets.storageEncryptionKeyFile = {
generator.script = "alnum";
mode = "440";
inherit (config.services.authelia.instances.main) group;
};
age.secrets.oidcHmacSecretFile = {
generator.script = "alnum";
mode = "440";
inherit (config.services.authelia.instances.main) group;
};
age.secrets.oidcIssuerPrivateKeyFile = {
generator.script = {pkgs, ...}: ''
${pkgs.openssl}/bin/openssl genrsa 4096
'';
mode = "440";
inherit (config.services.authelia.instances.main) group;
};
networking.firewall.allowedTCPPorts = [config.services.authelia.instances.main.settings.server.port];
services.authelia.instances.main = {
enable = true;
secrets = {
jwtSecretFile = config.age.secrets.jwtSecretFile.path;
sessionSecretFile = config.age.secrets.sessionSecretFile.path;
storageEncryptionKeyFile = config.age.secrets.storageEncryptionKeyFile.path;
oidcHmacSecretFile = config.age.secrets.oidcHmacSecretFile.path;
oidcIssuerPrivateKeyFile = config.age.secrets.oidcIssuerPrivateKeyFile.path;
}; # TODO
settings = {
session = {
domain = config.secrets.secrets.global.domains.web;
};
webauthn.disable = true;
duo_api.disable = true;
ntp.disable_startup_check = true;
theme = "dark";
default_2fa_method = "totp";
server.host = "0.0.0.0";
access_control.default_policy = "one_factor";
webauthn = {
attestation_conveyance_preference = "none";
user_verification = "discouraged";
};
authentication_backend = {
password_reset.disable = true;
file = {
path = pkgs.writeText "user-db" (builtins.toJSON {
users.patrick = {
disabled = false;
displayname = "Patrick";
password = "$argon2id$v=19$m=4096,t=3,p=1$Ym5yc3VhZHJub2I$ihbPHC697Nybk1H7WHCMKi+2KkvNhdwvScaorkkj5yM";
email = "patrick@${config.secrets.secrets.global.domains.mail_public}";
groups = ["admin" "forgejo_admin"];
};
users.test = {
disabled = false;
displayname = "Test";
password = "$argon2id$v=19$m=4096,t=3,p=1$cmJuaWJldGRheA$kG4NCJRryXTCe/8Jc2/BBnEmlWSRwq4pZG7LH7fKs/o";
email = "test@${config.secrets.secrets.global.domains.mail_public}";
groups = [];
};
});
};
};
password_policy.standard = {
enabled = true;
min_length = 32;
};
notifier.filesystem.filename = "${stateDir}/notifications.txt";
storage.local.path = "${stateDir}/db.sqlite3";
identity_providers.oidc.clients = [
{
id = "forgejo";
secret = "$argon2id$v=19$m=4096,t=3,p=1$Ym5yc3VhZHJub2I$0gZRilVu8O1rmVxX+ZTMFFHqya6YN8l+8QXFIorhtKM";
redirect_uris = ["https://git.${config.secrets.secrets.global.domains.web}/user/oauth2/authelia/callback"];
public = false;
scopes = ["openid" "email" "profile" "groups"];
}
];
};
};
}

View file

@ -1,10 +1,11 @@
{
config,
nodes,
pkgs,
lib,
...
}: let
giteaDomain = "git.${config.secrets.secrets.global.domains.web}";
forgejoDomain = "git.${config.secrets.secrets.global.domains.web}";
in {
age.secrets.resticpasswd = {
generator.script = "alnum";
@ -28,7 +29,7 @@ in {
inherit (config.secrets.secrets.global.hetzner.users.forgejo) subUid path;
sshAgeSecret = "forgejoHetznerSsh";
};
paths = [config.services.gitea.stateDir];
paths = [config.services.forgejo.stateDir];
pruneOpts = [
"--keep-daily 10"
"--keep-weekly 7"
@ -44,29 +45,27 @@ in {
environment.persistence."/panzer".directories = [
{
directory = config.services.gitea.stateDir;
user = "gitea";
group = "gitea";
directory = config.services.forgejo.stateDir;
user = "forgejo";
group = "forgejo";
mode = "0700";
}
];
age.secrets.gitea-mailer-passwd = {
rekeyFile = config.node.secretsDir + "/gitea-passwd.age";
owner = "gitea";
group = "gitea";
age.secrets.forgejo-mailer-passwd = {
rekeyFile = config.node.secretsDir + "/forgejo-passwd.age";
owner = "forgejo";
group = "forgejo";
mode = "0700";
};
services.gitea = {
services.forgejo = {
enable = true;
package = pkgs.forgejo;
appName = "Patricks tolles git"; # tungsten inert gas?
stateDir = "/var/lib/forgejo";
# TODO db backups
# dump.enable = true;
lfs.enable = true;
mailerPasswordFile = config.age.secrets.gitea-mailer-passwd.path;
mailerPasswordFile = config.age.secrets.forgejo-mailer-passwd.path;
settings = {
DEFAULT.APP_NAME = "Patricks tolles git"; # tungsten inert gas?
actions = {
ENABLED = true;
DEFAULT_ACTIONS_URL = "github";
@ -78,17 +77,17 @@ in {
# federation.ENABLED = true;
mailer = {
ENABLED = true;
SMTP_ADDR = config.secrets.secrets.local.gitea.mail.host;
FROM = config.secrets.secrets.local.gitea.mail.from;
USER = config.secrets.secrets.local.gitea.mail.user;
SMTP_ADDR = config.secrets.secrets.local.forgejo.mail.host;
FROM = config.secrets.secrets.local.forgejo.mail.from;
USER = config.secrets.secrets.local.forgejo.mail.user;
SEND_AS_PLAIN_TEXT = true;
};
oauth2_client = {
ACCOUNT_LINKING = "login";
ENABLE_AUTO_REGISTRATION = true;
ENABLE_AUTO_REGISTRATION = false;
REGISTER_EMAIL_CONFIRM = false;
UPDATE_AVATAR = true;
USERNAME = "email";
USERNAME = "nickname";
};
# packages.ENABLED = true;
repository = {
@ -99,8 +98,8 @@ in {
server = {
HTTP_ADDR = "0.0.0.0";
HTTP_PORT = 3000;
DOMAIN = giteaDomain;
ROOT_URL = "https://${giteaDomain}/";
DOMAIN = forgejoDomain;
ROOT_URL = "https://${forgejoDomain}/";
LANDING_PAGE = "login";
SSH_PORT = 9922;
# TODO
@ -108,9 +107,9 @@ in {
# port forwarding in elisabeth
};
service = {
DISABLE_REGISTRATION = true;
DISABLE_REGISTRATION = false;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = true;
SHOW_REGISTRATION_BUTTON = false;
REGISTER_EMAIL_CONFIRM = false;
ENABLE_NOTIFY_MAIL = true;
DEFAULT_KEEP_EMAIL_PRIVATE = true;
@ -126,13 +125,11 @@ in {
# XXX: PKCE is currently not supported by gitea/forgejo,
# see https://github.com/go-gitea/gitea/issues/21376.
# Disable PKCE manually in kanidm for now.
# `kanidm system oauth2 warning-insecure-client-disable-pkce forgejo`
systemd.services.gitea = {
systemd.services.forgejo = {
serviceConfig.RestartSec = "600"; # Retry every 10 minutes
preStart = let
exe = lib.getExe config.services.gitea.package;
providerName = "authelia";
exe = lib.getExe config.services.forgejo.package;
providerName = "kanidm";
clientId = "forgejo";
args = lib.escapeShellArgs [
"--name"
@ -143,18 +140,14 @@ in {
clientId
"--auto-discover-url"
"https://auth.${config.secrets.secrets.global.domains.web}/oauth2/openid/${clientId}/.well-known/openid-configuration"
"--required-claim-name"
"groups"
"--scopes"
"email"
"--scopes"
"profile"
"--scopes"
"groups"
"--group-claim-name"
"groups"
"--admin-group"
"forgejo_admin"
"admin"
"--skip-local-2fa"
];
in
@ -170,8 +163,8 @@ in {
};
age.secrets.openid-secret = {
generator.script = "alnum";
inherit (nodes.elisabeth-kanidm.config.age.secrets.oauth2-forgejo) rekeyFile;
mode = "440";
inherit (config.services.gitea) group;
inherit (config.services.forgejo) group;
};
}

View file

@ -1,6 +1,8 @@
{config, ...}: let
kanidmdomain = "auth.${config.secrets.secrets.global.domains.web}";
in {
imports = [../kanidm.nix];
disabledModules = ["services/security/kanidm.nix"];
networking.firewall.allowedTCPPorts = [3000];
environment.persistence."/persist".directories = [
{
@ -21,6 +23,11 @@ in {
group = "kanidm";
mode = "440";
};
oauth2-forgejo = {
generator.script = "alnum";
mode = "440";
group = "kanidm";
};
};
services.kanidm = {
enableServer = true;
@ -38,5 +45,39 @@ in {
verify_ca = true;
verify_hostnames = true;
};
provision = {
enable = true;
persons = {
"patrick" = {
displayName = "Patrick";
mailAddresses = ["patrick@${config.secrets.secrets.global.domains.mail}"];
groups = ["forgejo.admins"];
};
"test" = {
displayName = "test";
mailAddresses = ["test@${config.secrets.secrets.global.domains.mail}"];
groups = ["forgejo.access"];
};
};
groups."forgejo.access" = {
members = ["forgejo.admins"];
};
groups."forgejo.admins" = {};
systems.oauth2.forgejo = {
displayName = "Forgejo";
originUrl = "https://git.${config.secrets.secrets.global.domains.web}/";
basicSecretFile = config.age.secrets.oauth2-forgejo.path;
scopeMaps."forgejo.access" = ["openid" "email" "profile"];
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup."forgejo.admins" = ["admin"];
};
};
};
};
systemd.services.kanidm.serviceConfig.RestartSec = "60"; # Retry every minute
}

View file

@ -168,6 +168,11 @@
hasBunker = true;
hasPaperless = true;
} {})
(mkShare {
name = "printer";
user = "printer";
group = "printer";
} {})
(mkShare {
name = "family-data";
user = "family";
@ -211,7 +216,12 @@
${group} = {
};
}))
++ [{family.members = ["patrick" "david" "helen" "ggr"];}]);
++ [
{
family.members = ["patrick" "david" "helen" "ggr"];
printer.members = ["patrick" "david" "helen" "ggr"];
}
]);
};
fileSystems = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v:

View file

@ -16,5 +16,23 @@
wrapProgram $out/bin/nvim --add-flags "--clean"
'';
});
kanidm = super.kanidm.overrideAttrs (old: let
provisionSrc = super.fetchFromGitHub {
owner = "oddlama";
repo = "kanidm-provision";
rev = "aa7a1c8ec04622745b385bd3b0462e1878f56b51";
hash = "sha256-NRolS3l2kARjkhWP7FYUG//KCEiueh48ZrADdCDb9Zg=";
};
in {
patches =
old.patches
++ [
"${provisionSrc}/patches/${old.version}-oauth2-basic-secret-modify.patch"
"${provisionSrc}/patches/${old.version}-recover-account.patch"
];
passthru.enableSecretProvisioning = true;
doCheck = false;
});
kanidm-provision = super.callPackage ./kanidm-provision.nix {};
})
]

26
pkgs/kanidm-provision.nix Normal file
View file

@ -0,0 +1,26 @@
{
lib,
rustPlatform,
fetchFromGitHub,
}:
rustPlatform.buildRustPackage rec {
pname = "kanidm-provision";
version = "1.0.0";
src = fetchFromGitHub {
owner = "oddlama";
repo = "kanidm-provision";
rev = "v${version}";
hash = "sha256-T6kiBUdOMHCWRUF/vepoPrvaULDQrUGYsd/3I11HCLY=";
};
cargoHash = "sha256-nHp3C6szJxOogH/kETIqcQQNhFqBCO0P66j7n3UHuwo=";
meta = with lib; {
description = "A small utility to help with kanidm provisioning";
homepage = "https://github.com/oddlama/kanidm-provision";
license = with licenses; [asl20 mit];
maintainers = with maintainers; [oddlama];
mainProgram = "kanidm-provision";
};
}

Binary file not shown.

Binary file not shown.

View file

@ -23,6 +23,11 @@
remotePath = "family-data";
automatic = true;
}
{
inherit address credentials;
remotePath = "printer";
automatic = true;
}
{
inherit address credentials;
remotePath = "media";

View file

@ -59,11 +59,12 @@
"1" = {
"0" = {
icon = config.images.images."back.png";
switch_page = 0;
switch_page = 1;
background_color = config.lib.stylix.colors.withHashtag.base0C;
};
};
};
page = 0; # The startup page
brightness = 99; # brighness value between 0 and 99
display_timeout = 0; # dimmer timeout in seconds
brightness_dimmed = 99; # dimmed brighness