Compare commits

...

11 commits

Author SHA1 Message Date
b12f dec2d76d2a
tests: move back to old keycloak module
Some checks failed
Flake checks / Check (pull_request) Failing after 1m42s
2024-08-27 13:37:28 +02:00
b12f 6efc884353
hosts: remove nachtigall-test 2024-08-27 13:32:00 +02:00
b12f 38a34f4345
Merge branch 'main' into feat/automated-account-deletion 2024-08-27 13:31:52 +02:00
b12f 3bc699fccf
chore: run nix fmt 2024-08-27 13:17:30 +02:00
teutat3s fb8ee1278a
Merge pull request 'feat/tests' (#224) from feat/tests into main
Reviewed-on: #224
Reviewed-by: teutat3s <teutat3s@noreply.git.pub.solar>
2024-08-27 10:45:56 +00:00
teutat3s 66ed87e666
ci: avoid garbage collection of checks
All checks were successful
Flake checks / Check (pull_request) Successful in 17m36s
2024-08-27 12:37:37 +02:00
teutat3s 88b76beb5c
keycloak: use backups module
All checks were successful
Flake checks / Check (pull_request) Successful in 19m4s
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:09:07 +02:00
teutat3s e857c6198b
modules/backup: init
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:04:10 +02:00
teutat3s 998cf4c63d
website: force HTTPS
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:03:43 +02:00
teutat3s a0b52d51e5
nachtigall: make postgres wait for zfs mount
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 10:00:42 +02:00
teutat3s 701c62dd69
tests: create keycloak test, add working test for website
Co-authored-by: b12f <b12f@noreply.git.pub.solar>
Co-authored-by: Hendrik Sokolowski <hensoko@noreply.git.pub.solar>
2024-08-27 09:55:25 +02:00
14 changed files with 322 additions and 1031 deletions

View file

@ -18,14 +18,20 @@ jobs:
# Prevent cache garbage collection by creating GC roots
mkdir -p /var/lib/gitea-runner/tankstelle/.local/state/nix/results
for target in $(nix flake show --json --all-systems | jq '
for target in $(nix flake show --json --all-systems | jq --raw-output '
.["nixosConfigurations"] |
to_entries[] |
.key
' | tr -d '"'
.key'
); do
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$target" ".#nixosConfigurations.${target}.config.system.build.toplevel"
done
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' flake check
for check in $(nix flake show --json --all-systems | jq --raw-output '
.checks."x86_64-linux" |
to_entries[] |
.key'
); do
nix --print-build-logs --verbose --accept-flake-config --access-tokens '' \
build --out-link /var/lib/gitea-runner/tankstelle/.local/state/nix/results/"$check" ".#checks.x86_64-linux.${check}"
done

View file

@ -82,7 +82,7 @@
master = import inputs.master { inherit system; };
};
packages = import ./tests ({ inherit inputs self; } // args);
checks = import ./tests ({ inherit inputs self; } // args);
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
@ -111,7 +111,8 @@
devShells.ci = pkgs.mkShell { buildInputs = with pkgs; [ nodejs ]; };
};
flake = let
flake =
let
username = "barkeeper";
in
{

View file

@ -43,17 +43,6 @@
];
};
nachtigall-test = {
imports = [
self.inputs.agenix.nixosModules.default
self.nixosModules.home-manager
./nachtigall/test-vm.nix
self.nixosModules.overlays
self.nixosModules.core
self.nixosModules.docker
];
};
flora-6 = self.nixos-flake.lib.mkLinuxSystem {
imports = [
self.inputs.agenix.nixosModules.default

View file

@ -59,11 +59,16 @@
database-password-file = config.age.secrets.keycloak-database-password.path;
};
pub-solar-os.backups.stores.storagebox = {
pub-solar-os.backups.repos.storagebox = {
passwordFile = config.age.secrets."restic-repo-storagebox".path;
repository = "sftp:u377325@u377325.your-storagebox.de:/backups";
};
systemd.services.postgresql = {
after = [ "var-lib-postgresql.mount" ];
requisite = [ "var-lib-postgresql.mount" ];
};
# This value determines the NixOS release with which your system is to be
# compatible, in order to avoid breaking some software such as database
# servers. You should change this only after NixOS release notes say you

View file

@ -1,40 +1,39 @@
{ flake, lib, ... }:
{
imports =
[
./backups.nix
./apps/nginx.nix
imports = [
./backups.nix
./apps/nginx.nix
./apps/collabora.nix
./apps/coturn.nix
./apps/forgejo.nix
./apps/keycloak.nix
./apps/mailman.nix
./apps/mastodon.nix
./apps/mediawiki.nix
./apps/nextcloud.nix
./apps/nginx-mastodon.nix
./apps/nginx-mastodon-files.nix
./apps/nginx-prometheus-exporters.nix
./apps/nginx-website.nix
./apps/nginx-website-miom.nix
./apps/opensearch.nix
./apps/owncast.nix
./apps/postgresql.nix
./apps/prometheus-exporters.nix
./apps/promtail.nix
./apps/searx.nix
./apps/tmate.nix
./apps/collabora.nix
./apps/coturn.nix
./apps/forgejo.nix
./apps/keycloak.nix
./apps/mailman.nix
./apps/mastodon.nix
./apps/mediawiki.nix
./apps/nextcloud.nix
./apps/nginx-mastodon.nix
./apps/nginx-mastodon-files.nix
./apps/nginx-prometheus-exporters.nix
./apps/nginx-website.nix
./apps/nginx-website-miom.nix
./apps/opensearch.nix
./apps/owncast.nix
./apps/postgresql.nix
./apps/prometheus-exporters.nix
./apps/promtail.nix
./apps/searx.nix
./apps/tmate.nix
./apps/matrix/irc.nix
./apps/matrix/mautrix-telegram.nix
./apps/matrix/synapse.nix
./apps/nginx-matrix.nix
];
./apps/matrix/irc.nix
./apps/matrix/mautrix-telegram.nix
./apps/matrix/synapse.nix
./apps/nginx-matrix.nix
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
security.acme.defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
security.acme.preliminarySelfsigned = true;

View file

@ -7,7 +7,8 @@
{
# Configuration common to all Linux systems
flake = {
lib = let
lib =
let
callLibs = file: import file { inherit lib; };
in
rec {

View file

@ -4,260 +4,268 @@
lib,
pkgs,
...
}: let
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" { inherit lib; inherit config; inherit pkgs; };
}:
let
utils = import "${flake.inputs.nixpkgs}/nixos/lib/utils.nix" {
inherit lib;
inherit config;
inherit pkgs;
};
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
inherit (utils.systemdUtils.unitOptions) unitOption;
in {
inherit (lib)
literalExpression
mkOption
mkPackageOption
types
;
in
{
options.pub-solar-os.backups = {
stores =
with lib;
mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Read the repository password from a file.
'';
example = "/etc/nixos/restic-password";
};
repository = mkOption {
type = with types; nullOr str;
default = null;
description = ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
repos = mkOption {
description = ''
Configuration of Restic repositories.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Read the repository password from a file.
'';
example = "/etc/nixos/restic-password";
};
}
)
);
default = { };
example = {
remotebackup = {
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/restic-password";
};
};
};
backups =
with lib;
mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
paths = mkOption {
# This is nullable for legacy reasons only. We should consider making it a pure listOf
# after some time has passed since this comment was added.
type = types.nullOr (types.listOf types.str);
default = [ ];
description = ''
Which paths to backup, in addition to ones specified via
`dynamicFilesFrom`. If null or an empty array and
`dynamicFilesFrom` is also null, no backup command will be run.
This can be used to create a prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Patterns to exclude when backing up. See
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
details on syntax.
'';
example = [
"/var/cache"
"/home/*/.cache"
".git"
];
};
timerConfig = mkOption {
type = types.nullOr (types.attrsOf unitOption);
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''
When to run the backup. See {manpage}`systemd.timer(5)` for
details. If null no timer is created and the backup will only
run when explicitly started.
'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
user = mkOption {
type = types.str;
default = "root";
description = ''
As which user the backup should run.
'';
example = "postgresql";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments passed to restic backup.
'';
example = [ "--exclude-file=/etc/nixos/restic-ignore" ];
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra extended options to be passed to the restic --option flag.
'';
example = [ "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" ];
};
initialize = mkOption {
type = types.bool;
default = false;
description = ''
Create the repository if it doesn't exist.
'';
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options (--keep-\* et al.) for 'restic forget
--prune', to automatically prune old snapshots. The
'forget' command is run *after* the 'backup' command, so
keep that in mind when constructing the --keep-\* options.
'';
example = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
runCheck = mkOption {
type = types.bool;
default = (builtins.length config.pub-solar-os.backups.backups.${name}.checkOpts > 0);
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
description = "Whether to run the `check` command with the provided `checkOpts` options.";
example = true;
};
checkOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options for 'restic check'.
'';
example = [ "--with-cache" ];
};
dynamicFilesFrom = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that produces a list of files to back up. The
results of this command are given to the '--files-from'
option. The result is merged with paths specified via `paths`.
'';
example = "find /home/matt/git -type d -name .git";
};
backupPrepareCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run before starting the backup process.
'';
};
backupCleanupCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run after finishing the backup process.
'';
};
package = mkPackageOption pkgs "restic" { };
createWrapper = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to generate and add a script to the system path, that has the same environment variables set
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
having to manually specify most options.
'';
};
repository = mkOption {
type = with types; nullOr str;
default = null;
description = ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
}
)
);
default = { };
example = {
localbackup = {
paths = [ "/home" ];
exclude = [ "/home/*/.cache" ];
initialize = true;
};
remotebackup = {
paths = [ "/home" ];
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
}
)
);
default = { };
example = {
remotebackup = {
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/restic-password";
};
};
};
backups = mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
paths = mkOption {
# This is nullable for legacy reasons only. We should consider making it a pure listOf
# after some time has passed since this comment was added.
type = types.nullOr (types.listOf types.str);
default = [ ];
description = ''
Which paths to backup, in addition to ones specified via
`dynamicFilesFrom`. If null or an empty array and
`dynamicFilesFrom` is also null, no backup command will be run.
This can be used to create a prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Patterns to exclude when backing up. See
https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
details on syntax.
'';
example = [
"/var/cache"
"/home/*/.cache"
".git"
];
};
timerConfig = mkOption {
type = types.nullOr (types.attrsOf unitOption);
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''
When to run the backup. See {manpage}`systemd.timer(5)` for
details. If null no timer is created and the backup will only
run when explicitly started.
'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
user = mkOption {
type = types.str;
default = "root";
description = ''
As which user the backup should run.
'';
example = "postgresql";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments passed to restic backup.
'';
example = [ "--exclude-file=/etc/nixos/restic-ignore" ];
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra extended options to be passed to the restic --option flag.
'';
example = [ "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" ];
};
initialize = mkOption {
type = types.bool;
default = false;
description = ''
Create the repository if it doesn't exist.
'';
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options (--keep-\* et al.) for 'restic forget
--prune', to automatically prune old snapshots. The
'forget' command is run *after* the 'backup' command, so
keep that in mind when constructing the --keep-\* options.
'';
example = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
runCheck = mkOption {
type = types.bool;
default = (builtins.length config.pub-solar-os.backups.backups.${name}.checkOpts > 0);
defaultText = literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
description = "Whether to run the `check` command with the provided `checkOpts` options.";
example = true;
};
checkOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options for 'restic check'.
'';
example = [ "--with-cache" ];
};
dynamicFilesFrom = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that produces a list of files to back up. The
results of this command are given to the '--files-from'
option. The result is merged with paths specified via `paths`.
'';
example = "find /home/matt/git -type d -name .git";
};
backupPrepareCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run before starting the backup process.
'';
};
backupCleanupCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run after finishing the backup process.
'';
};
package = mkPackageOption pkgs "restic" { };
createWrapper = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to generate and add a script to the system path, that has the same environment variables set
as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
having to manually specify most options.
'';
};
};
}
)
);
default = { };
example = {
localbackup = {
paths = [ "/home" ];
exclude = [ "/home/*/.cache" ];
initialize = true;
};
remotebackup = {
paths = [ "/home" ];
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
};
};
};
};
config = {
services.restic.backups =
let
stores = config.pub-solar-os.backups.stores;
repos = config.pub-solar-os.backups.repos;
backups = config.pub-solar-os.backups.backups;
storeNames = builtins.attrNames stores;
storeNames = builtins.attrNames repos;
backupNames = builtins.attrNames backups;
createBackups =
backupName:
map (storeName: {
name = "${backupName}-${storeName}";
value = stores."${storeName}" // backups."${backupName}";
value = repos."${storeName}" // backups."${backupName}";
}) storeNames;
in

View file

@ -6,9 +6,6 @@
...
}:
{
disabledModules = [ "services/web-apps/keycloak.nix" ];
imports = [ ./keycloak.nix ];
options.pub-solar-os.auth = with lib; {
enable = mkEnableOption "Enable keycloak to run on the node";
@ -59,9 +56,7 @@
"pub.solar" =
flake.inputs.keycloak-theme-pub-solar.legacyPackages.${pkgs.system}.keycloak-theme-pub-solar;
};
plugins = [
flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener
];
plugins = [ flake.inputs.keycloak-event-listener.packages.${pkgs.system}.keycloak-event-listener ];
};
pub-solar-os.backups.backups.keycloak = {

View file

@ -1,705 +0,0 @@
{ config, options, pkgs, lib, ... }:
let
cfg = config.services.keycloak;
opt = options.services.keycloak;
inherit (lib)
types
mkMerge
mkOption
mkChangedOptionModule
mkRenamedOptionModule
mkRemovedOptionModule
mkPackageOption
concatStringsSep
mapAttrsToList
escapeShellArg
mkIf
optionalString
optionals
mkDefault
literalExpression
isAttrs
literalMD
maintainers
catAttrs
collect
hasPrefix
;
inherit (builtins)
elem
typeOf
isInt
isString
hashString
isPath
;
prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
in
{
imports =
[
(mkRenamedOptionModule
[ "services" "keycloak" "bindAddress" ]
[ "services" "keycloak" "settings" "http-host" ])
(mkRenamedOptionModule
[ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
[ "services" "keycloak" "settings" "hostname-strict-backchannel"])
(mkChangedOptionModule
[ "services" "keycloak" "httpPort" ]
[ "services" "keycloak" "settings" "http-port" ]
(config:
builtins.fromJSON config.services.keycloak.httpPort))
(mkChangedOptionModule
[ "services" "keycloak" "httpsPort" ]
[ "services" "keycloak" "settings" "https-port" ]
(config:
builtins.fromJSON config.services.keycloak.httpsPort))
(mkRemovedOptionModule
[ "services" "keycloak" "frontendUrl" ]
''
Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
See its description for more information.
'')
(mkRemovedOptionModule
[ "services" "keycloak" "extraConfig" ]
"Use `services.keycloak.settings' instead.")
];
options.services.keycloak =
let
inherit (types)
bool
str
int
nullOr
attrsOf
oneOf
path
enum
package
port;
assertStringPath = optionName: value:
if isPath value then
throw ''
services.keycloak.${optionName}:
${toString value}
is a Nix path, but should be a string, since Nix
paths are copied into the world-readable Nix store.
''
else value;
in
{
enable = mkOption {
type = bool;
default = false;
example = true;
description = ''
Whether to enable the Keycloak identity and access management
server.
'';
};
sslCertificate = mkOption {
type = nullOr path;
default = null;
example = "/run/keys/ssl_cert";
apply = assertStringPath "sslCertificate";
description = ''
The path to a PEM formatted certificate to use for TLS/SSL
connections.
'';
};
sslCertificateKey = mkOption {
type = nullOr path;
default = null;
example = "/run/keys/ssl_key";
apply = assertStringPath "sslCertificateKey";
description = ''
The path to a PEM formatted private key to use for TLS/SSL
connections.
'';
};
plugins = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
Keycloak plugin jar, ear files or derivations containing
them. Packaged plugins are available through
`pkgs.keycloak.plugins`.
'';
};
database = {
type = mkOption {
type = enum [ "mysql" "mariadb" "postgresql" ];
default = "postgresql";
example = "mariadb";
description = ''
The type of database Keycloak should connect to.
'';
};
host = mkOption {
type = str;
default = "localhost";
description = ''
Hostname of the database to connect to.
'';
};
port =
let
dbPorts = {
postgresql = 5432;
mariadb = 3306;
mysql = 3306;
};
in
mkOption {
type = port;
default = dbPorts.${cfg.database.type};
defaultText = literalMD "default port of selected database";
description = ''
Port of the database to connect to.
'';
};
useSSL = mkOption {
type = bool;
default = cfg.database.host != "localhost";
defaultText = literalExpression ''config.${opt.database.host} != "localhost"'';
description = ''
Whether the database connection should be secured by SSL /
TLS.
'';
};
caCert = mkOption {
type = nullOr path;
default = null;
description = ''
The SSL / TLS CA certificate that verifies the identity of the
database server.
Required when PostgreSQL is used and SSL is turned on.
For MySQL, if left at `null`, the default
Java keystore is used, which should suffice if the server
certificate is issued by an official CA.
'';
};
createLocally = mkOption {
type = bool;
default = true;
description = ''
Whether a database should be automatically created on the
local host. Set this to false if you plan on provisioning a
local database yourself. This has no effect if
services.keycloak.database.host is customized.
'';
};
name = mkOption {
type = str;
default = "keycloak";
description = ''
Database name to use when connecting to an external or
manually provisioned database; has no effect when a local
database is automatically provisioned.
To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
`false` and create the database and user
manually.
'';
};
username = mkOption {
type = str;
default = "keycloak";
description = ''
Username to use when connecting to an external or manually
provisioned database; has no effect when a local database is
automatically provisioned.
To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to
`false` and create the database and user
manually.
'';
};
passwordFile = mkOption {
type = path;
example = "/run/keys/db_password";
apply = assertStringPath "passwordFile";
description = ''
The path to a file containing the database password.
'';
};
};
package = mkPackageOption pkgs "keycloak" { };
initialAdminPassword = mkOption {
type = str;
default = "changeme";
description = ''
Initial password set for the `admin`
user. The password is not stored safely and should be changed
immediately in the admin panel.
'';
};
themes = mkOption {
type = attrsOf package;
default = { };
description = ''
Additional theme packages for Keycloak. Each theme is linked into
subdirectory with a corresponding attribute name.
Theme packages consist of several subdirectories which provide
different theme types: for example, `account`,
`login` etc. After adding a theme to this option you
can select it by its name in Keycloak administration console.
'';
};
extraStartupFlags = lib.mkOption {
type = lib.types.listOf str;
default = [ ];
description = ''
Extra flags to be added to the startup command kc.sh.
This can be used to import a realm during startup or to
set configuration variables, see <https://www.keycloak.org/server/configuration>.
--verbose and --optimized are always added.
'';
};
settings = mkOption {
type = lib.types.submodule {
freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
options = {
http-host = mkOption {
type = str;
default = "0.0.0.0";
example = "127.0.0.1";
description = ''
On which address Keycloak should accept new connections.
'';
};
http-port = mkOption {
type = port;
default = 80;
example = 8080;
description = ''
On which port Keycloak should listen for new HTTP connections.
'';
};
https-port = mkOption {
type = port;
default = 443;
example = 8443;
description = ''
On which port Keycloak should listen for new HTTPS connections.
'';
};
http-relative-path = mkOption {
type = str;
default = "/";
example = "/auth";
apply = x: if !(hasPrefix "/") x then "/" + x else x;
description = ''
The path relative to `/` for serving
resources.
::: {.note}
In versions of Keycloak using Wildfly (&lt;17),
this defaulted to `/auth`. If
upgrading from the Wildfly version of Keycloak,
i.e. a NixOS version before 22.05, you'll likely
want to set this to `/auth` to
keep compatibility with your clients.
See <https://www.keycloak.org/migration/migrating-to-quarkus>
for more information on migrating from Wildfly to Quarkus.
:::
'';
};
hostname = mkOption {
type = nullOr str;
example = "keycloak.example.com";
description = ''
The hostname part of the public URL used as base for
all frontend requests.
See <https://www.keycloak.org/server/hostname>
for more information about hostname configuration.
'';
};
hostname-backchannel-dynamic = mkOption {
type = bool;
default = false;
example = true;
description = ''
Enables dynamic resolving of backchannel URLs,
including hostname, scheme, port and context path.
See <https://www.keycloak.org/server/hostname>
for more information about hostname configuration.
'';
};
proxy = mkOption {
type = enum [ "edge" "reencrypt" "passthrough" "none" ];
default = "none";
example = "edge";
description = ''
The proxy address forwarding mode if the server is
behind a reverse proxy.
- `edge`:
Enables communication through HTTP between the
proxy and Keycloak.
- `reencrypt`:
Requires communication through HTTPS between the
proxy and Keycloak.
- `passthrough`:
Enables communication through HTTP or HTTPS between
the proxy and Keycloak.
See <https://www.keycloak.org/server/reverseproxy> for more information.
'';
};
};
};
example = literalExpression ''
{
hostname = "keycloak.example.com";
proxy = "reencrypt";
https-key-store-file = "/path/to/file";
https-key-store-password = { _secret = "/run/keys/store_password"; };
}
'';
description = ''
Configuration options corresponding to parameters set in
{file}`conf/keycloak.conf`.
Most available options are documented at <https://www.keycloak.org/server/all-config>.
Options containing secret data should be set to an attribute
set containing the attribute `_secret` - a
string pointing to a file containing the value the option
should be set to. See the example to get a better picture of
this: in the resulting
{file}`conf/keycloak.conf` file, the
`https-key-store-password` key will be set
to the contents of the
{file}`/run/keys/store_password` file.
'';
};
};
config =
let
# We only want to create a database if we're actually going to
# connect to it.
databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
'';
# Both theme and theme type directories need to be actual
# directories in one hierarchy to pass Keycloak checks.
themesBundle = pkgs.runCommand "keycloak-themes" { } ''
linkTheme() {
theme="$1"
name="$2"
mkdir "$out/$name"
for typeDir in "$theme"/*; do
if [ -d "$typeDir" ]; then
type="$(basename "$typeDir")"
mkdir "$out/$name/$type"
for file in "$typeDir"/*; do
ln -sn "$file" "$out/$name/$type/$(basename "$file")"
done
fi
done
}
mkdir -p "$out"
for theme in ${keycloakBuild}/themes/*; do
if [ -d "$theme" ]; then
linkTheme "$theme" "$(basename "$theme")"
fi
done
${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
'';
keycloakConfig = lib.generators.toKeyValue {
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
mkValueString = v:
if isInt v then toString v
else if isString v then v
else if true == v then "true"
else if false == v then "false"
else if isSecret v then hashString "sha256" v._secret
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
};
};
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
keycloakBuild = cfg.package.override {
inherit confFile;
plugins = cfg.package.enabledPlugins ++ cfg.plugins ++
(with cfg.package.plugins; [quarkus-systemd-notify quarkus-systemd-notify-deployment]);
};
in
mkIf cfg.enable
{
assertions = [
{
assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
}
{
assertion = createLocalPostgreSQL -> config.services.postgresql.settings.standard_conforming_strings or true;
message = "Setting up a local PostgreSQL db for Keycloak requires `standard_conforming_strings` turned on to work reliably";
}
{
assertion = cfg.settings.hostname != null || ! cfg.settings.hostname-strict or true;
message = "Setting the Keycloak hostname is required, see `services.keycloak.settings.hostname`";
}
{
assertion = cfg.settings.hostname-url or null == null;
message = ''
The option `services.keycloak.settings.hostname-url' has been removed.
Set `services.keycloak.settings.hostname' instead.
See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details.
'';
}
{
assertion = cfg.settings.hostname-strict-backchannel or null == null;
message = ''
The option `services.keycloak.settings.hostname-strict-backchannel' has been removed.
Set `services.keycloak.settings.hostname-backchannel-dynamic' instead.
See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details.
'';
}
];
environment.systemPackages = [ keycloakBuild ];
services.keycloak.settings =
let
postgresParams = concatStringsSep "&" (
optionals cfg.database.useSSL [
"ssl=true"
] ++ optionals (cfg.database.caCert != null) [
"sslrootcert=${cfg.database.caCert}"
"sslmode=verify-ca"
]
);
mariadbParams = concatStringsSep "&" ([
"characterEncoding=UTF-8"
] ++ optionals cfg.database.useSSL [
"useSSL=true"
"requireSSL=true"
"verifyServerCertificate=true"
] ++ optionals (cfg.database.caCert != null) [
"trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
"trustCertificateKeyStorePassword=notsosecretpassword"
]);
dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
in
mkMerge [
{
db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
db-password._secret = cfg.database.passwordFile;
db-url-host = cfg.database.host;
db-url-port = toString cfg.database.port;
db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
db-url-properties = prefixUnlessEmpty "?" dbProps;
db-url = null;
}
(mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
https-certificate-file = "/run/keycloak/ssl/ssl_cert";
https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
})
];
systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
after = [ "postgresql.service" ];
before = [ "keycloak.service" ];
bindsTo = [ "postgresql.service" ];
path = [ config.services.postgresql.package ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "postgres";
Group = "postgres";
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
};
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
create_role="$(mktemp)"
trap 'rm -f "$create_role"' EXIT
# Read the password from the credentials directory and
# escape any single quotes by adding additional single
# quotes after them, following the rules laid out here:
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
db_password="''${db_password//\'/\'\'}"
echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
'';
};
systemd.services.keycloakMySQLInit = mkIf createLocalMySQL {
after = [ "mysql.service" ];
before = [ "keycloak.service" ];
bindsTo = [ "mysql.service" ];
path = [ config.services.mysql.package ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = config.services.mysql.user;
Group = config.services.mysql.group;
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
};
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
# Read the password from the credentials directory and
# escape any single quotes by adding additional single
# quotes after them, following the rules laid out here:
# https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
db_password="''${db_password//\'/\'\'}"
( echo "SET sql_mode = 'NO_BACKSLASH_ESCAPES';"
echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
) | mysql -N
'';
};
systemd.services.keycloak =
let
databaseServices =
if createLocalPostgreSQL then [
"keycloakPostgreSQLInit.service"
"postgresql.service"
]
else if createLocalMySQL then [
"keycloakMySQLInit.service"
"mysql.service"
]
else [ ];
secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
mkSecretReplacement = file: ''
replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
'';
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
extraStartupFlags = lib.concatStringsSep " " cfg.extraStartupFlags;
in
{
after = databaseServices;
bindsTo = databaseServices;
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
keycloakBuild
openssl
replace-secret
];
environment = {
KC_HOME_DIR = "/run/keycloak";
KC_CONF_DIR = "/run/keycloak/conf";
};
serviceConfig = {
LoadCredential =
map (p: "${baseNameOf p}:${p}") secretPaths
++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
"ssl_cert:${cfg.sslCertificate}"
"ssl_key:${cfg.sslCertificateKey}"
];
User = "keycloak";
Group = "keycloak";
DynamicUser = true;
RuntimeDirectory = "keycloak";
RuntimeDirectoryMode = "0700";
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
Type = "notify"; # Requires quarkus-systemd-notify plugin
NotifyAccess = "all";
};
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
umask u=rwx,g=,o=
ln -s ${themesBundle} /run/keycloak/themes
ln -s ${keycloakBuild}/providers /run/keycloak/
install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
${secretReplacements}
# Escape any backslashes in the db parameters, since
# they're otherwise unexpectedly read as escape
# sequences.
sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf
'' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
mkdir -p /run/keycloak/ssl
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
'' + ''
export KEYCLOAK_ADMIN=admin
export KEYCLOAK_ADMIN_PASSWORD=${escapeShellArg cfg.initialAdminPassword}
kc.sh --verbose start --optimized ${extraStartupFlags}
'';
};
services.postgresql.enable = mkDefault createLocalPostgreSQL;
services.mysql.enable = mkDefault createLocalMySQL;
services.mysql.package =
let
dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
in
mkIf createLocalMySQL (mkDefault dbPkg);
};
meta.doc = ./keycloak.md;
meta.maintainers = [ maintainers.talyz ];
}

View file

@ -5,15 +5,22 @@ args@{
pkgs,
inputs,
...
}: let
}:
let
nixos-lib = import (inputs.nixpkgs + "/nixos/lib") { };
loadTestFiles = with lib; dir: mapAttrs' (name: _: let
test = ((import (dir + "/${name}")) args);
in {
name = "test-" + (lib.strings.removeSuffix ".nix" name);
value = nixos-lib.runTest test;
})
(filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix")
(builtins.readDir dir));
in loadTestFiles ./.
loadTestFiles =
with lib;
dir:
mapAttrs' (
name: _:
let
test = ((import (dir + "/${name}")) args);
in
{
name = "test-" + (lib.strings.removeSuffix ".nix" name);
value = nixos-lib.runTest test;
}
) (filterAttrs (name: _: (hasSuffix ".nix" name) && name != "default.nix") (builtins.readDir dir));
in
loadTestFiles ./.

View file

@ -72,17 +72,7 @@ in
};
testScript =
{ nodes, ... }:
let
user = nodes.client.users.users.${nodes.client.pub-solar-os.authentication.username};
#uid = toString user.uid;
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u ${user.name})/bus";
gdbus = "${bus} gdbus";
su = command: "su - ${user.name} -c '${command}'";
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
wmClass = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
in
''
{ ... }: ''
def puppeteer_run(cmd):
client.succeed(f'puppeteer-run \'{cmd}\' ')

View file

@ -3,10 +3,12 @@
lib,
config,
...
}: let
puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) {});
puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) {});
in {
}:
let
puppeteer-socket = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-socket.nix) { });
puppeteer-run = (pkgs.callPackage (import ./puppeteer-socket/puppeteer-run.nix) { });
in
{
imports = [ ./global.nix ];
security.polkit.enable = true;
@ -18,9 +20,7 @@ in {
services.getty.autologinUser = config.pub-solar-os.authentication.username;
virtualisation.qemu.options = [
"-vga std"
];
virtualisation.qemu.options = [ "-vga std" ];
home-manager.users.${config.pub-solar-os.authentication.username} = {
programs.bash.profileExtra = ''
@ -34,9 +34,9 @@ in {
'';
config = {
modifier = "Mod4";
terminal = "${pkgs.alacritty}/bin/alacritty";
terminal = "${pkgs.alacritty}/bin/alacritty";
startup = [
{command = "EXECUTABLE=${pkgs.firefox}/bin/firefox ${puppeteer-socket}/bin/puppeteer-socket";}
{ command = "EXECUTABLE=${pkgs.firefox}/bin/firefox ${puppeteer-socket}/bin/puppeteer-socket"; }
];
};
};

View file

@ -1,8 +1,6 @@
{
writeShellScriptBin,
curl
}: writeShellScriptBin "puppeteer-run" ''
set -e
{ writeShellScriptBin, curl }:
writeShellScriptBin "puppeteer-run" ''
set -e
exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket
exec ${curl}/bin/curl --fail-with-body -X POST -d "$@" --unix-socket "/tmp/puppeteer.sock" http://puppeteer-socket
''

View file

@ -1,7 +1,4 @@
{
buildNpmPackage,
nodejs,
}:
{ buildNpmPackage, nodejs }:
buildNpmPackage rec {
src = ./.;
name = "puppeteer-socket";