Compare commits

..

26 Commits

Author SHA1 Message Date
Vegard Bieker Matthey be87d98060 prometheus for dibbler 2026-06-08 10:54:03 +02:00
h7x4 b4582a160f skrot/dibbler: rotate database password 2026-06-07 17:58:33 +09:00
h7x4 ac094d350d base/timesyncd: specify ntp servers 2026-06-07 17:52:54 +09:00
h7x4 b848e0f1cc temmie/userweb: add log processor for apache 2026-06-07 06:03:18 +09:00
h7x4 c671329b93 temmie/userweb: inject users from passwd into httpd sandbox 2026-06-07 05:28:24 +09:00
Vegard Bieker Matthey e6a3d43493 modules/drumknotty: use correct screen window name for dibbler 2026-06-05 22:14:02 +02:00
h7x4 cafc95db8f bicep/mjolnir: use nodejs v22 2026-06-06 04:43:58 +09:00
h7x4 2d6b09cb32 bikkje: label ports in firewall port list 2026-06-06 04:08:16 +09:00
h7x4 ce0af2f6e4 flake.nix: add app for building gitea workflows locally 2026-06-06 04:05:26 +09:00
h7x4 88892115b5 base: enable autoScrub for all btrfs machine by default 2026-06-06 04:05:26 +09:00
h7x4 8a290d30e7 modules/drumknotty: split into several parts
This also fixes a few issues, such as enabling `createLocalDatabase` for
multiple programs, and wraps all the screen logic within a screenrc
file. Some assertions were also added to avoid some easy-to-make
mistakes.
2026-06-05 14:21:35 +02:00
Vegard Bieker Matthey 3197c6a5e3 attach with dibbler window selected 2026-06-05 14:21:35 +02:00
Vegard Bieker Matthey f8dcaddefb use main branch for worblehat after merge 2026-06-05 14:21:28 +02:00
Vegard Bieker Matthey 009d89f959 set default settings for worblehat and dibbler 2026-06-05 14:09:06 +02:00
Vegard Bieker Matthey 21bba3ec7e add worblehat daemons 2026-06-05 14:09:06 +02:00
Vegard Bieker Matthey 9552351776 add database password for worblehat 2026-06-05 14:09:06 +02:00
Vegard Bieker Matthey 7e754ade71 drumknotty: init 2026-06-05 14:08:58 +02:00
h7x4 fcd81aed00 packages/ooye: 3.5.1 -> 3.6.0 2026-06-04 19:17:29 +09:00
h7x4 966081ebfc bicep/mysql: enable userstat 2026-06-03 15:31:27 +09:00
h7x4 39d313579c bicep/mysql: rotate slow query logs 2026-06-03 15:21:18 +09:00
h7x4 3386153b8b ildkule/prometheus/exim: make scheme explicit 2026-06-03 13:35:13 +09:00
h7x4 56906241f6 bekkalokk/roundcube: temporary fix for webmail redirects 2026-06-01 03:52:09 +09:00
h7x4 3fe71d21f6 bekkalokk/roundcube: webdir moved to public_html within package 2026-06-01 02:57:43 +09:00
h7x4 074d240595 base: tag generation as auto if built by auto upgrade service 2026-06-01 01:00:50 +09:00
h7x4 1ce3372683 lupine/binfmt: enable 2026-06-01 01:00:50 +09:00
Adrian G L 5f14c15679 feat: add radicale to bekkalokk 2026-06-01 00:59:54 +09:00
30 changed files with 1481 additions and 136 deletions
+8
View File
@@ -1,4 +1,5 @@
{ {
config,
pkgs, pkgs,
lib, lib,
inputs, inputs,
@@ -39,6 +40,7 @@
./services/scrutiny-collector.nix ./services/scrutiny-collector.nix
./services/smartd.nix ./services/smartd.nix
./services/thermald.nix ./services/thermald.nix
./services/timesyncd.nix
./services/uptimed.nix ./services/uptimed.nix
./services/userborn.nix ./services/userborn.nix
./services/userdbd.nix ./services/userdbd.nix
@@ -46,12 +48,18 @@
system.nixos.tags = lib.optionals (inputs.self.sourceInfo ? dirtyRev) [ "dirty" ]; system.nixos.tags = lib.optionals (inputs.self.sourceInfo ? dirtyRev) [ "dirty" ];
specialisation."auto-upgrade".configuration = {
system.nixos.tags = [ "auto" ];
};
boot.tmp.cleanOnBoot = lib.mkDefault true; boot.tmp.cleanOnBoot = lib.mkDefault true;
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest; boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
boot.loader.systemd-boot.enable = lib.mkDefault true; boot.loader.systemd-boot.enable = lib.mkDefault true;
boot.loader.efi.canTouchEfiVariables = lib.mkDefault true; boot.loader.efi.canTouchEfiVariables = lib.mkDefault true;
services.btrfs.autoScrub.enable = lib.mkDefault (lib.any ({ fsType, ... }: fsType == "btrfs") (lib.attrValues config.fileSystems));
time.timeZone = "Europe/Oslo"; time.timeZone = "Europe/Oslo";
i18n.defaultLocale = "en_US.UTF-8"; i18n.defaultLocale = "en_US.UTF-8";
+1
View File
@@ -13,6 +13,7 @@ in
"--refresh" "--refresh"
"--no-write-lock-file" "--no-write-lock-file"
"--specialisation auto-upgrade"
# --update-input is deprecated since nix 2.22, and removed in lix 2.90 # --update-input is deprecated since nix 2.22, and removed in lix 2.90
# as such we instead use --override-input combined with --refresh # as such we instead use --override-input combined with --refresh
# https://git.lix.systems/lix-project/lix/issues/400 # https://git.lix.systems/lix-project/lix/issues/400
+12
View File
@@ -0,0 +1,12 @@
{ ... }:
{
services.timesyncd = {
servers = [ "ntp.ntnu.no" ];
fallbackServers = [
"0.pool.ntp.org"
"1.pool.ntp.org"
"0.no.pool.ntp.org"
];
};
}
Generated
+49 -5
View File
@@ -161,6 +161,27 @@
"url": "https://git.pvv.ntnu.no/Grzegorz/grzegorz-clients.git" "url": "https://git.pvv.ntnu.no/Grzegorz/grzegorz-clients.git"
} }
}, },
"libdib": {
"inputs": {
"nixpkgs": [
"worblehat",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769338528,
"narHash": "sha256-t18ZoSt9kaI1yde26ok5s7aFLkap1Q9+/2icVh2zuaE=",
"ref": "refs/heads/main",
"rev": "7218348163fd8d84df4a6f682c634793e67a3fed",
"revCount": 13,
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
},
"original": {
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
}
},
"matrix-next": { "matrix-next": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -316,11 +337,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780062186, "lastModified": 1780764154,
"narHash": "sha256-FSkwKO/56i9RddwSydK804fSnIvbczBnFJgr2/m+F9U=", "narHash": "sha256-Xvf9aBNLYDnbDKdtFjp5GEA/rZwVczHZWbJ0hac8Vv4=",
"ref": "main", "ref": "main",
"rev": "db2b19f144af046161b7f9ca69ddaf3f06fcceea", "rev": "8b4541be73ee3bd6c60525b2f42605efe89398c9",
"revCount": 13, "revCount": 14,
"type": "git", "type": "git",
"url": "https://git.pvv.ntnu.no/Projects/passwd2systemd-users.git" "url": "https://git.pvv.ntnu.no/Projects/passwd2systemd-users.git"
}, },
@@ -413,7 +434,8 @@
"pvv-nettsiden": "pvv-nettsiden", "pvv-nettsiden": "pvv-nettsiden",
"qotd": "qotd", "qotd": "qotd",
"roowho2": "roowho2", "roowho2": "roowho2",
"sops-nix": "sops-nix" "sops-nix": "sops-nix",
"worblehat": "worblehat"
} }
}, },
"roowho2": { "roowho2": {
@@ -544,6 +566,28 @@
"repo": "sops-nix", "repo": "sops-nix",
"type": "github" "type": "github"
} }
},
"worblehat": {
"inputs": {
"libdib": "libdib",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773932847,
"narHash": "sha256-IklIAdlonrmO8/lkDxNIVz9+ORL4pcVotMTxeyvxzoc=",
"ref": "main",
"rev": "0871a319f51d3cb0d1abb5b11edb768b39906d3f",
"revCount": 104,
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/worblehat.git"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/worblehat.git"
}
} }
}, },
"root": "root", "root": "root",
+28 -2
View File
@@ -23,6 +23,9 @@
dibbler.url = "git+https://git.pvv.ntnu.no/Projects/dibbler.git?ref=main"; dibbler.url = "git+https://git.pvv.ntnu.no/Projects/dibbler.git?ref=main";
dibbler.inputs.nixpkgs.follows = "nixpkgs"; dibbler.inputs.nixpkgs.follows = "nixpkgs";
worblehat.url = "git+https://git.pvv.ntnu.no/Projects/worblehat.git?ref=main";
worblehat.inputs.nixpkgs.follows = "nixpkgs";
matrix-next.url = "github:dali99/nixos-matrix-modules/v0.8.0"; matrix-next.url = "github:dali99/nixos-matrix-modules/v0.8.0";
matrix-next.inputs.nixpkgs.follows = "nixpkgs"; matrix-next.inputs.nixpkgs.follows = "nixpkgs";
@@ -94,6 +97,18 @@
]; ];
}); });
apps = forAllSystems (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
gitea-workflows = {
type = "app";
meta.description = "Run all gitea workflows locally";
program = toString (pkgs.writeShellScript "pvv-nixos-config-run-gitea-worflows" ''
${lib.getExe pkgs.gitea-actions-runner} exec -i node:current-trixie
'');
};
});
nixosConfigurations = let nixosConfigurations = let
nixosConfig = nixpkgs: name: configurationPath: extraArgs @ { nixosConfig = nixpkgs: name: configurationPath: extraArgs @ {
localSystem ? "x86_64-linux", # buildPlatform localSystem ? "x86_64-linux", # buildPlatform
@@ -189,6 +204,12 @@
(final: prev: { (final: prev: {
inherit (self.packages.${prev.stdenv.hostPlatform.system}) out-of-your-element; inherit (self.packages.${prev.stdenv.hostPlatform.system}) out-of-your-element;
}) })
(final: prev: {
# See https://git.pvv.ntnu.no/Drift/issues/issues/369
mjolnir = prev.mjolnir.override {
nodejs = prev.nodejs_22;
};
})
]; ];
}; };
bekkalokk = stableNixosConfig "bekkalokk" { bekkalokk = stableNixosConfig "bekkalokk" {
@@ -212,10 +233,14 @@
}; };
skrot = stableNixosConfig "skrot" { skrot = stableNixosConfig "skrot" {
modules = [ modules = [
self.nixosModules.drumknotty
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.dibbler.nixosModules.default
]; ];
overlays = [inputs.dibbler.overlays.default]; overlays =
[
inputs.dibbler.overlays.default
inputs.worblehat.overlays.default
];
}; };
shark = stableNixosConfig "shark" {}; shark = stableNixosConfig "shark" {};
wenche = stableNixosConfig "wenche" {}; wenche = stableNixosConfig "wenche" {};
@@ -287,6 +312,7 @@
rsync-pull-targets = ./modules/rsync-pull-targets.nix; rsync-pull-targets = ./modules/rsync-pull-targets.nix;
snakeoil-certs = ./modules/snakeoil-certs.nix; snakeoil-certs = ./modules/snakeoil-certs.nix;
snappymail = ./modules/snappymail.nix; snappymail = ./modules/snappymail.nix;
drumknotty = ./modules/drumknotty;
}; };
devShells = forAllSystems (system: { devShells = forAllSystems (system: {
+1 -2
View File
@@ -7,6 +7,7 @@
./services/alps.nix ./services/alps.nix
./services/bluemap.nix ./services/bluemap.nix
./services/radicale.nix
./services/idp-simplesamlphp ./services/idp-simplesamlphp
./services/kerberos.nix ./services/kerberos.nix
./services/mediawiki ./services/mediawiki
@@ -24,8 +25,6 @@
address = with values.hosts.bekkalokk; [ (ipv4 + "/25") (ipv6 + "/64") ]; address = with values.hosts.bekkalokk; [ (ipv4 + "/25") (ipv6 + "/64") ];
}; };
services.btrfs.autoScrub.enable = true;
# Don't change (even during upgrades) unless you know what you are doing. # Don't change (even during upgrades) unless you know what you are doing.
# See https://search.nixos.org/options?show=system.stateVersion # See https://search.nixos.org/options?show=system.stateVersion
system.stateVersion = "25.11"; system.stateVersion = "25.11";
+40
View File
@@ -0,0 +1,40 @@
{ config, lib, ... }:
let
domain = "dav.pvv.ntnu.no";
radicalePort = 5232;
in {
services.radicale = {
enable = true;
settings = {
server = {
hosts = [ "127.0.0.1:${toString radicalePort}" ];
};
auth = {
type = "imap";
imap_host = "imap.pvv.ntnu.no";
imap_security = "tls";
};
storage = {
filesystem_folder = "/var/lib/radicale/collections";
};
};
};
services.nginx.virtualHosts."${domain}" = {
forceSSL = true;
enableACME = true;
kTLS = true;
extraConfig = ''
client_max_body_size 128M;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:${toString radicalePort}";
proxyWebsockets = true;
};
};
}
+2 -1
View File
@@ -10,8 +10,9 @@
enableACME = true; enableACME = true;
kTLS = true; kTLS = true;
locations = { locations = {
"= /".return = "302 https://webmail.pvv.ntnu.no/roundcube"; # "= /".return = "302 https://webmail.pvv.ntnu.no/roundcube";
"/roundcube".return = "302 https://webmail.pvv.ntnu.no/";
"/afterlogic_lite".return = "302 https://webmail.pvv.ntnu.no/roundcube"; "/afterlogic_lite".return = "302 https://webmail.pvv.ntnu.no/roundcube";
"/squirrelmail".return = "302 https://webmail.pvv.ntnu.no/roundcube"; "/squirrelmail".return = "302 https://webmail.pvv.ntnu.no/roundcube";
"/rainloop".return = "302 https://snappymail.pvv.ntnu.no/"; "/rainloop".return = "302 https://snappymail.pvv.ntnu.no/";
+2 -37
View File
@@ -29,7 +29,7 @@ in
dicts = with pkgs.aspellDicts; [ en en-computers nb nn fr de it ]; dicts = with pkgs.aspellDicts; [ en en-computers nb nn fr de it ];
maxAttachmentSize = 20; maxAttachmentSize = 20;
hostName = "roundcubeplaceholder.example.com"; hostName = domain;
database = { database = {
host = "postgres.pvv.ntnu.no"; host = "postgres.pvv.ntnu.no";
@@ -49,44 +49,9 @@ in
''; '';
}; };
services.nginx.virtualHosts."roundcubeplaceholder.example.com" = lib.mkForce { }; # TODO: move this back to `webmail.pvv.ntnu.no/roundcube` subpath
services.nginx.virtualHosts.${domain} = { services.nginx.virtualHosts.${domain} = {
kTLS = true; kTLS = true;
locations."/roundcube" = {
tryFiles = "$uri $uri/ =404";
index = "index.php";
root = pkgs.linkFarm "roundcube-dir" {
roundcube = "${cfg.package}";
};
extraConfig = ''
location ~ ^/roundcube/(${builtins.concatStringsSep "|" [
# https://wiki.archlinux.org/title/Roundcube
"README"
"INSTALL"
"LICENSE"
"CHANGELOG"
"UPGRADING"
"bin"
"SQL"
".+\\.md"
"\\."
"config"
"temp"
"logs"
]})/? {
deny all;
}
location ~ ^/roundcube/(.+\.php)(/?.*)$ {
fastcgi_split_path_info ^/roundcube(/.+\.php)(/.+)$;
include ${config.services.nginx.package}/conf/fastcgi_params;
include ${config.services.nginx.package}/conf/fastcgi.conf;
fastcgi_index index.php;
fastcgi_pass unix:${config.services.phpfpm.pools.roundcube.socket};
}
'';
};
}; };
} }
+15
View File
@@ -23,6 +23,9 @@ in
bind-address = values.services.mysql.ipv4; bind-address = values.services.mysql.ipv4;
skip-networking = 0; skip-networking = 0;
# Useful for the mysqld prometheus exporter
userstat = 1;
# This was needed in order to be able to use all of the old users # This was needed in order to be able to use all of the old users
# during migration from knakelibrak to bicep in Sep. 2023 # during migration from knakelibrak to bicep in Sep. 2023
secure_auth = 0; secure_auth = 0;
@@ -71,4 +74,16 @@ in
]; ];
}; };
}; };
services.logrotate = lib.mkIf (cfg.settings.mysqld.slow-query-log == 1) {
enable = true;
settings.mysql-slowlog = {
files = [ cfg.settings.mysqld.slow-query-log-file ];
frequency = "weekly";
rotate = 12;
create = "0660 mysql mysql";
minsize = "1M";
compress = true;
};
};
} }
+23 -1
View File
@@ -29,7 +29,29 @@
firewall = { firewall = {
enable = true; enable = true;
# Allow SSH and HTTP and ports for email and irc # Allow SSH and HTTP and ports for email and irc
allowedTCPPorts = [ 80 22 194 994 6665 6666 6667 6668 6669 6697 995 993 25 465 587 110 143 993 995 ]; allowedTCPPorts = [
22 # SSH
80 # HTTP
# IRC
194 # IRC
994 # IRC (TLS)
6697 # IRC (SSL)
6665
6666
6667
6668
6669
# EMAIL
25 # STMP
465 # STMP (SSL)
587 # STMP (TLS/STARTTLS)
110 # POP3
995 # POP3 (SSL/TLS)
143 # IMAP
993 # IMAP (SSL/TLS)
];
allowedUDPPorts = [ 80 22 194 994 6665 6666 6667 6668 6669 6697 995 993 25 465 587 110 143 993 995 ]; allowedUDPPorts = [ 80 22 194 994 6665 6666 6667 6668 6669 6697 995 993 25 465 587 110 143 993 995 ];
}; };
# Use systemd-resolved inside the container # Use systemd-resolved inside the container
@@ -8,6 +8,7 @@ in {
./matrix-synapse.nix ./matrix-synapse.nix
./mysqld.nix ./mysqld.nix
./postgres.nix ./postgres.nix
./dibbler.nix
]; ];
services.prometheus = { services.prometheus = {
@@ -0,0 +1,96 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.prometheus.exporters.sql;
configFile =
if cfg.configFile != null then
cfg.configFile
else
let
nameInline = lib.mapAttrsToList (k: v: v // { name = k; });
renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ];
configuration = {
jobs = map renameStartupSql (
nameInline (lib.mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs)
);
};
in
builtins.toFile "config.yaml" (builtins.toJSON configuration);
in
{
sops.secrets."config/postgresql_dibbler_password" = { };
services.prometheus.scrapeConfigs = [
{
job_name = "sql_exporter";
scrape_interval = "1m";
scheme = "http";
static_configs = [
{
targets = [ "localhost:9237" ];
}
];
}
];
services.prometheus.exporters.sql = {
enable = true;
configuration = {
jobs.dibbler = {
interval = "1m";
queries."daily_purchase_sum" = {
help = "Sum of purchases for the current day.";
labels = [ "thing" ];
values = [ "sum" ];
query = "SELECT SUM(price) FROM purchases GROUP BY DATE(time) ORDER BY DATE(time) DESC LIMIT 1";
};
queries."total_purchase_sum" = {
help = "Sum of all purchases.";
labels = [ "thing" ];
values = [ "sum" ];
query = "SELECT SUM(price) FROM purchases";
};
queries."total_stock_value" = {
help = "The value of all stock in dibbler.";
labels = [ "thing" ];
values = [ "sum" ];
query = "SELECT SUM(price * stock) FROM products";
};
queries."user_credit_sum" = {
help = "The sum of all user credit.";
labels = [ "thing" ];
values = [ "sum" ];
query = "SELECT SUM(credit) FROM users";
};
};
};
};
systemd.services."prometheus-sql-exporter".serviceConfig = {
RuntimeDirectory = "prometheus-sql-exporter";
LoadCredential = "postgresql_dibbler_password:${
config.sops.secrets."config/postgresql_dibbler_password".path
}";
ExecStartPre = ''
|${lib.getExe pkgs.jq} \
--null-input \
--compact-output \
--slurpfile config '${configFile}' \
--rawfile pw '%d/postgresql_dibbler_password' \
--from-file '${pkgs.writeText "prometheus-sql-exec-start-jq-filter" ''
("postgres://pvv_vv:\($pw | gsub("\n"; ""))@postgres.pvv.ntnu.no") as $pg_uri
| $config[0]
| .jobs[0].connections[0] = $pg_uri
''}' > /run/prometheus-sql-exporter/config.yaml
'';
};
}
@@ -1,14 +1,12 @@
{ ... }: { ... }:
{ {
services.prometheus = { services.prometheus.scrapeConfigs = [{
scrapeConfigs = [
{
job_name = "exim"; job_name = "exim";
scrape_interval = "15s"; scrape_interval = "15s";
scheme = "http";
static_configs = [{ static_configs = [{
targets = [ "microbel.pvv.ntnu.no:9636" ]; targets = [ "microbel.pvv.ntnu.no:9636" ];
}]; }];
} }];
];
};
} }
-2
View File
@@ -15,8 +15,6 @@
address = with values.hosts.kommode; [ (ipv4 + "/25") (ipv6 + "/64") ]; address = with values.hosts.kommode; [ (ipv4 + "/25") (ipv6 + "/64") ];
}; };
services.btrfs.autoScrub.enable = true;
services.qemuGuest.enable = true; services.qemuGuest.enable = true;
# Don't change (even during upgrades) unless you know what you are doing. # Don't change (even during upgrades) unless you know what you are doing.
+6
View File
@@ -9,6 +9,12 @@
sops.defaultSopsFile = fp /secrets/lupine/lupine.yaml; sops.defaultSopsFile = fp /secrets/lupine/lupine.yaml;
boot.binfmt.emulatedSystems = [
"aarch64-linux"
"armv7l-linux"
"i686-linux"
];
systemd.network.networks."30-enp0s31f6" = values.defaultNetworkConfig // { systemd.network.networks."30-enp0s31f6" = values.defaultNetworkConfig // {
matchConfig.Name = "enp0s31f6"; matchConfig.Name = "enp0s31f6";
address = with values.hosts.${lupineName}; [ (ipv4 + "/25") (ipv6 + "/64") ]; address = with values.hosts.${lupineName}; [ (ipv4 + "/25") (ipv6 + "/64") ];
+31 -5
View File
@@ -28,17 +28,26 @@
sops.secrets = { sops.secrets = {
"dibbler/postgresql/password" = { "dibbler/postgresql/password" = {
owner = "dibbler"; owner = "drumknotty";
group = "dibbler"; group = "drumknotty";
};
"worblehat/postgresql/password" = {
owner = "drumknotty";
group = "drumknotty";
}; };
}; };
services.dibbler = { services.drumknotty = {
enable = true; enable = true;
kioskMode = true; kioskMode = true;
limitScreenWidth = 80;
limitScreenHeight = 42;
screen = {
limitWidth = 80;
limitHeight = 42;
};
dibbler = {
enable = true;
settings = { settings = {
general.quit_allowed = false; general.quit_allowed = false;
database = { database = {
@@ -53,6 +62,23 @@
}; };
}; };
worblehat = {
enable = true;
settings = {
general.quit_allowed = false;
database = {
type = "postgresql";
postgresql = {
username = "worblehat";
dbname = "worblehat";
host = "postgres.pvv.ntnu.no";
password = config.sops.secrets."worblehat/postgresql/password".path;
};
};
};
};
};
systemd.services."serial-getty@ttyUSB0" = lib.mkIf (!config.virtualisation.isVmVariant) { systemd.services."serial-getty@ttyUSB0" = lib.mkIf (!config.virtualisation.isVmVariant) {
enable = true; enable = true;
wantedBy = [ "getty.target" ]; # to start at boot wantedBy = [ "getty.target" ]; # to start at boot
@@ -0,0 +1 @@
target
@@ -0,0 +1,171 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "apache-log-processor"
version = "0.1.0"
dependencies = [
"nix",
"time",
]
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
@@ -0,0 +1,19 @@
[package]
name = "apache-log-processor"
version = "0.1.0"
edition = "2024"
autobins = false
license = "MIT"
authors = [
"projects@pvv.ntnu.no",
]
[dependencies]
nix = { version = "0.31.3", features = ["event", "fs", "user"] }
time = { version = "0.3.47", features = ["formatting", "local-offset"] }
[[bin]]
name = "apache-log-processor"
bench = false
path = "src/main.rs"
@@ -0,0 +1,33 @@
{
lib
, rustPlatform
, stdenv
}:
let
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
cargoLock = ./Cargo.lock;
mainProgram = (lib.head cargoToml.bin).name;
pname = cargoToml.package.name;
in
rustPlatform.buildRustPackage {
inherit pname;
inherit (cargoToml.package) version;
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./Cargo.toml
./Cargo.lock
./src
];
};
cargoLock.lockFile = cargoLock;
doCheck = true;
meta = with lib; {
license = licenses.mit;
platforms = platforms.linux;
inherit mainProgram;
};
}
@@ -0,0 +1,321 @@
use nix::{
errno::Errno,
fcntl::{FcntlArg, OFlag, fcntl, open},
sys::{
epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout},
stat::Mode,
},
unistd::{User, getegid, geteuid, read, setegid, seteuid, write},
};
use std::{
collections::VecDeque,
os::fd::{AsFd, BorrowedFd, OwnedFd},
path::PathBuf,
process::exit,
};
use time::{OffsetDateTime, format_description};
const READ_BUFFER_SIZE: usize = 8 * 1024;
#[derive(Debug, Clone, Copy)]
enum LogMode {
Access,
Error,
}
fn main() -> Result<(), String> {
let log_mode = match std::env::args().nth(1).as_deref() {
Some("access") => LogMode::Access,
Some("error") => LogMode::Error,
Some(other) => {
return Err(format!(
"invalid log mode `{other}`; expected `access` or `error`"
));
}
None => return Err("missing log mode argument; expected `access` or `error`".to_string()),
};
let tee_file = match log_mode {
LogMode::Access => None,
LogMode::Error => Some(
open(
&PathBuf::from("/var/log/httpd/error.log"),
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT | OFlag::O_CLOEXEC,
Mode::S_IRUSR | Mode::S_IWUSR,
)
.map_err(|error| format!("failed to open error log for teeing: {error}"))?,
),
};
let stdin = std::io::stdin();
fcntl(stdin.as_fd(), FcntlArg::F_GETFL)
.map(OFlag::from_bits_retain)
.map(|flags| FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK))
.and_then(|flags| fcntl(stdin.as_fd(), flags))
.map_err(|error| format!("failed to make stdin nonblocking: {error}"))?;
let epoll = Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC)
.map_err(|error| format!("failed to create epoll instance: {error}"))?;
epoll
.add(
stdin.as_fd(),
EpollEvent::new(
EpollFlags::EPOLLIN | EpollFlags::EPOLLERR | EpollFlags::EPOLLHUP,
0,
),
)
.map_err(|error| format!("failed to register stdin with epoll: {error}"))?;
if let Err(error) = event_loop(log_mode, epoll, stdin.as_fd(), tee_file) {
eprintln!("Error: {error}");
exit(1);
}
Ok(())
}
fn event_loop(
log_mode: LogMode,
epoll: Epoll,
stdin_fd: BorrowedFd<'_>,
mut tee_file: Option<OwnedFd>,
) -> Result<(), String> {
let mut events = [EpollEvent::empty(); 1];
let mut pending = VecDeque::new();
loop {
let ready = loop {
match epoll.wait(&mut events, EpollTimeout::NONE) {
Ok(ready) => break ready,
Err(Errno::EINTR) => continue,
Err(error) => {
return Err(format!("epoll wait failed: {error}"));
}
}
};
if ready == 0 {
continue;
}
let mut scratch = [0u8; READ_BUFFER_SIZE];
let eof = loop {
match read(stdin_fd, &mut scratch) {
Ok(0) => break true,
Ok(read_bytes) => pending.extend(scratch[..read_bytes].iter().copied()),
Err(Errno::EINTR) => continue,
Err(Errno::EAGAIN) => break false,
Err(error) => {
return Err(format!("failed to read from stdin: {error}"));
}
}
};
while let Some(newline_index) = pending.iter().position(|byte| *byte == b'\n') {
let line = pending.make_contiguous();
process_line(log_mode, &line[..=newline_index], &mut tee_file)?;
pending.drain(..=newline_index);
}
if eof {
if !pending.is_empty() {
process_line(log_mode, pending.make_contiguous(), &mut tee_file)?;
pending.clear();
}
return Ok(());
}
}
}
fn process_line(
log_mode: LogMode,
line: &[u8],
tee_file: &mut Option<OwnedFd>,
) -> Result<(), String> {
if let Some(tee_file) = tee_file.as_ref() {
write_all_fd(tee_file, line).map_err(|error| {
format!("failed to append to APACHE_LOG_PROCESSOR_TEE_FILE: {error}")
})?;
}
if let Some(user) =
parse_username_from_line(line).and_then(|name| User::from_name(name).ok().flatten())
{
let identity = EffectiveIdentity::switch_to(&user).map_err(|error| {
format!(
"failed to switch effective identity to {} (uid {}, gid {}): {error}",
user.name, user.uid, user.gid
)
})?;
let result: Result<(), String> = (|| {
let dir = user.dir.join("nobackup/weblogs");
if !dir.is_dir() {
return Err(format!(
"logs directory {} does not exist for user {}",
dir.display(),
user.name
));
}
let now = OffsetDateTime::now_local()
.unwrap_or_else(|_| OffsetDateTime::now_utc())
.format(&format_description::parse("[year]-[month]-[day]").unwrap())
.map_err(|error| {
format!("failed to format current date for log file name: {error}")
})?;
let logfile = dir.join(match log_mode {
LogMode::Access => format!("access-{now}.log"),
LogMode::Error => format!("error-{now}.log"),
});
let fd = open(
&logfile,
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT | OFlag::O_CLOEXEC,
Mode::S_IRUSR
| Mode::S_IWUSR
| Mode::S_IRGRP
| Mode::S_IROTH
| Mode::S_IWGRP
| Mode::S_IWOTH,
)
.map_err(|error| format!("failed to open log file for user {}: {error}", user.name))?;
write_all_fd(fd.as_fd(), line).map_err(|error| {
format!(
"failed to append to log file for user {}: {error}",
user.name
)
})?;
Ok(())
})();
if let Err(error) = result {
eprintln!("Error processing log line for user {}: {error}", user.name);
}
identity.restore().map_err(|error| {
format!(
"failed to restore original effective identity after handling {}: {error}",
user.name
)
})?;
}
Ok(())
}
fn parse_username_from_line(line: &[u8]) -> Option<&str> {
line.splitn(8, |&b| b == b' ')
.nth(6)
.and_then(|path| {
path.strip_prefix(b"/~")
.and_then(|rest| rest.split(|&b| b == b'/').next())
})
.or_else(|| {
line.windows(b"/home/pvv/".len())
.enumerate()
.find_map(|(start, window)| {
(window == b"/home/pvv/")
.then_some(start + b"/home/pvv/".len())
.and_then(|start| line.get(start..))
.filter(|rest| rest.get(1) == Some(&b'/'))
.and_then(|rest| rest.get(2..))
.and_then(|rest| rest.split(|&b| b == b'/').next())
})
})
.filter(|segment| !segment.is_empty())
.and_then(|segment| std::str::from_utf8(segment).ok())
}
fn write_all_fd<Fd: AsFd>(fd: Fd, mut buffer: &[u8]) -> nix::Result<()> {
while !buffer.is_empty() {
match write(fd.as_fd(), buffer) {
Ok(0) => return Err(Errno::EIO),
Ok(written) => buffer = &buffer[written..],
Err(Errno::EINTR) => continue,
Err(error) => return Err(error),
}
}
Ok(())
}
struct EffectiveIdentity {
saved_euid: nix::unistd::Uid,
saved_egid: nix::unistd::Gid,
restored: bool,
}
impl EffectiveIdentity {
fn switch_to(user: &User) -> nix::Result<Self> {
let guard = Self {
saved_euid: geteuid(),
saved_egid: getegid(),
restored: false,
};
setegid(user.gid)?;
if let Err(error) = seteuid(user.uid) {
let _ = setegid(guard.saved_egid);
return Err(error);
}
Ok(guard)
}
fn restore(mut self) -> nix::Result<()> {
let restore_uid = seteuid(self.saved_euid);
let restore_gid = setegid(self.saved_egid);
self.restored = true;
restore_uid?;
restore_gid?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_user_from_access_log() {
let inputs = [(
"1.2.3.4 - - [25/May/2026:10:07:24 +0200] \"GET /~oysteikt/ HTTP/2.0\" 200 3708",
"oysteikt",
)];
for (line, expected_user) in inputs {
let parsed_user = parse_username_from_line(line.as_bytes());
assert_eq!(
parsed_user,
Some(expected_user),
"Failed to parse user from line: {line}"
);
}
}
#[test]
fn test_parse_user_from_error_log() {
let inputs = [(
"[Sat May 09 20:45:21.480016 2026] [authz_core:error] [pid 3555:tid 3617] [remote 1::2:42000] AH01630: client denied by server configuration: /home/pvv/d/oysteikt/web-docs/.git",
"oysteikt",
)];
for (line, expected_user) in inputs {
let parsed_user = parse_username_from_line(line.as_bytes());
assert_eq!(
parsed_user,
Some(expected_user),
"Failed to parse user from line: {line}"
);
}
}
}
+54 -36
View File
@@ -2,6 +2,9 @@
let let
cfg = config.services.httpd; cfg = config.services.httpd;
# NOTE Enable this if you want to strace stuff in the sandbox...
debug = false;
homeLetters = [ "a" "b" "c" "d" "h" "i" "j" "k" "l" "m" "z" ]; homeLetters = [ "a" "b" "c" "d" "h" "i" "j" "k" "l" "m" "z" ];
phpOptions = lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}"){ phpOptions = lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}"){
@@ -11,6 +14,8 @@ let
upload_max_filesize = "40M"; upload_max_filesize = "40M";
}); });
apache-log-processor = pkgs.callPackage ./apache-log-processor { };
# https://nixos.org/manual/nixpkgs/stable/#ssec-php-user-guide-installing-with-extensions # https://nixos.org/manual/nixpkgs/stable/#ssec-php-user-guide-installing-with-extensions
phpEnv = pkgs.php.buildEnv { phpEnv = pkgs.php.buildEnv {
extensions = { all, ... }: with all; [ extensions = { all, ... }: with all; [
@@ -130,9 +135,6 @@ let
file file
findutils findutils
gawk gawk
glibc.getent
strace
systemd
gnugrep gnugrep
gnumake gnumake
gnupg gnupg
@@ -144,6 +146,10 @@ let
wget wget
which which
xdg-utils xdg-utils
] ++ lib.optionals debug [
glibc.getent
strace
systemd
]; ];
extraOutputsToInstall = [ extraOutputsToInstall = [
@@ -201,10 +207,11 @@ in
} }
]; ];
logPerVirtualHost = false;
extraConfig = '' extraConfig = ''
TraceEnable on TraceEnable on
LogLevel warn rewrite:trace3 LogLevel warn rewrite:trace3
ScriptLog ${cfg.logDir}/cgi.log
''; '';
virtualHosts."temmie.pvv.ntnu.no" = { virtualHosts."temmie.pvv.ntnu.no" = {
@@ -216,6 +223,11 @@ in
]; ];
extraConfig = '' extraConfig = ''
CustomLog "${cfg.logDir}/access.log" combined
CustomLog "|${lib.getExe apache-log-processor} access" combined
ErrorLog "|${lib.getExe apache-log-processor} error"
ScriptLog "${cfg.logDir}/cgi.log"
UserDir ${lib.concatMapStringsSep " " (l: "/home/pvv/${l}/*/web-docs") homeLetters} UserDir ${lib.concatMapStringsSep " " (l: "/home/pvv/${l}/*/web-docs") homeLetters}
UserDir disabled root UserDir disabled root
AddHandler cgi-script .cgi AddHandler cgi-script .cgi
@@ -298,30 +310,33 @@ in
done done
''; '';
})}" })}"
# "${pkgs.systemd}/bin/resolvectl query smtp.pvv.ntnu.no"
"${pkgs.strace}/bin/strace ${pkgs.glibc.getent}/bin/getent ahosts smtp.pvv.ntnu.no" "${rsyncCommand} pvv@smtp.pvv.ntnu.no:/etc/passwd /run/httpd/pamunix-in/"
"${rsyncCommand} pvv@smtp.pvv.ntnu.no:/etc/passwd /run/httpd/pamunix-sync/" "${rsyncCommand} pvv@smtp.pvv.ntnu.no:/etc/group /run/httpd/pamunix-in/"
"${rsyncCommand} pvv@smtp.pvv.ntnu.no:/etc/group /run/httpd/pamunix-sync/"
# "+|echo 'wwwrun:x:54:54:Apache httpd user:/var/empty:/run/current-system/sw/bin/nologin' >> /run/httpd/pamunix-sync/passwd"
# "+|echo 'root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash' >> /run/httpd/pamunix-sync/passwd"
# "+|echo 'wwwrun:x:54:' >> /run/httpd/pamunix-sync/group"
# "${rsyncCommand} pvv@smtp.pvv.ntnu.no:/etc/shadow /run/httpd/pamunix-sync/"
(let (let
args = lib.cli.toCommandLineShellGNU { } { args = lib.cli.toCommandLineShellGNU { } {
passwd-file = "/run/httpd/pamunix-sync/passwd"; passwd-file = "/run/httpd/pamunix-in/passwd";
group-file = "/run/httpd/pamunix-sync/group"; group-file = "/run/httpd/pamunix-in/group";
output-dir = "/run/httpd/systemd-userdb"; output-dir = "/run/httpd/pamunix-out";
shadow-file = pkgs.emptyFile; shadow-file = pkgs.emptyFile;
email-domain = "pvv.ntnu.no";
output-passwd = true;
ignore-user-file = toString ./ignore_user_file.txt; ignore-user-file = toString ./ignore_user_file.txt;
ignore-group-file = toString ./ignore_group_file.txt; ignore-group-file = toString ./ignore_group_file.txt;
set-default-umask = "0077";
set-default-mount-no-devices = "true";
set-default-mount-no-suid = "true";
set-default-mount-no-execute = "false";
}; };
in ''${lib.getExe pkgs.passwd2systemd-users} ${args}'') in ''${lib.getExe pkgs.passwd2systemd-users} ${args}'')
"${lib.getExe' pkgs.coreutils "shred"} -u /run/httpd/pamunix-sync/passwd /run/httpd/pamunix-sync/group" "${lib.getExe' pkgs.coreutils "shred"} -u /run/httpd/pamunix-in/passwd /run/httpd/pamunix-in/group"
":${lib.getExe pkgs.gnused} -i '$ a\\\\root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash' /run/httpd/pamunix-out/passwd"
":${lib.getExe pkgs.gnused} -i '$ a\\\\wwwrun:x:54:54:Apache httpd user:/var/empty:/run/current-system/sw/bin/bash' /run/httpd/pamunix-out/passwd"
":${lib.getExe pkgs.gnused} -i '$ a\\\\root:x:0:' /run/httpd/pamunix-out/group"
":${lib.getExe pkgs.gnused} -i '$ a\\\\wwwrun:x:54:' /run/httpd/pamunix-out/group"
"${lib.getExe' pkgs.coreutils "cat"} /run/httpd/pamunix-out/passwd"
"+${lib.getExe' pkgs.coreutils "chown"} root:root /run/httpd/pamunix-out/passwd /run/httpd/pamunix-out/group"
"+${lib.getExe' pkgs.coreutils "chmod"} 0644 /run/httpd/pamunix-out/passwd /run/httpd/pamunix-out/group"
"+${lib.getExe pkgs.mount} --bind /run/httpd/pamunix-out/passwd /etc/passwd"
"+${lib.getExe pkgs.mount} --bind /run/httpd/pamunix-out/group /etc/group"
]; ];
ExecStart = lib.mkForce "${cfg.package}/bin/httpd -D FOREGROUND -f /etc/httpd/httpd.conf -k start"; ExecStart = lib.mkForce "${cfg.package}/bin/httpd -D FOREGROUND -f /etc/httpd/httpd.conf -k start";
ExecReload = lib.mkForce "${cfg.package}/bin/httpd -f /etc/httpd/httpd.conf -k graceful"; ExecReload = lib.mkForce "${cfg.package}/bin/httpd -f /etc/httpd/httpd.conf -k graceful";
@@ -337,9 +352,9 @@ in
LogsDirectory = [ "httpd" ]; LogsDirectory = [ "httpd" ];
LogsDirectoryMode = "0700"; LogsDirectoryMode = "0700";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_PTRACE" ]; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SETUID" "CAP_SETGID" ] ++ lib.optionals debug [ "CAP_SYS_PTRACE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_PTRACE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SETUID" "CAP_SETGID" ] ++ lib.optionals debug [ "CAP_SYS_PTRACE" ];
# LockPersonality = true; LockPersonality = !debug;
PrivateDevices = true; PrivateDevices = true;
PrivateTmp = true; PrivateTmp = true;
# NOTE: this removes CAP_NET_BIND_SERVICE... # NOTE: this removes CAP_NET_BIND_SERVICE...
@@ -366,15 +381,17 @@ in
"tcp:443" "tcp:443"
]; ];
SystemCallArchitectures = "native"; SystemCallArchitectures = "native";
# SystemCallFilter = [ SystemCallFilter = lib.mkIf (!debug) [
# "@system-service" "@system-service"
# ]; "@setuid"
];
UMask = "0077"; UMask = "0077";
RuntimeDirectoryMode = "0750";
RuntimeDirectory = [ RuntimeDirectory = [
"httpd/root-mnt" "httpd/root-mnt"
"httpd/pamunix-sync" "httpd/pamunix-in"
"httpd/systemd-userdb" "httpd/pamunix-out"
]; ];
RootDirectory = "/run/httpd/root-mnt"; RootDirectory = "/run/httpd/root-mnt";
MountAPIVFS = true; MountAPIVFS = true;
@@ -382,16 +399,16 @@ in
builtins.storeDir builtins.storeDir
"/etc" "/etc"
"/dev/null" "/dev/null"
# NCSD socket
# "/var/run"
# "/var/run/systemd/resolve"
"/etc/resolv.conf" "/etc/resolv.conf"
"/var/lib/acme" "/var/lib/acme"
"/run/httpd/systemd-userdb:/etc/userdb"
"-/run/httpd/pamunix-out/passwd:/etc/passwd"
"-/run/httpd/pamunix-out/group:/etc/group"
"${pkgs.writeText "userweb-fake-nsswitch.conf" '' "${pkgs.writeText "userweb-fake-nsswitch.conf" ''
passwd: systemd files passwd: files
group: systemd files group: files
shadow: systemd files shadow: files
sudoers: files sudoers: files
hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
@@ -419,6 +436,7 @@ in
"/store/gnu" "/store/gnu"
"/usr" "/usr"
"/usr/local" "/usr/local"
"/run/current-system/sw"
]; ];
child = [ child = [
"/bin" "/bin"
View File
+198
View File
@@ -0,0 +1,198 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.drumknotty;
in
{
imports = [
./dibbler.nix
./worblehat.nix
];
options.services.drumknotty = {
enable = lib.mkEnableOption "DrumknoTTY";
kioskMode = lib.mkEnableOption "" // {
description = ''
Whether to let dibbler take over the entire machine.
This will restrict the machine to a single TTY and make the program unquittable.
You can still get access to PTYs via SSH and similar, if enabled.
'';
};
screen = {
package = lib.mkPackageOption pkgs "screen" { };
sessionName = lib.mkOption {
type = lib.types.str;
default = "drumknotty";
example = "myscreensessionname";
description = ''
Sets the screen session name.
'';
};
limitHeight = lib.mkOption {
type = with lib.types; nullOr ints.unsigned;
default = null;
example = 42;
description = ''
If set, limits the height of the screen dibbler uses to the given number of lines.
'';
};
limitWidth = lib.mkOption {
type = with lib.types; nullOr ints.unsigned;
default = null;
example = 80;
description = ''
If set, limits the width of the screen dibbler uses to the given number of columns.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enable -> lib.any (b: b) [
cfg.dibbler.enable
cfg.worblehat.enable
];
message = "DrumknoTTY must have at least one service enabled";
}
];
users = {
users.drumknotty = {
group = "drumknotty";
extraGroups = [ "lp" ];
isNormalUser = true;
# TODO: make this display the error log or error message in case that
# the screen session service is bootlooping or otherwise off.
shell =
lib.mkIf cfg.kioskMode
(pkgs.writeShellScriptBin "login-shell"
"${lib.getExe' cfg.screen.package "screen"} -x ${cfg.screen.sessionName} -p dibbler"
// {
shellPath = "/bin/login-shell";
});
};
groups.drumknotty = { };
};
boot.kernelParams = lib.mkIf cfg.kioskMode [
"console=tty1"
];
services.getty.autologinUser = lib.mkIf cfg.kioskMode "drumknotty";
systemd.services.drumknotty-screen-session = lib.mkIf cfg.kioskMode {
description = "Drumknotty Screen Session";
wantedBy = [
"default.target"
];
after =
# TODO: this could be refined
if (cfg.dibbler.createLocalDatabase || cfg.worblehat.createLocalDatabase) then
[
"postgresql.service"
"dibbler-setup-database.service"
"worblehat-setup-database.service"
]
else
[
"network.target"
];
serviceConfig = {
Type = "forking";
RemainAfterExit = false;
Restart = "always";
RestartSec = "5s";
SuccessExitStatus = 1;
User = "drumknotty";
Group = "drumknotty";
ExecStartPre =
let
screenArgs = lib.escapeShellArgs [
# Send the specified command to a running screen session
"-X"
# Session name
"-S"
"${cfg.screen.sessionName}"
"kill"
];
in
"-${lib.getExe' cfg.screen.package "screen"} ${screenArgs}";
ExecStart =
let
screenrc = let
convertToFile = lines: lib.pipe lines [
lib.concatLists
(lib.concatStringsSep "\n")
(pkgs.writeText "drumknotty-screenrc")
];
in convertToFile [
(lib.optionals (cfg.screen.limitWidth != null) [
"screen width ${toString cfg.screen.limitWidth}"
])
(lib.optionals (cfg.screen.limitHeight != null) [
"screen height ${toString cfg.screen.limitHeight}"
])
(let
dibblerArgs = lib.cli.toCommandLineShellGNU { } {
config = "/etc/dibbler/dibbler.toml";
};
in lib.optionals cfg.dibbler.enable [
"screen -t dibbler ${lib.getExe cfg.dibbler.package} ${dibblerArgs} loop"
])
(let
worblehatArgs = lib.cli.toCommandLineShellGNU { } {
config = "/etc/worblehat/config.toml";
};
in lib.optionals cfg.worblehat.enable [
"screen -t worblehat ${lib.getExe cfg.worblehat.package} ${worblehatArgs} cli"
])
[ "select 0" ]
];
screenArgs = lib.escapeShellArgs [
# -dm creates the screen in detached mode without accessing it
"-dm"
# Session name
"-S"
"${cfg.screen.sessionName}"
# Set optimal output mode instead of VT100 emulation
"-O"
# Enable login mode, updates utmp entries
"-l"
# Config file path
"-c"
"${screenrc}"
];
in
"${lib.getExe' cfg.screen.package "screen"} ${screenArgs}";
};
};
};
}
+113
View File
@@ -0,0 +1,113 @@
{
config,
pkgs,
lib,
...
}:
let
mainCfg = config.services.drumknotty;
cfg = config.services.drumknotty.dibbler;
format = pkgs.formats.toml { };
in
{
options.services.drumknotty.dibbler = {
enable = lib.mkEnableOption "";
package = lib.mkPackageOption pkgs "dibbler" { };
settings = lib.mkOption {
description = "Configuration for dibbler";
default = { };
type = lib.types.submodule {
freeformType = format.type;
};
};
createLocalDatabase = lib.mkEnableOption "" // {
description = ''
Whether to set up a local postgres database automatically.
::: {.note}
You must set up postgres manually before enabling this option.
:::
'';
};
};
config = lib.mkIf (mainCfg.enable && cfg.enable) {
assertions = [
{
assertion = cfg.createLocalDatabase -> config.services.postgresql.enable;
message = "PostgreSQL must be enabled for dibbler to create a local database";
}
];
environment.systemPackages = [ cfg.package ];
environment.etc."dibbler/dibbler.toml".source = format.generate "dibbler.toml" cfg.settings;
services.drumknotty.dibbler.settings = {
limits = {
low_credit_warning_limit = lib.mkDefault (-100);
user_recent_transaction_limit = lib.mkDefault 100;
};
printer = {
label_type = lib.mkDefault "62";
label_rotate = lib.mkDefault false;
};
database = {
type = lib.mkIf cfg.createLocalDatabase "postgresql";
postgresql = {
username = lib.mkDefault "dibbler";
dbname = lib.mkDefault "dibbler";
host = lib.mkIf cfg.createLocalDatabase "/run/postgresql";
};
};
};
services.drumknotty.dibbler.settings.general = lib.mkIf mainCfg.kioskMode {
quit_allowed = false;
stop_allowed = false;
};
services.postgresql = lib.mkIf cfg.createLocalDatabase {
authentication = ''
local ${cfg.settings.database.postgresql.dbname} ${cfg.settings.database.postgresql.username} peer map=${cfg.settings.database.postgresql.username}
'';
identMap = ''
${cfg.settings.database.postgresql.username} drumknotty ${cfg.settings.database.postgresql.username}
'';
ensureDatabases = [ cfg.settings.database.postgresql.dbname ];
ensureUsers = [{
name = cfg.settings.database.postgresql.username;
ensureDBOwnership = true;
ensureClauses.login = true;
}];
};
systemd.services.dibbler-setup-database = lib.mkIf cfg.createLocalDatabase {
description = "Dibbler database setup";
wantedBy = [ "default.target" ];
requiredBy = [ "drumknotty-screen-session.service" ];
before = [ "drumknotty-screen-session.service" ];
after = [ "postgresql.service" ];
unitConfig = {
ConditionPathExists = "!/var/lib/dibbler/.db-setup-done";
};
serviceConfig = {
Type = "oneshot";
ExecStart = "${lib.getExe cfg.package} --config /etc/dibbler/dibbler.toml create-db";
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/dibbler/.db-setup-done";
StateDirectory = "dibbler";
User = "drumknotty";
Group = "drumknotty";
};
};
};
}
+209
View File
@@ -0,0 +1,209 @@
{
config,
pkgs,
lib,
...
}:
let
mainCfg = config.services.drumknotty;
cfg = config.services.drumknotty.worblehat;
format = pkgs.formats.toml { };
in
{
options.services.drumknotty.worblehat = {
enable = lib.mkEnableOption "";
package = lib.mkPackageOption pkgs "worblehat" { };
settings = lib.mkOption {
description = "Configuration for worblehat";
default = { };
type = lib.types.submodule {
freeformType = format.type;
};
};
createLocalDatabase = lib.mkEnableOption "" // {
description = ''
Whether to set up a local postgres database automatically.
::: {.note}
You must set up postgres manually before enabling this option.
:::
'';
};
deadline-daemon = {
enable = lib.mkEnableOption "" // {
description = ''
Whether to enable the worblehat deadline-daemon service,
which periodically checks for upcoming deadlines and notifies users.
Note that this service is independent of the main worblehat service,
and must be enabled separately.
'';
};
onCalendar = lib.mkOption {
type = lib.types.str;
description = ''
How often to trigger rendering the map,
in the format of a systemd timer onCalendar configuration.
See {manpage}`systemd.timer(5)`.
'';
default = "*-*-* 10:15:00";
};
};
};
config = lib.mkMerge [
{
assertions = [
{
assertion = cfg.createLocalDatabase -> config.services.postgresql.enable;
message = "PostgreSQL must be enabled for worblehat to create a local database";
}
];
# TODO: Retrieve defaults from the example config file in the project code.
services.drumknotty.worblehat.settings = {
logging = {
debug = lib.mkDefault true;
debug_sql = lib.mkDefault false;
};
database = {
type = lib.mkDefault "sqlite";
sqlite.path = lib.mkDefault "./worblehat.sqlite";
postgresql = {
host = lib.mkDefault "localhost";
port = lib.mkDefault 5432;
username = lib.mkDefault "worblehat";
password = lib.mkDefault "/var/lib/worblehat/db-password";
database = lib.mkDefault "worblehat";
};
};
flask = {
TESTING = lib.mkDefault true;
DEBUG = lib.mkDefault true;
FLASK_ENV = lib.mkDefault "development";
SECRET_KEY = lib.mkDefault "change-me";
};
smtp = {
enabled = lib.mkDefault false;
host = lib.mkDefault "smtp.pvv.ntnu.no";
port = lib.mkDefault 587;
username = lib.mkDefault "worblehat";
password = lib.mkDefault "/var/lib/worblehat/smtp-password";
from = lib.mkDefault "worblehat@pvv.ntnu.no";
subject_prefix = lib.mkDefault "[Worblehat]";
};
deadline_daemon = {
enabled = lib.mkDefault true;
dryrun = lib.mkDefault false;
warn_days_before_borrowing_deadline = lib.mkDefault [
5
1
];
days_before_queue_position_expires = lib.mkDefault 14;
warn_days_before_expiring_queue_position_deadline = lib.mkDefault [
3
1
];
};
};
}
(lib.mkIf ((mainCfg.enable && cfg.enable) || cfg.deadline-daemon.enable) {
environment.systemPackages = [ cfg.package ];
environment.etc."worblehat/config.toml".source = format.generate "worblehat-config.toml" cfg.settings;
})
(lib.mkIf (mainCfg.enable && cfg.enable) {
services.drumknotty.worblehat.settings.general = lib.mkIf mainCfg.kioskMode {
quit_allowed = false;
stop_allowed = false;
};
services.drumknotty.worblehat.settings.database = lib.mkIf cfg.createLocalDatabase {
type = "postgresql";
postgresql.host = "/run/postgresql";
};
services.postgresql = lib.mkIf cfg.createLocalDatabase {
authentication = ''
local ${cfg.settings.database.postgresql.database} ${cfg.settings.database.postgresql.username} peer map=${cfg.settings.database.postgresql.username}
'';
identMap = ''
${cfg.settings.database.postgresql.username} drumknotty ${cfg.settings.database.postgresql.username}
'';
ensureDatabases = [ cfg.settings.database.postgresql.database ];
ensureUsers = [{
name = cfg.settings.database.postgresql.username;
ensureDBOwnership = true;
ensureClauses.login = true;
}];
};
systemd.services.worblehat-setup-database = lib.mkIf cfg.createLocalDatabase {
description = "Worblehat database setup";
wantedBy = [ "default.target" ];
requiredBy = [ "drumknotty-screen-session.service" ];
before = [ "drumknotty-screen-session.service" ];
after = [ "postgresql.service" ];
unitConfig = {
ConditionPathExists = "!/var/lib/worblehat/.db-setup-done";
};
serviceConfig = {
Type = "oneshot";
ExecStart = "${lib.getExe cfg.package} --config /etc/worblehat/config.toml create-db";
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/worblehat/.db-setup-done";
StateDirectory = "worblehat";
User = "drumknotty";
Group = "drumknotty";
};
};
})
(lib.mkIf cfg.deadline-daemon.enable {
systemd.timers.worblehat-deadline-daemon = lib.mkIf cfg.deadline-daemon.enable {
description = "Worblehat Deadline Daemon";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.deadline-daemon.onCalendar;
Persistent = true;
};
};
systemd.services.worblehat-deadline-daemon = lib.mkIf cfg.deadline-daemon.enable {
description = "Worblehat Deadline Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
CPUSchedulingPolicy = "idle";
IOSchedulingClass = "idle";
ExecStart =
let
worblehatArgs = lib.cli.toCommandLineShellGNU { } {
config = "/etc/worblehat/config.toml";
};
in
"${lib.getExe cfg.package} ${worblehatArgs} deadline-daemon";
User = "drumknotty";
Group = "drumknotty";
};
};
})
];
}
+4 -4
View File
@@ -10,18 +10,18 @@ let
in in
buildNpmPackage { buildNpmPackage {
pname = "delete-your-element"; pname = "delete-your-element";
version = "3.5.1"; version = "3.6.0";
src = fetchFromGitea { src = fetchFromGitea {
domain = "git.pvv.ntnu.no"; domain = "git.pvv.ntnu.no";
owner = "Drift"; owner = "Drift";
repo = "delete-your-element"; repo = "delete-your-element";
rev = "80ac1d9d79207b6327975a264fcd9747b99a2a5d"; rev = "44fb6a02d3139e8ab10e9660ad931e5e70d1205f";
hash = "sha256-fcBpUZ+WEMUXyyo/uaArl4D1NJmK95isWqhFSt6HzUU="; hash = "sha256-wDQhPbxwdkAm0kPhaDNjbk8rVFxnGinffVdASdFrYnU=";
}; };
inherit nodejs; inherit nodejs;
npmDepsHash = "sha256-EYxJi6ObJQOLyiJq4C3mV6I62ns9l64ZHcdoQxmN5Ao="; npmDepsHash = "sha256-h1mmE0/+Y7SBwnI0vaYvV+KqRDJGzwJvDUOkigzHcOY=";
dontNpmBuild = true; dontNpmBuild = true;
nativeBuildInputs = [ makeWrapper ]; nativeBuildInputs = [ makeWrapper ];
+4 -3
View File
@@ -1,5 +1,6 @@
config: config:
mysqld_exporter_password: ENC[AES256_GCM,data:I9K+QMqaN3FOOVKzeOR9Q6UERStXX0P8WEHyN1jzzbM=,iv:UxvIdlfAyJvNuxPkU4+guKPa0fiD0vVLzHOTYktcmso=,tag:ltnIqEwESYx9HBu8UN0ZLw==,type:str] mysqld_exporter_password: ENC[AES256_GCM,data:I9K+QMqaN3FOOVKzeOR9Q6UERStXX0P8WEHyN1jzzbM=,iv:UxvIdlfAyJvNuxPkU4+guKPa0fiD0vVLzHOTYktcmso=,tag:ltnIqEwESYx9HBu8UN0ZLw==,type:str]
postgresql_dibbler_password: ENC[AES256_GCM,data:wP4CVz9qRE3CJrblWWYqOIkcH3LM5H81,iv:j8zr1TRNlPPqIppYlWhDoKlL7m2Ph2wQlU6bvFj8R9A=,tag:Cfp8zbYkoLqI7DTDpOBlJw==,type:str]
keys: keys:
grafana: grafana:
secret_key: ENC[AES256_GCM,data:+WoAJbDBEgKs0RoHT+7oEELAVQ+/2Xt+5RTMSXg23moCqVRx+Gzll9P5Drw=,iv:AkRn/Y20iEe5i1T+84wAgLCTFtAox2G3giyawAkltAw=,tag:BZbt5Wb5lYLIJBm/pfP4GQ==,type:str] secret_key: ENC[AES256_GCM,data:+WoAJbDBEgKs0RoHT+7oEELAVQ+/2Xt+5RTMSXg23moCqVRx+Gzll9P5Drw=,iv:AkRn/Y20iEe5i1T+84wAgLCTFtAox2G3giyawAkltAw=,tag:BZbt5Wb5lYLIJBm/pfP4GQ==,type:str]
@@ -72,8 +73,8 @@ sops:
dC9meDZlc3d3aUJEVjc4REF0Y1BLcGcK79LbJzc5KVgEgyJR11crGuX8YcVoJBbT dC9meDZlc3d3aUJEVjc4REF0Y1BLcGcK79LbJzc5KVgEgyJR11crGuX8YcVoJBbT
Fin7Zoon06L7qx0Zw5u27wV7RKMnYT7hOMiWs6660ZTLcYJ5M1aEZQ== Fin7Zoon06L7qx0Zw5u27wV7RKMnYT7hOMiWs6660ZTLcYJ5M1aEZQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-03-16T20:08:18Z" lastmodified: "2026-06-07T12:51:04Z"
mac: ENC[AES256_GCM,data:C2tpWppc13jKJq5d4nmAKQOaNWHm27TKwxAxm1fi2lejN1lqUaoz5bHfTBA7MfaWvuP5uZnfbtG32eeu48mnlWpo58XRUFFecAhb9JUpW9s5IR3/nbzLNkGU7H5C0oWPrxI4thd+bAVduIgBjjFyGj1pe6J9db3c0yUWRwNlwGU=,iv:YpoQ4psiFYOWLGipxv1QvRvr034XFsyn2Bhyy39HmOo=,tag:ByiCWygFC/VokVTbdLoLgg==,type:str] mac: ENC[AES256_GCM,data:otGwzc3Bme1PGRU4zWRRf3kmAf5EDjT8sPkDK/zDrO0ve+d3x99Qls4DBZV8G6FsFtUXbeKVo70I5VpqUgaIef+nyMl6zzWkW1wpbKIBYjh5fkaP0xxhLnw+shrB088PAXT0wkP7hJBLO0ZJgTrpprJKhmeqVj2HEXBSJ1wSjk8=,iv:cjqtdbWBVLUfpQdykb3vKDKa/VC/kGBybwYtG/eStqc=,tag:remOj4bX/H/LlAPgcXHAdA==,type:str]
pgp: pgp:
- created_at: "2026-05-20T17:35:58Z" - created_at: "2026-05-20T17:35:58Z"
enc: |- enc: |-
@@ -96,4 +97,4 @@ sops:
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: F7D37890228A907440E1FD4846B9228E814A2AAC fp: F7D37890228A907440E1FD4846B9228E814A2AAC
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.4 version: 3.12.2
+21 -18
View File
@@ -1,10 +1,12 @@
dibbler: dibbler:
postgresql: postgresql:
password: ENC[AES256_GCM,data:3X9A3jOpFVRuBg0gRiCEsZVKfLI=,iv:XC7LBNUhALk9IEhItV8fO5p/m7VKL0REBY1W2IZt7G4=,tag:l18R7EhbOlucZHFQiEvpHw==,type:str] password: ENC[AES256_GCM,data:ZeNKipcCB+z8QVGeg1iV3MUXqALjotVz,iv:xCtgOoe6Pkr6Cq3vL+T4L+GW1KAcgP/xUz3YbHs5bCc=,tag:/X5phRYDAws8Aam1j+UaTw==,type:str]
worblehat:
postgresql:
password: ENC[AES256_GCM,data:WpJR6MumY+7WUYdVVgAqv1af+NmqecTMO9aP5lidSpE=,iv:7aoN8mjXckd81LxasMSG3R2vqj0SvzSl7wrEQ1LwToo=,tag:zeeNcEpkYnqyd8be0ZS+kQ==,type:str]
sops: sops:
age: age:
- recipient: age1hzkvnktkr8t5gvtq0ccw69e44z5z6wf00n3xhk3hj24emf07je5s6q2evr - enc: |
enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvTk5YU3Z2Yy9HS1R4ME5I YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvTk5YU3Z2Yy9HS1R4ME5I
UU1PRWVncHJYcXY5RlFpOWVQUWZsdy93ZDFBCnlxWkpaL1g5WmNSckNYd202WE40 UU1PRWVncHJYcXY5RlFpOWVQUWZsdy93ZDFBCnlxWkpaL1g5WmNSckNYd202WE40
@@ -12,8 +14,8 @@ sops:
ZnllQzJiK1ZkRmFndmtYdW9IclFWY1EK82f1iGt3nt8dJnEQlMujNqConf6Qq6GX ZnllQzJiK1ZkRmFndmtYdW9IclFWY1EK82f1iGt3nt8dJnEQlMujNqConf6Qq6GX
hqoqPoc2EM4kun28Bbpq4pAY7eEPRrWFqOkjYVvgIRoS88D7xT3LWg== hqoqPoc2EM4kun28Bbpq4pAY7eEPRrWFqOkjYVvgIRoS88D7xT3LWg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ug30gg4y7ftuya0wdv7q0vh4egn00wlv2th7mt7cgc2ze46wmvyq9lq6ge recipient: age1hzkvnktkr8t5gvtq0ccw69e44z5z6wf00n3xhk3hj24emf07je5s6q2evr
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5WTJIOUcxRlBuNmRrNUZo YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5WTJIOUcxRlBuNmRrNUZo
MXFxeVJBTEhDK00yTUw1U2dHckNFYWZKWkhNCnYxYmtrUEVvd1RaYUI5WTRTRW16 MXFxeVJBTEhDK00yTUw1U2dHckNFYWZKWkhNCnYxYmtrUEVvd1RaYUI5WTRTRW16
@@ -21,8 +23,8 @@ sops:
eTB4WldMNW9GNUwwaEUzRThsemxRVzQKGpa0J2PBzDRdHijm0e3nFAaxQCHUjz+L eTB4WldMNW9GNUwwaEUzRThsemxRVzQKGpa0J2PBzDRdHijm0e3nFAaxQCHUjz+L
KataXJEMCijJ6k+7vpb5QMxe2jB1J2PMxNGFp0bWAy2Al3p/Ez2Kww== KataXJEMCijJ6k+7vpb5QMxe2jB1J2PMxNGFp0bWAy2Al3p/Ez2Kww==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1mrnldl334l2nszuta6ywvewng0fswv2dz9l5g4qcwe3nj4yxf92qjskdx6 recipient: age1ug30gg4y7ftuya0wdv7q0vh4egn00wlv2th7mt7cgc2ze46wmvyq9lq6ge
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZaW1ZSXhVeFVTQW9WYzVh YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZaW1ZSXhVeFVTQW9WYzVh
WkVUM2JkOU5VNU9oQXE2Y2pvcFlOWTdvbnpJClduS0RHL2xja291a2doQ0wzbzhQ WkVUM2JkOU5VNU9oQXE2Y2pvcFlOWTdvbnpJClduS0RHL2xja291a2doQ0wzbzhQ
@@ -30,8 +32,8 @@ sops:
ZUdnS2RvOXI1dGNYQTl6ZHE1cUdMWHMK4ycAJQLyKCgJIzjQ02bPjz4Ct9eO6ivw ZUdnS2RvOXI1dGNYQTl6ZHE1cUdMWHMK4ycAJQLyKCgJIzjQ02bPjz4Ct9eO6ivw
kfWhyMaoWwM9PhFcwSak0cLpX0C/IOzSzO78pf3WhG16pV7aXapdog== kfWhyMaoWwM9PhFcwSak0cLpX0C/IOzSzO78pf3WhG16pV7aXapdog==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1hmpdk4h69wxpwqk9tkud39f66hprhehxtzhgw97r6dvr7v0mx5jscsuhkn recipient: age1mrnldl334l2nszuta6ywvewng0fswv2dz9l5g4qcwe3nj4yxf92qjskdx6
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqaml0OVlhcUJSU1hSY3lP YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqaml0OVlhcUJSU1hSY3lP
bkM0cUV4Z2ZLeERHZ3BUNExuYS9KSU5CekQ4CmQ3SE1vdDBtdFJ6czZYR3U5Tk1X bkM0cUV4Z2ZLeERHZ3BUNExuYS9KSU5CekQ4CmQ3SE1vdDBtdFJ6czZYR3U5Tk1X
@@ -39,8 +41,8 @@ sops:
Sy9XbjhwOFR6SFpaNHZLd3ZxdmxOVUEKBBbGmdVVlKHxO+/iODznLP3+dJGppybW Sy9XbjhwOFR6SFpaNHZLd3ZxdmxOVUEKBBbGmdVVlKHxO+/iODznLP3+dJGppybW
+1k9uenVHzie+pDKcrQpSyX2WDnmgg7hUAUiXPuz1eEWmwbRJnU/5w== +1k9uenVHzie+pDKcrQpSyX2WDnmgg7hUAUiXPuz1eEWmwbRJnU/5w==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1wrssr4z4g6vl3fd3qme5cewchmmhm0j2xe6wf2meu4r6ycn37anse98mfs recipient: age1hmpdk4h69wxpwqk9tkud39f66hprhehxtzhgw97r6dvr7v0mx5jscsuhkn
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXK01vOVV5YlhsZ2ljYS91 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXK01vOVV5YlhsZ2ljYS91
OUVEaEpTbXFKOHVNVDVoMTlrS05wRmsyM2dvCjZHOXlCUGowd0J4UlQzSzM5dWJ0 OUVEaEpTbXFKOHVNVDVoMTlrS05wRmsyM2dvCjZHOXlCUGowd0J4UlQzSzM5dWJ0
@@ -48,8 +50,8 @@ sops:
RUR6Yi9SUDFCUkZmRk5hYTVFeGloZXcKY/XtaSoW8Pu2wS4oistLSc0T5JvMnt+w RUR6Yi9SUDFCUkZmRk5hYTVFeGloZXcKY/XtaSoW8Pu2wS4oistLSc0T5JvMnt+w
s3yfe/zx9/1K6OtbeljF9FZVOB/dOamvk+Qlfl0T5qush7/WgGzErA== s3yfe/zx9/1K6OtbeljF9FZVOB/dOamvk+Qlfl0T5qush7/WgGzErA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1zhxul786an743u0fascv4wtc5xduu7qfy803lfs539yzhgmlq5ds2lznt5 recipient: age1wrssr4z4g6vl3fd3qme5cewchmmhm0j2xe6wf2meu4r6ycn37anse98mfs
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOM0pFb2tRTURtWmp6elRN YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOM0pFb2tRTURtWmp6elRN
M0xtajlzMTNPMnppcGhJMVlsNHdwWmNGbFVFCnlxM1JQTkR2elAvdytKUEJ3djBS M0xtajlzMTNPMnppcGhJMVlsNHdwWmNGbFVFCnlxM1JQTkR2elAvdytKUEJ3djBS
@@ -57,8 +59,8 @@ sops:
eWlyWGhaS1JCNitUSVVScFk2WGEvOG8K2rpYPGx5jhyyRK4UkeJR96wDFr4Frzsr eWlyWGhaS1JCNitUSVVScFk2WGEvOG8K2rpYPGx5jhyyRK4UkeJR96wDFr4Frzsr
QWz7fYZRWKWf0H0qn+bm9IfVJiBAlS5i16D1FnipZVmdWefFaZSEPg== QWz7fYZRWKWf0H0qn+bm9IfVJiBAlS5i16D1FnipZVmdWefFaZSEPg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1sqs7urnzsdy64efmd0zukzv3gs5pnjksuxd7nqmdwdy5l0nqnunq6hyune recipient: age1zhxul786an743u0fascv4wtc5xduu7qfy803lfs539yzhgmlq5ds2lznt5
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJVFV0WVZrK0wzbnhkcmcz YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJVFV0WVZrK0wzbnhkcmcz
c2lIdVlKcFpoYjZIWlNPN0M5N2g2WG9YdlRJCjg5YlNoSzQ5YW5yRUVSeTEzRThY c2lIdVlKcFpoYjZIWlNPN0M5N2g2WG9YdlRJCjg5YlNoSzQ5YW5yRUVSeTEzRThY
@@ -66,8 +68,9 @@ sops:
MmxPMWNPYzJiOFRqY2VYczhvRm5IR3cKpUVV+zsMolsHI2YK9YqC6ecNT6QXv0TV MmxPMWNPYzJiOFRqY2VYczhvRm5IR3cKpUVV+zsMolsHI2YK9YqC6ecNT6QXv0TV
d1SpXRAexZBeWCCHBjSdvQBl8AT4EwrAIP2M2o++6i5DaGoGiEIWZQ== d1SpXRAexZBeWCCHBjSdvQBl8AT4EwrAIP2M2o++6i5DaGoGiEIWZQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-10T20:02:28Z" recipient: age1sqs7urnzsdy64efmd0zukzv3gs5pnjksuxd7nqmdwdy5l0nqnunq6hyune
mac: ENC[AES256_GCM,data:i8CjVxoD7zdkLNJlI9DCo/tDV5DUI7JdpozLtYZzI7Cu51GayaE2Y3Wg4de6P0L7C3FER04WfRe/h+G9PLZICX/CfSipQysyrEq3Pjt9IKsjytDhP9VYJ36QFGF0PuHUQAMSLts/tAoAvLue6MP+V82l5js9ghvyBrzyBGxoyJw=,iv:QFNxvCYxrSkwy7iT+2BEacNPftDXju1cibprVPDjic0=,tag:496E+oCy/VwTylyaWhQD+A==,type:str] lastmodified: "2026-06-07T08:58:13Z"
mac: ENC[AES256_GCM,data:rchs8pGkw7dthGOQNDB5p/kgQdfdystaC+jRr0bZnA4Q41+PVMu+vBSMIZ+9zZek6oENgchmV5rRS0CEeb9UQMMiPXCk2Q2jMaDfiNCmOmjf0YYeFWRM6g2lA+IZ3RgKjwhXa6i5JOeNrNewMjtx7MFcHTn3EBlg2mztyn1xbT0=,iv:0yvoFPDxpugaBmTtXSmhNz9XusJHrU3E02tBm1hVsZo=,tag:hZ1BzqfmlT1OhPSsn+CCTg==,type:str]
pgp: pgp:
- created_at: "2026-02-10T20:01:32Z" - created_at: "2026-02-10T20:01:32Z"
enc: |- enc: |-
@@ -90,4 +93,4 @@ sops:
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: F7D37890228A907440E1FD4846B9228E814A2AAC fp: F7D37890228A907440E1FD4846B9228E814A2AAC
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.13.0