Compare commits

..

1 commit

Author SHA1 Message Date
aef71f548a
WIP: authelia: Add SSO 2024-06-14 01:01:39 +02:00
69 changed files with 3658 additions and 3641 deletions

View file

@ -1,14 +0,0 @@
# Run this command to always ignore formatting commits in `git blame`
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Switch to nixfmt formatting
04f7a7ef1d38906163afc9cddfa8ce2b0ebf3b45
# Switch to nixpkgs-fmt formatting
fd138d45e6a2cad89fead6e9f246ba282070d6b7
# Switch to alejandra formatting
046a88905ddfa7f9edc3291c310dbb985dee34f9
# Apply wide linting
63b3cbe00be80ccb4b221aad64eb657ae5c96d70

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
/result /result
*.qcow2 *.qcow2
/gcroots/

View file

@ -1,5 +1,5 @@
keys: keys:
- &tlater B9C7AE554602D82B1AC2CFD0BC7BB2DB17C78E42! - &tlater 535B61015823443941C744DD12264F6BBDFABA89
- &server_tlaternet 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b - &server_tlaternet 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b
- &server_hetzner1 0af7641adb8aa843136cf6d047f71da3e5ad79f9 - &server_hetzner1 0af7641adb8aa843136cf6d047f71da3e5ad79f9
- &server_staging 2f5caa73e7ceea4fcc8d2881fde587e6737d2dbc - &server_staging 2f5caa73e7ceea4fcc8d2881fde587e6737d2dbc

View file

@ -1,61 +0,0 @@
{
self,
nixpkgs,
deploy-rs,
system,
...
}:
let
pkgs = nixpkgs.legacyPackages.${system};
statix' = pkgs.statix.overrideAttrs (old: {
patches = old.patches ++ [
(pkgs.fetchpatch {
url = "https://github.com/oppiliappan/statix/commit/925dec39bb705acbbe77178b4d658fe1b752abbb.patch";
hash = "sha256-0wacO6wuYJ4ufN9PGucRVJucFdFFNF+NoHYIrLXsCWs=";
})
];
});
runNuCheck =
{
name,
packages,
check,
}:
pkgs.stdenvNoCC.mkDerivation {
inherit name;
src = nixpkgs.lib.cleanSourceWith {
src = self;
filter = nixpkgs.lib.cleanSourceFilter;
};
dontPatch = true;
dontConfigure = true;
dontBuild = true;
dontInstall = true;
dontFixup = true;
doCheck = true;
checkInputs = nixpkgs.lib.singleton pkgs.nushell ++ packages;
checkPhase = ''
nu ${check}
'';
};
in
nixpkgs.lib.recursiveUpdate {
lints = runNuCheck {
name = "lints";
packages = [
pkgs.deadnix
pkgs.nixfmt-rfc-style
pkgs.shellcheck
statix'
];
check = ./lints.nu;
};
} (deploy-rs.lib.${system}.deployChecks self.deploy)

View file

@ -1,39 +0,0 @@
#!/usr/bin/env nu
let shell_files = ls **/*.sh | get name
let nix_files = ls **/*.nix | where name !~ "hardware-configuration.nix|_sources" | get name
let linters = [
([shellcheck] ++ $shell_files)
([nixfmt --check --strict] ++ $nix_files)
([deadnix --fail] ++ $nix_files)
([statix check] ++ $nix_files)
]
mkdir $env.out
def run-linter [linterArgs: list<string>] {
print $'Running ($linterArgs.0)...'
let exit_code = try {
^$linterArgs.0 ...($linterArgs | skip 1)
$env.LAST_EXIT_CODE
} catch {|e| $e.exit_code}
[$linterArgs.0, $exit_code]
}
let results = $linters | each {|linter| run-linter $linter}
print 'Linter results:'
let success = $results | each {|result|
match $result.1 {
0 => {print $'(ansi green)($result.0)(ansi reset)'}
_ => {print $'(ansi red)($result.0)(ansi reset)'}
}
$result.1
} | math sum
exit $success

View file

@ -1,10 +1,11 @@
{ {
config, config,
pkgs,
lib,
modulesPath, modulesPath,
flake-inputs, flake-inputs,
... ...
}: }: {
{
imports = [ imports = [
flake-inputs.disko.nixosModules.disko flake-inputs.disko.nixosModules.disko
flake-inputs.sops-nix.nixosModules.sops flake-inputs.sops-nix.nixosModules.sops
@ -13,40 +14,49 @@
"${modulesPath}/profiles/minimal.nix" "${modulesPath}/profiles/minimal.nix"
(import ../modules) (import ../modules)
./services/afvalcalendar.nix
./services/auth.nix
./services/backups.nix ./services/backups.nix
./services/battery-manager.nix ./services/battery-manager.nix
./services/conduit ./services/conduit.nix
./services/crowdsec.nix ./services/fail2ban.nix
./services/foundryvtt.nix ./services/foundryvtt.nix
./services/gitea.nix ./services/gitea.nix
./services/immich.nix
./services/metrics ./services/metrics
./services/nextcloud.nix ./services/nextcloud.nix
./services/webserver.nix ./services/webserver.nix
./services/wireguard.nix ./services/wireguard.nix
# ./services/starbound.nix -- Not currently used ./services/starbound.nix
./services/postgres.nix ./services/postgres.nix
./nginx.nix ./nginx.nix
./sops.nix ./sops.nix
]; ];
nixpkgs.overlays = [ (_: prev: { local = import ../pkgs { pkgs = prev; }; }) ]; nixpkgs.overlays = [
(final: prev: {
local = import ../pkgs {
pkgs = prev;
lib = prev.lib;
};
})
];
nix = { nix = {
package = pkgs.nixFlakes;
extraOptions = '' extraOptions = ''
experimental-features = nix-command flakes experimental-features = nix-command flakes
''; '';
# Enable remote builds from tlater # Enable remote builds from tlater
settings.trusted-users = [ "@wheel" ]; settings.trusted-users = ["@wheel"];
}; };
nixpkgs.config.allowUnfreePredicate = pkg:
builtins.elem (lib.getName pkg) ["steam-original" "steam-runtime" "steam-run" "steamcmd"];
# Optimization for minecraft servers, see: # Optimization for minecraft servers, see:
# https://bugs.mojang.com/browse/MC-183518 # https://bugs.mojang.com/browse/MC-183518
boot.kernelParams = [ boot.kernelParams = ["highres=off" "nohz=off"];
"highres=off"
"nohz=off"
];
networking = { networking = {
usePredictableInterfaceNames = false; usePredictableInterfaceNames = false;
@ -97,15 +107,15 @@
users.users.tlater = { users.users.tlater = {
isNormalUser = true; isNormalUser = true;
extraGroups = [ "wheel" ]; extraGroups = ["wheel"];
openssh.authorizedKeys.keyFiles = [ ../keys/tlater.pub ]; openssh.authorizedKeys.keyFiles = [../keys/tlater.pub];
}; };
services = { services = {
openssh = { openssh = {
enable = true; enable = true;
allowSFTP = false; allowSFTP = false;
ports = [ 2222 ]; ports = [2222];
startWhenNeeded = true; startWhenNeeded = true;
settings = { settings = {
@ -124,14 +134,14 @@
pam = { pam = {
sshAgentAuth = { sshAgentAuth = {
enable = true; enable = true;
authorizedKeysFiles = [ "/etc/ssh/authorized_keys.d/%u" ]; authorizedKeysFiles = ["/etc/ssh/authorized_keys.d/%u"];
}; };
services.sudo.sshAgentAuth = true; services.sudo.sshAgentAuth = true;
}; };
}; };
# Remove some unneeded packages # Remove some unneeded packages
environment.defaultPackages = [ ]; environment.defaultPackages = [];
system.stateVersion = "20.09"; system.stateVersion = "20.09";
} }

View file

@ -8,7 +8,7 @@
# disables it by default. # disables it by default.
# #
# TODO(tlater): See if would be useful for anything? # TODO(tlater): See if would be useful for anything?
boot.kernelParams = [ "nosgx" ]; boot.kernelParams = ["nosgx"];
networking.hostName = "hetzner-1"; networking.hostName = "hetzner-1";
services.nginx.domain = "tlater.net"; services.nginx.domain = "tlater.net";
@ -19,11 +19,15 @@
addresses = [ addresses = [
# IPv4 # IPv4
{ {
Address = "116.202.158.55/32"; addressConfig = {
Peer = "116.202.158.1/32"; # Gateway Address = "116.202.158.55/32";
Peer = "116.202.158.1/32"; # Gateway
};
} }
# IPv6 # IPv6
{ Address = "2a01:4f8:10b:3c85::2/64"; } {
addressConfig.Address = "2a01:4f8:10b:3c85::2/64";
}
]; ];
networkConfig = { networkConfig = {

View file

@ -1,106 +1,82 @@
{ {
disko.devices.disk = disko.devices.disk = let
let bootPartition = {
bootPartition = { size = "1M";
size = "1M"; type = "EF02";
type = "EF02"; };
swapPartition = {
# 8G is apparently recommended for this much RAM, but we set up
# 4G on both disks for mirroring purposes.
#
# That'll still be 8G during normal operation, and it's probably
# not too bad to have slightly less swap if a disk dies.
size = "4G";
content = {
type = "swap";
randomEncryption = true;
}; };
};
swapPartition = { mountOptions = ["compress=zstd" "noatime"];
# 8G is apparently recommended for this much RAM, but we set up in {
# 4G on both disks for mirroring purposes. sda = {
# type = "disk";
# That'll still be 8G during normal operation, and it's probably device = "/dev/sda";
# not too bad to have slightly less swap if a disk dies. content = {
size = "4G"; type = "gpt";
content = { partitions = {
type = "swap"; boot = bootPartition;
randomEncryption = true; swap = swapPartition;
};
};
mountOptions = [ disk1 = {
"compress=zstd" size = "100%";
"noatime" # Empty partition to combine in RAID0 with the other disk
];
in
{
sda = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
boot = bootPartition;
swap = swapPartition;
disk1 = {
size = "100%";
# Empty partition to combine in RAID0 with the other disk
};
}; };
}; };
}; };
};
sdb = { sdb = {
type = "disk"; type = "disk";
device = "/dev/sdb"; device = "/dev/sdb";
content = { content = {
type = "gpt"; type = "gpt";
partitions = { partitions = {
boot = bootPartition; boot = bootPartition;
swap = swapPartition; swap = swapPartition;
disk2 = { disk2 = {
size = "100%"; size = "100%";
content = { content = {
type = "btrfs"; type = "btrfs";
# Hack to get multi-device btrfs going # Hack to get multi-device btrfs going
# See https://github.com/nix-community/disko/issues/99 # See https://github.com/nix-community/disko/issues/99
extraArgs = [ extraArgs = ["-d" "raid1" "-m" "raid1" "--runtime-features" "quota" "/dev/sda3"];
"-d" subvolumes = {
"raid1" "/volume" = {};
"-m" "/volume/root" = {
"raid1" inherit mountOptions;
"--runtime-features" mountpoint = "/";
"quota"
"/dev/sda3"
];
subvolumes = {
"/volume" = { };
"/volume/root" = {
inherit mountOptions;
mountpoint = "/";
};
"/volume/home" = {
inherit mountOptions;
mountpoint = "/home";
};
"/volume/var" = {
inherit mountOptions;
mountpoint = "/var";
};
"/volume/var/lib/private/matrix-conduit" = {
mountOptions = [
# Explicitly don't compress here, since
# conduwuit's database does compression by
# itself, and relies on being able to read the
# raw file data from disk (which is impossible
# if btrfs compresses it)
"noatime"
];
mountpoint = "/var/lib/private/matrix-conduit";
};
"/volume/nix-store" = {
inherit mountOptions;
mountpoint = "/nix";
};
"/snapshots" = { };
}; };
"/volume/home" = {
inherit mountOptions;
mountpoint = "/home";
};
"/volume/var" = {
inherit mountOptions;
mountpoint = "/var";
};
"/volume/nix-store" = {
inherit mountOptions;
mountpoint = "/nix";
};
"/snapshots" = {};
}; };
}; };
}; };
}; };
}; };
}; };
};
} }

View file

@ -1,35 +1,19 @@
{ lib, ... }: {lib, ...}: {
{
users.users.tlater.password = "insecure"; users.users.tlater.password = "insecure";
# Disable graphical tty so -curses works # Disable graphical tty so -curses works
boot.kernelParams = [ "nomodeset" ]; boot.kernelParams = ["nomodeset"];
networking.hostName = "testvm"; networking.hostName = "testvm";
# Sets the base domain for nginx to a local domain so that we can
services = { # easily test locally with the VM.
# Sets the base domain for nginx to a local domain so that we can services.nginx.domain = "dev.local";
# easily test locally with the VM.
nginx.domain = "dev.local";
# Don't run this
batteryManager.enable = lib.mkForce false;
openssh.hostKeys = lib.mkForce [
{
type = "rsa";
bits = 4096;
path = "/etc/staging.key";
}
];
};
# Use the staging secrets # Use the staging secrets
sops.defaultSopsFile = lib.mkOverride 99 ../../keys/staging.yaml; sops.defaultSopsFile = lib.mkOverride 99 ../../keys/staging.yaml;
systemd.network.networks."10-eth0" = { systemd.network.networks."10-eth0" = {
matchConfig.Name = "eth0"; matchConfig.Name = "eth0";
gateway = [ "192.168.9.1" ];
networkConfig = { networkConfig = {
Address = "192.168.9.2/24"; Address = "192.168.9.2/24";
}; };
@ -43,6 +27,14 @@
source = ../../keys/hosts/staging.key; source = ../../keys/hosts/staging.key;
}; };
services.openssh.hostKeys = lib.mkForce [
{
type = "rsa";
bits = 4096;
path = "/etc/staging.key";
}
];
virtualisation.vmVariant = { virtualisation.vmVariant = {
virtualisation = { virtualisation = {
memorySize = 3941; memorySize = 3941;

View file

@ -1,78 +1,67 @@
{ config, lib, ... }:
{ {
services = { config,
nginx = { lib,
enable = true; ...
recommendedTlsSettings = true; }: {
recommendedOptimisation = true; services.nginx = {
recommendedGzipSettings = true; enable = true;
recommendedProxySettings = true; recommendedTlsSettings = true;
clientMaxBodySize = "10G"; recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
clientMaxBodySize = "10G";
statusPage = true; # For metrics, should be accessible only from localhost statusPage = true; # For metrics, should be accessible only from localhost
commonHttpConfig = '' commonHttpConfig = ''
log_format upstream_time '$remote_addr - $remote_user [$time_local] ' log_format upstream_time '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent ' '"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" ' '"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" ' 'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"'; 'uht="$upstream_header_time" urt="$upstream_response_time"';
''; '';
};
logrotate.settings =
{
# Override the default, just keep fewer logs
nginx.rotate = 6;
}
// lib.mapAttrs' (
virtualHost: _:
lib.nameValuePair "/var/log/nginx/${virtualHost}/access.log" {
frequency = "daily";
rotate = 2;
compress = true;
delaycompress = true;
su = "${config.services.nginx.user} ${config.services.nginx.group}";
postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`";
}
) config.services.nginx.virtualHosts;
backups.acme = {
user = "acme";
paths = lib.mapAttrsToList (
virtualHost: _: "/var/lib/acme/${virtualHost}"
) config.services.nginx.virtualHosts;
};
}; };
systemd.tmpfiles.rules = lib.mapAttrsToList ( services.logrotate.settings =
virtualHost: _: {
# # Override the default, just keep fewer logs
"d /var/log/nginx/${virtualHost} 0750 ${config.services.nginx.user} ${config.services.nginx.group}" nginx.rotate = 6;
) config.services.nginx.virtualHosts; }
// lib.mapAttrs' (virtualHost: _:
lib.nameValuePair "/var/log/nginx/${virtualHost}/access.log" {
frequency = "daily";
rotate = 2;
compress = true;
delaycompress = true;
su = "${config.services.nginx.user} ${config.services.nginx.group}";
postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`";
})
config.services.nginx.virtualHosts;
systemd.tmpfiles.rules =
lib.mapAttrsToList (
virtualHost: _:
#
"d /var/log/nginx/${virtualHost} 0750 ${config.services.nginx.user} ${config.services.nginx.group}"
)
config.services.nginx.virtualHosts;
security.acme = { security.acme = {
defaults.email = "tm@tlater.net"; defaults.email = "tm@tlater.net";
acceptTerms = true; acceptTerms = true;
certs."tlater.net" = { certs."tlater.net" = {
extraDomainNames = [ extraDomainNames = ["*.tlater.net"];
"*.tlater.net" dnsProvider = "hetzner";
"tlater.com" group = "nginx";
"*.tlater.com" credentialFiles."HETZNER_API_KEY_FILE" = config.sops.secrets."hetzner-api".path;
];
dnsProvider = "porkbun";
group = "ssl-cert";
credentialFiles = {
PORKBUN_API_KEY_FILE = config.sops.secrets."porkbun/api-key".path;
PORKBUN_SECRET_API_KEY_FILE = config.sops.secrets."porkbun/secret-api-key".path;
};
}; };
}; };
users.groups.ssl-cert = { }; services.backups.acme = {
user = "acme";
systemd.services.nginx.serviceConfig.SupplementaryGroups = [ paths =
config.security.acme.certs."tlater.net".group lib.mapAttrsToList (virtualHost: _: "/var/lib/acme/${virtualHost}")
]; config.services.nginx.virtualHosts;
};
} }

View file

@ -0,0 +1,67 @@
{
pkgs,
config,
...
}: {
systemd.services.afvalcalendar = {
description = "Enschede afvalcalendar -> ical converter";
wantedBy = ["multi-user.target"];
after = ["network.target"];
script = ''
${pkgs.local.afvalcalendar}/bin/afvalcalendar > /srv/afvalcalendar/afvalcalendar.ical
'';
startAt = "daily";
serviceConfig = {
DynamicUser = true;
ProtectHome = true; # Override the default (read-only)
PrivateDevices = true;
PrivateIPC = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = ["@system-service" "~@privileged @resources @setuid @keyring"];
Umask = 0002;
SupplementaryGroups = "afvalcalendar-hosting";
ReadWritePaths = "/srv/afvalcalendar";
};
};
services.nginx.virtualHosts."afvalcalendar.${config.services.nginx.domain}" = {
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
root = "/srv/afvalcalendar";
};
users.groups.afvalcalendar-hosting = {};
systemd.tmpfiles.settings."10-afvalcalendar" = {
"/srv/afvalcalendar".d = {
user = "nginx";
group = "afvalcalendar-hosting";
mode = "0775";
};
"/srv/afvalcalendar/afvalcalendar.ical".f = {
user = "nginx";
group = "afvalcalendar-hosting";
mode = "0775";
};
};
}

View file

@ -0,0 +1,95 @@
{
pkgs,
config,
...
}: let
user = config.services.authelia.instances.main.user;
domain = "auth.${config.services.nginx.domain}";
in {
services.authelia.instances.main = {
enable = true;
settings = {
theme = "auto";
access_control.default_policy = "one_factor";
authentication_backend = {
password_reset.disable = true;
file.path = "/var/lib/authelia-main/users.yml";
};
notifier.filesystem.filename = "/var/lib/authelia-main/notification.txt";
session = {
domain = config.services.nginx.domain;
redis.host = config.services.redis.servers.authelia.unixSocket;
};
storage.postgres = {
host = "/run/postgresql";
port = 5432;
database = user;
username = user;
password = "unnecessary";
};
};
secrets = {
storageEncryptionKeyFile = config.sops.secrets."authelia/storageEncryptionKey".path; # Database
sessionSecretFile = config.sops.secrets."authelia/sessionSecret".path; # Redis
jwtSecretFile = config.sops.secrets."authelia/jwtSecret".path;
};
};
systemd.services.authelia-main.after = ["postgresql.service"];
services.nginx = {
# TODO(tlater): Possibly remove on next authelia release
additionalModules = with pkgs.nginxModules; [
develkit
set-misc
];
virtualHosts."${domain}" = {
forceSSL = true;
enableACME = true;
enableHSTS = true;
locations = {
"/" = {
proxyPass = "http://127.0.0.1:9091";
recommendedProxySettings = false;
enableAutheliaProxy = true;
};
"/api/verify" = {
proxyPass = "http://127.0.0.1:9091";
recommendedProxySettings = false;
};
};
};
};
services.redis.servers.authelia = {
inherit user;
enable = true;
};
sops.secrets = {
"authelia/storageEncryptionKey" = {
owner = user;
group = user;
};
"authelia/sessionSecret" = {
owner = user;
group = user;
};
"authelia/jwtSecret" = {
owner = user;
group = user;
};
};
}

View file

@ -3,33 +3,27 @@
pkgs, pkgs,
lib, lib,
... ...
}: }: let
let
inherit (lib) types optional singleton; inherit (lib) types optional singleton;
mkShutdownScript = mkShutdownScript = service:
service:
pkgs.writeShellScript "backup-${service}-shutdown" '' pkgs.writeShellScript "backup-${service}-shutdown" ''
if systemctl is-active --quiet '${service}'; then if systemctl is-active --quiet '${service}'; then
touch '/tmp/${service}-was-active' touch '/tmp/${service}-was-active'
systemctl stop '${service}' systemctl stop '${service}'
fi fi
''; '';
mkRestartScript = mkRestartScript = service:
service:
pkgs.writeShellScript "backup-${service}-restart" '' pkgs.writeShellScript "backup-${service}-restart" ''
if [ -f '/tmp/${service}-was-active' ]; then if [ -f '/tmp/${service}-was-active' ]; then
rm '/tmp/${service}-was-active' rm '/tmp/${service}-was-active'
systemctl start '${service}' systemctl start '${service}'
fi fi
''; '';
writeScript = writeScript = name: packages: text:
name: packages: text: lib.getExe (pkgs.writeShellApplication {
lib.getExe ( inherit name text;
pkgs.writeShellApplication { runtimeInputs = packages;
inherit name text; });
runtimeInputs = packages;
}
);
# *NOT* a TOML file, for some reason quotes are interpreted # *NOT* a TOML file, for some reason quotes are interpreted
# *literally # *literally
@ -48,98 +42,96 @@ let
RESTIC_REPOSITORY = "rclone:storagebox:backups"; RESTIC_REPOSITORY = "rclone:storagebox:backups";
RCLONE_CONFIG = rcloneConfig; RCLONE_CONFIG = rcloneConfig;
}; };
in in {
{
options = { options = {
services.backups = lib.mkOption { services.backups = lib.mkOption {
description = lib.mdDoc '' description = lib.mdDoc ''
Configure restic backups with a specific tag. Configure restic backups with a specific tag.
''; '';
type = types.attrsOf ( type = types.attrsOf (types.submodule ({
types.submodule ( config,
{ name, ... }: name,
{ ...
options = { }: {
user = lib.mkOption { options = {
type = types.str; user = lib.mkOption {
description = '' type = types.str;
The user as which to run the backup. description = ''
''; The user as which to run the backup.
}; '';
paths = lib.mkOption { };
type = types.listOf types.str; paths = lib.mkOption {
description = '' type = types.listOf types.str;
The paths to back up. description = ''
''; The paths to back up.
}; '';
tag = lib.mkOption { };
type = types.str; tag = lib.mkOption {
description = '' type = types.str;
The restic tag to mark the backup with. description = ''
''; The restic tag to mark the backup with.
default = name; '';
}; default = name;
preparation = { };
packages = lib.mkOption { preparation = {
type = types.listOf types.package; packages = lib.mkOption {
default = [ ]; type = types.listOf types.package;
description = '' default = [];
The list of packages to make available in the description = ''
preparation script. The list of packages to make available in the
''; preparation script.
}; '';
text = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The preparation script to run before the backup.
This should include things like database dumps and
enabling maintenance modes. If a service needs to be
shut down for backups, use `pauseServices` instead.
'';
};
};
cleanup = {
packages = lib.mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
The list of packages to make available in the
cleanup script.
'';
};
text = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The cleanup script to run after the backup.
This should do things like cleaning up database dumps
and disabling maintenance modes.
'';
};
};
pauseServices = lib.mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
The systemd services that need to be shut down before
the backup can run. Services will be restarted after the
backup is complete.
This is intended to be used for services that do not
support hot backups.
'';
};
}; };
} text = lib.mkOption {
) type = types.nullOr types.str;
); default = null;
description = ''
The preparation script to run before the backup.
This should include things like database dumps and
enabling maintenance modes. If a service needs to be
shut down for backups, use `pauseServices` instead.
'';
};
};
cleanup = {
packages = lib.mkOption {
type = types.listOf types.package;
default = [];
description = ''
The list of packages to make available in the
cleanup script.
'';
};
text = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The cleanup script to run after the backup.
This should do things like cleaning up database dumps
and disabling maintenance modes.
'';
};
};
pauseServices = lib.mkOption {
type = types.listOf types.str;
default = [];
description = ''
The systemd services that need to be shut down before
the backup can run. Services will be restarted after the
backup is complete.
This is intended to be used for services that do not
support hot backups.
'';
};
};
}));
}; };
}; };
config = lib.mkIf (config.services.backups != { }) { config = lib.mkIf (config.services.backups != {}) {
systemd.services = systemd.services =
{ {
restic-prune = { restic-prune = {
@ -172,15 +164,16 @@ in
}; };
}; };
} }
// lib.mapAttrs' ( // lib.mapAttrs' (name: backup:
name: backup:
lib.nameValuePair "backup-${name}" { lib.nameValuePair "backup-${name}" {
# Don't want to restart mid-backup # Don't want to restart mid-backup
restartIfChanged = false; restartIfChanged = false;
environment = resticEnv // { environment =
RESTIC_CACHE_DIR = "%C/backup-${name}"; resticEnv
}; // {
RESTIC_CACHE_DIR = "%C/backup-${name}";
};
path = with pkgs; [ path = with pkgs; [
coreutils coreutils
@ -203,60 +196,47 @@ in
PrivateTmp = true; PrivateTmp = true;
ExecStart = [ ExecStart = [
(lib.concatStringsSep " " ( (lib.concatStringsSep " " (["${pkgs.restic}/bin/restic" "backup" "--tag" name] ++ backup.paths))
[
"${pkgs.restic}/bin/restic"
"backup"
"--tag"
name
]
++ backup.paths
))
]; ];
ExecStartPre = ExecStartPre =
map (service: "+${mkShutdownScript service}") backup.pauseServices map (service: "+${mkShutdownScript service}") backup.pauseServices
++ singleton ( ++ singleton (writeScript "backup-${name}-repo-init" [] ''
writeScript "backup-${name}-repo-init" [ ] '' restic snapshots || restic init
restic snapshots || restic init '')
'' ++ optional (backup.preparation.text != null)
) (writeScript "backup-${name}-prepare" backup.preparation.packages backup.preparation.text);
++ optional (backup.preparation.text != null) (
writeScript "backup-${name}-prepare" backup.preparation.packages backup.preparation.text
);
# TODO(tlater): Add repo pruning/checking # TODO(tlater): Add repo pruning/checking
ExecStopPost = ExecStopPost =
map (service: "+${mkRestartScript service}") backup.pauseServices map (service: "+${mkRestartScript service}") backup.pauseServices
++ optional (backup.cleanup.text != null) ( ++ optional (backup.cleanup.text != null)
writeScript "backup-${name}-cleanup" backup.cleanup.packages backup.cleanup.text (writeScript "backup-${name}-cleanup" backup.cleanup.packages backup.cleanup.text);
);
}; };
} })
) config.services.backups; config.services.backups;
systemd.timers = systemd.timers =
{ {
restic-prune = { restic-prune = {
wantedBy = [ "timers.target" ]; wantedBy = ["timers.target"];
timerConfig.OnCalendar = "Thursday 03:00:00 UTC"; timerConfig.OnCalendar = "Thursday 03:00:00 UTC";
# Don't make this persistent, in case the server was offline # Don't make this persistent, in case the server was offline
# for a while. This job cannot run at the same time as any # for a while. This job cannot run at the same time as any
# of the backup jobs. # of the backup jobs.
}; };
} }
// lib.mapAttrs' ( // lib.mapAttrs' (name: backup:
name: _:
lib.nameValuePair "backup-${name}" { lib.nameValuePair "backup-${name}" {
wantedBy = [ "timers.target" ]; wantedBy = ["timers.target"];
timerConfig = { timerConfig = {
OnCalendar = "Wednesday 02:30:00 UTC"; OnCalendar = "Wednesday 02:30:00 UTC";
RandomizedDelaySec = "1h"; RandomizedDelaySec = "1h";
FixedRandomDelay = true; FixedRandomDelay = true;
Persistent = true; Persistent = true;
}; };
} })
) config.services.backups; config.services.backups;
users = { users = {
# This user is only used to own the ssh key, because apparently # This user is only used to own the ssh key, because apparently
@ -265,7 +245,7 @@ in
group = "backup"; group = "backup";
isSystemUser = true; isSystemUser = true;
}; };
groups.backup = { }; groups.backup = {};
}; };
}; };
} }

View file

@ -1,16 +1,17 @@
{ config, flake-inputs, ... }:
{ {
imports = [ flake-inputs.sonnenshift.nixosModules.default ]; config,
flake-inputs,
...
}: {
imports = [
flake-inputs.sonnenshift.nixosModules.default
];
services.batteryManager = { services.batteryManager = {
enable = true; enable = true;
battery = "3ca39300-c523-4315-b9a3-d030f85a9373";
emailFile = "${config.sops.secrets."battery-manager/email".path}"; emailFile = "${config.sops.secrets."battery-manager/email".path}";
passwordFile = "${config.sops.secrets."battery-manager/password".path}"; passwordFile = "${config.sops.secrets."battery-manager/password".path}";
settings = {
battery_id = "3ca39300-c523-4315-b9a3-d030f85a9373";
log_level = "DEBUG";
};
}; };
} }

View file

@ -0,0 +1,254 @@
{
pkgs,
config,
lib,
...
}: let
inherit (lib.strings) concatMapStringsSep;
cfg = config.services.matrix-conduit;
domain = "matrix.${config.services.nginx.domain}";
turn-realm = "turn.${config.services.nginx.domain}";
in {
services.matrix-conduit = {
enable = true;
settings.global = {
address = "127.0.0.1";
server_name = domain;
database_backend = "rocksdb";
turn_uris = let
address = "${config.services.coturn.realm}:${toString config.services.coturn.listening-port}";
tls-address = "${config.services.coturn.realm}:${toString config.services.coturn.tls-listening-port}";
in [
"turn:${address}?transport=udp"
"turn:${address}?transport=tcp"
"turns:${tls-address}?transport=udp"
"turns:${tls-address}?transport=tcp"
];
};
};
systemd.services.heisenbridge = let
replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
registrationFile = builtins.toFile "heisenbridge-registration.yaml" (builtins.toJSON {
id = "heisenbridge";
url = "http://127.0.0.1:9898";
as_token = "@AS_TOKEN@";
hs_token = "@HS_TOKEN@";
rate_limited = false;
sender_localpart = "heisenbridge";
namespaces = {
users = [
{
regex = "@irc_.*";
exclusive = true;
}
{
regex = "@heisenbridge:.*";
exclusive = true;
}
];
aliases = [];
rooms = [];
};
});
# TODO(tlater): Starting with systemd 253 it will become possible
# to do the credential setup as part of ExecStartPre/preStart
# instead.
#
# This will also make it possible to actually set caps on the
# heisenbridge process using systemd, so that we can run the
# identd process.
execScript = pkgs.writeShellScript "heisenbridge" ''
cp ${registrationFile} "$RUNTIME_DIRECTORY/heisenbridge-registration.yaml"
chmod 600 $RUNTIME_DIRECTORY/heisenbridge-registration.yaml
${replaceSecretBin} '@AS_TOKEN@' "$CREDENTIALS_DIRECTORY/heisenbridge_as-token" "$RUNTIME_DIRECTORY/heisenbridge-registration.yaml"
${replaceSecretBin} '@HS_TOKEN@' "$CREDENTIALS_DIRECTORY/heisenbridge_hs-token" "$RUNTIME_DIRECTORY/heisenbridge-registration.yaml"
chmod 400 $RUNTIME_DIRECTORY/heisenbridge-registration.yaml
${pkgs.heisenbridge}/bin/heisenbridge \
--config $RUNTIME_DIRECTORY/heisenbridge-registration.yaml \
--owner @tlater:matrix.tlater.net \
'http://localhost:${toString cfg.settings.global.port}'
'';
in {
description = "Matrix<->IRC bridge";
wantedBy = ["multi-user.target"];
after = ["conduit.service"];
serviceConfig = {
Type = "simple";
LoadCredential = "heisenbridge:/run/secrets/heisenbridge";
ExecStart = execScript;
DynamicUser = true;
RuntimeDirectory = "heisenbridge";
RuntimeDirectoryMode = "0700";
RestrictNamespaces = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = ["AF_INET AF_INET6"];
LockPersonality = true;
RestrictRealtime = true;
ProtectProc = "invisible";
ProcSubset = "pid";
UMask = 0077;
# For the identd port
# CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
# AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
};
};
# Pass in the TURN secret via EnvironmentFile, not supported by
# upstream module currently.
#
# See also https://gitlab.com/famedly/conduit/-/issues/314
systemd.services.conduit.serviceConfig.EnvironmentFile = config.sops.secrets."turn/env".path;
services.coturn = {
enable = true;
no-cli = true;
use-auth-secret = true;
static-auth-secret-file = config.sops.secrets."turn/secret".path;
realm = turn-realm;
relay-ips = [
"116.202.158.55"
];
# SSL config
#
# TODO(tlater): Switch to letsencrypt once google fix:
# https://github.com/vector-im/element-android/issues/1533
pkey = config.sops.secrets."turn/ssl-key".path;
cert = config.sops.secrets."turn/ssl-cert".path;
# Based on suggestions from
# https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md
# and
# https://www.foxypossibilities.com/2018/05/19/setting-up-a-turn-sever-for-matrix-on-nixos/
no-tcp-relay = true;
secure-stun = true;
extraConfig = ''
# Deny various local IP ranges, see
# https://www.rtcsec.com/article/cve-2020-26262-bypass-of-coturns-access-control-protection/
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255 denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
# *Allow* any IP addresses that we explicitly set as relay IPs
${concatMapStringsSep "\n" (ip: "allowed-peer-ip=${ip}") config.services.coturn.relay-ips}
# Various other security settings
no-tlsv1
no-tlsv1_1
# Monitoring
prometheus
'';
};
services.nginx.virtualHosts."${domain}" = {
useACMEHost = "tlater.net";
listen = [
{
addr = "0.0.0.0";
port = 80;
}
{
addr = "[::0]";
port = 80;
}
{
addr = "0.0.0.0";
port = 443;
ssl = true;
}
{
addr = "[::0]";
port = 443;
ssl = true;
}
{
addr = "0.0.0.0";
port = 8448;
ssl = true;
}
{
addr = "[::0]";
port = 8448;
ssl = true;
}
];
forceSSL = true;
enableHSTS = true;
extraConfig = ''
merge_slashes off;
'';
locations = {
"/_matrix" = {
proxyPass = "http://${cfg.settings.global.address}:${toString cfg.settings.global.port}";
# Recommended by conduit
extraConfig = ''
proxy_buffering off;
'';
};
# Add Element X support
# TODO(tlater): Remove when no longer required: https://github.com/vector-im/element-x-android/issues/1085
"=/.well-known/matrix/client" = {
alias = pkgs.writeText "well-known-matrix-client" (builtins.toJSON {
"m.homeserver".base_url = "https://${domain}";
"org.matrix.msc3575.proxy".url = "https://${domain}";
});
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin "*";
'';
};
};
};
services.backups.conduit = {
user = "root";
paths = [
"/var/lib/private/matrix-conduit/"
];
# Other services store their data in conduit, so no other services
# need to be shut down currently.
pauseServices = ["conduit.service"];
};
}

View file

@ -1,182 +0,0 @@
{
pkgs,
config,
lib,
...
}:
let
inherit (lib.strings) concatMapStringsSep;
cfg = config.services.matrix-conduit;
domain = "matrix.${config.services.nginx.domain}";
turn-realm = "turn.${config.services.nginx.domain}";
in
{
imports = [
./heisenbridge.nix
./matrix-hookshot.nix
];
services = {
matrix-conduit = {
enable = true;
package = pkgs.matrix-continuwuity;
settings.global = {
address = "127.0.0.1";
server_name = domain;
new_user_displayname_suffix = "🦆";
allow_check_for_updates = true;
# Set up delegation: https://docs.conduit.rs/delegation.html#automatic-recommended
# This is primarily to make sliding sync work
well_known = {
client = "https://${domain}";
server = "${domain}:443";
};
turn_uris =
let
address = "${config.services.coturn.realm}:${toString config.services.coturn.listening-port}";
tls-address = "${config.services.coturn.realm}:${toString config.services.coturn.tls-listening-port}";
in
[
"turn:${address}?transport=udp"
"turn:${address}?transport=tcp"
"turns:${tls-address}?transport=udp"
"turns:${tls-address}?transport=tcp"
];
};
};
coturn = {
enable = true;
no-cli = true;
use-auth-secret = true;
static-auth-secret-file = config.sops.secrets."turn/secret".path;
realm = turn-realm;
relay-ips = [ "116.202.158.55" ];
# SSL config
pkey = "${config.security.acme.certs."tlater.net".directory}/key.pem";
cert = "${config.security.acme.certs."tlater.net".directory}/fullchain.pem";
# Based on suggestions from
# https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md
# and
# https://www.foxypossibilities.com/2018/05/19/setting-up-a-turn-sever-for-matrix-on-nixos/
no-tcp-relay = true;
secure-stun = true;
extraConfig = ''
# Deny various local IP ranges, see
# https://www.rtcsec.com/article/cve-2020-26262-bypass-of-coturns-access-control-protection/
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255 denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
# *Allow* any IP addresses that we explicitly set as relay IPs
${concatMapStringsSep "\n" (ip: "allowed-peer-ip=${ip}") config.services.coturn.relay-ips}
# Various other security settings
no-tlsv1
no-tlsv1_1
# Monitoring
prometheus
'';
};
nginx.virtualHosts."${domain}" = {
useACMEHost = "tlater.net";
listen = [
{
addr = "0.0.0.0";
port = 80;
}
{
addr = "[::0]";
port = 80;
}
{
addr = "0.0.0.0";
port = 443;
ssl = true;
}
{
addr = "[::0]";
port = 443;
ssl = true;
}
{
addr = "0.0.0.0";
port = 8448;
ssl = true;
}
{
addr = "[::0]";
port = 8448;
ssl = true;
}
];
forceSSL = true;
enableHSTS = true;
extraConfig = ''
merge_slashes off;
'';
locations = {
"/_matrix" = {
proxyPass = "http://${cfg.settings.global.address}:${toString cfg.settings.global.port}";
# Recommended by conduit
extraConfig = ''
proxy_buffering off;
'';
};
"/.well-known/matrix" = {
proxyPass = "http://${cfg.settings.global.address}:${toString cfg.settings.global.port}";
};
};
};
backups.conduit = {
user = "root";
paths = [ "/var/lib/private/matrix-conduit/" ];
# Other services store their data in conduit, so no other services
# need to be shut down currently.
pauseServices = [ "conduit.service" ];
};
};
systemd.services.conduit.serviceConfig = {
ExecStart = lib.mkForce "${config.services.matrix-conduit.package}/bin/conduwuit";
# Pass in the TURN secret via EnvironmentFile, not supported by
# upstream module currently.
#
# See also https://gitlab.com/famedly/conduit/-/issues/314
EnvironmentFile = config.sops.secrets."turn/env".path;
};
systemd.services.coturn.serviceConfig.SupplementaryGroups = [
config.security.acme.certs."tlater.net".group
];
}

View file

@ -1,78 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
conduitCfg = config.services.matrix-conduit;
matrixLib = pkgs.callPackage ./lib.nix { };
in
{
systemd.services.heisenbridge =
let
registration = matrixLib.writeRegistrationScript {
id = "heisenbridge";
url = "http://127.0.0.1:9898";
sender_localpart = "heisenbridge";
namespaces = {
users = [
{
regex = "@irc_.*";
exclusive = true;
}
{
regex = "@heisenbridge:.*";
exclusive = true;
}
];
aliases = [ ];
rooms = [ ];
};
};
in
{
description = "Matrix<->IRC bridge";
wantedBy = [ "multi-user.target" ];
after = [ "conduit.service" ];
serviceConfig = {
Type = "exec";
LoadCredential = "heisenbridge:/run/secrets/heisenbridge";
inherit (registration) ExecStartPre;
ExecStart = lib.concatStringsSep " " [
"${lib.getExe pkgs.heisenbridge}"
"--config \${RUNTIME_DIRECTORY}/heisenbridge-registration.yaml"
"--owner @tlater:matrix.tlater.net"
"http://localhost:${toString conduitCfg.settings.global.port}"
];
DynamicUser = true;
RuntimeDirectory = "heisenbridge";
RuntimeDirectoryMode = "0700";
RestrictNamespaces = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
LockPersonality = true;
RestrictRealtime = true;
ProtectProc = "invisible";
ProcSubset = "pid";
UMask = 77;
# For the identd port
# CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
# AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
};
};
}

View file

@ -1,67 +0,0 @@
{
lib,
writeShellScript,
formats,
replace-secret,
}:
let
replaceSecretBin = "${lib.getExe replace-secret}";
in
{
# Write a script that will set up the service's registration.yaml
# with secrets from systemd credentials.
#
# The credentials should be named `${id}_as-token` and
# `${id}_hs-token`.
#
# This registration file needs to be manually added to conduit by
# messaging the admin with the yaml file.
#
# TODO(tlater): Conduwuit seems to support a CLI interface for this,
# may want to migrate to that sometime.
writeRegistrationScript =
{
id, # Must be unique among all registered appservices/bots
url, # The URL on which the service listens
sender_localpart,
rate_limited ? false,
namespaces ? {
aliases = [ ];
rooms = [ ];
users = [ ];
},
extraSettings ? { },
# The location to place the file; assumes systemd runtime dir
runtimeRegistration ? "$RUNTIME_DIRECTORY/${id}-registration.yaml",
}:
let
registrationFile = (formats.yaml { }).generate "${id}-registration.yaml" (
{
inherit
id
url
sender_localpart
rate_limited
namespaces
;
as_token = "@AS_TOKEN@";
hs_token = "@HS_TOKEN@";
}
// extraSettings
);
in
{
inherit runtimeRegistration;
ExecStartPre = writeShellScript "${id}-registration-setup.sh" ''
cp -f ${registrationFile} "${runtimeRegistration}"
chmod 600 "${runtimeRegistration}"
# Write actual secrets into config
${replaceSecretBin} '@AS_TOKEN@' "$CREDENTIALS_DIRECTORY/${id}_as-token" "${runtimeRegistration}"
${replaceSecretBin} '@HS_TOKEN@' "$CREDENTIALS_DIRECTORY/${id}_hs-token" "${runtimeRegistration}"
chmod 400 "${runtimeRegistration}"
'';
};
}

View file

@ -1,166 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
matrixLib = pkgs.callPackage ./lib.nix { };
cfg = config.services.matrix-hookshot;
conduitCfg = config.services.matrix-conduit;
domain = conduitCfg.settings.global.server_name;
registration = matrixLib.writeRegistrationScript {
id = "matrix-hookshot";
url = "http://127.0.0.1:9993";
sender_localpart = "hookshot";
namespaces = {
aliases = [ ];
rooms = [ ];
users = [
{
regex = "@${cfg.settings.generic.userIdPrefix}.*:${domain}";
exclusive = true;
}
];
};
# Encryption support
# TODO(tlater): Enable when
# https://github.com/matrix-org/matrix-hookshot/issues/1060 is
# fixed
# extraSettings = {
# "de.sorunome.msc2409.push_ephemeral" = true;
# push_ephemeral = true;
# "org.matrix.msc3202" = true;
# };
runtimeRegistration = "${cfg.registrationFile}";
};
in
{
# users = {
# users.matrix-hookshot = {
# home = "/run/matrix-hookshot";
# group = "matrix-hookshot";
# isSystemUser = true;
# };
# groups.matrix-hookshot = { };
# };
systemd.services.matrix-hookshot = {
serviceConfig = {
Type = lib.mkForce "exec";
LoadCredential = "matrix-hookshot:/run/secrets/matrix-hookshot";
inherit (registration) ExecStartPre;
# Some library in matrix-hookshot wants a home directory
Environment = [ "HOME=/run/matrix-hookshot" ];
# User = "matrix-hookshot";
DynamicUser = true;
StateDirectory = "matrix-hookshot";
RuntimeDirectory = "matrix-hookshot";
RuntimeDirectoryMode = "0700";
RestrictNamespaces = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
# "AF_UNIX"
"AF_INET"
"AF_INET6"
];
LockPersonality = true;
RestrictRealtime = true;
ProtectProc = "invisible";
ProcSubset = "pid";
UMask = 77;
};
};
# services.redis.servers.matrix-hookshot = {
# enable = true;
# user = "matrix-hookshot";
# };
services.matrix-hookshot = {
enable = true;
serviceDependencies = [ "conduit.service" ];
registrationFile = "/run/matrix-hookshot/registration.yaml";
settings = {
bridge = {
inherit domain;
url = "http://localhost:${toString conduitCfg.settings.global.port}";
mediaUrl = conduitCfg.settings.global.well_known.client;
port = 9993;
bindAddress = "127.0.0.1";
};
bot.displayname = "Hookshot";
# cache.redisUri = "redis://${config.services.redis.servers.matrix-hookshot.unixSocket}";
generic = {
enabled = true;
outbound = false;
# Only allow webhooks from localhost for the moment
urlPrefix = "http://127.0.0.1:9000/webhook";
userIdPrefix = "_webhooks_";
allowJsTransformationFunctions = true;
};
# TODO(tlater): Enable when
# https://github.com/matrix-org/matrix-hookshot/issues/1060 is
# fixed
# encryption.storagePath = "/var/lib/matrix-hookshot/cryptostore";
permissions = [
{
actor = "matrix.tlater.net";
services = [
{
service = "*";
level = "notifications";
}
];
}
{
actor = "@tlater:matrix.tlater.net";
services = [
{
service = "*";
level = "admin";
}
];
}
];
listeners = [
{
port = 9000;
resources = [ "webhooks" ];
}
{
port = 9001;
resources = [ "metrics" ];
}
];
metrics.enabled = true;
};
};
}

View file

@ -1,82 +0,0 @@
{
pkgs,
config,
lib,
...
}:
{
security.crowdsec = {
enable = true;
parserWhitelist = [ "10.45.249.2" ];
extraGroups = [
"systemd-journal"
"nginx"
];
acquisitions = [
{
source = "journalctl";
labels.type = "syslog";
journalctl_filter = [ "SYSLOG_IDENTIFIER=Nextcloud" ];
}
{
source = "journalctl";
labels.type = "syslog";
journalctl_filter = [ "SYSLOG_IDENTIFIER=sshd-session" ];
}
{
labels.type = "nginx";
filenames =
[ "/var/log/nginx/*.log" ]
++ lib.mapAttrsToList (
vHost: _: "/var/log/nginx/${vHost}/access.log"
) config.services.nginx.virtualHosts;
}
];
remediationComponents.firewallBouncer = {
enable = true;
settings.prometheus = {
enabled = true;
listen_addr = "127.0.0.1";
listen_port = "60601";
};
};
};
# Add whitelists for matrix
systemd.tmpfiles.settings."10-matrix" =
let
stateDir = config.security.crowdsec.stateDirectory;
in
{
"${stateDir}/config/postoverflows".d = {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
"${stateDir}/config/postoverflows/s01-whitelist".d = {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
"${stateDir}/config/postoverflows/s01-whitelist/matrix-whitelist.yaml"."L+".argument =
((pkgs.formats.yaml { }).generate "crowdsec-matrix-whitelist.yaml" {
name = "tetsumaki/matrix";
description = "custom matrix whitelist";
whitelist = {
reason = "whitelist false positive for matrix";
expression = [
"evt.Overflow.Alert.Events[0].GetMeta('target_fqdn') == '${config.services.matrix-conduit.settings.global.server_name}'"
"evt.Overflow.Alert.GetScenario() in ['crowdsecurity/http-probing', 'crowdsecurity/http-crawl-non_statics']"
];
};
}).outPath;
};
}

View file

@ -0,0 +1,42 @@
{pkgs, ...}: {
services.fail2ban = {
enable = true;
extraPackages = [pkgs.ipset];
banaction = "iptables-ipset-proto6-allports";
bantime-increment.enable = true;
jails = {
nginx-botsearch = ''
enabled = true
logpath = /var/log/nginx/access.log
'';
};
ignoreIP = [
"127.0.0.0/8"
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/16"
];
};
# Allow metrics services to connect to the socket as well
users.groups.fail2ban = {};
systemd.services.fail2ban.serviceConfig = {
ExecStartPost =
"+"
+ (pkgs.writeShellScript "fail2ban-post-start" ''
while ! [ -S /var/run/fail2ban/fail2ban.sock ]; do
sleep 1
done
while ! ${pkgs.netcat}/bin/nc -zU /var/run/fail2ban/fail2ban.sock; do
sleep 1
done
${pkgs.coreutils}/bin/chown root:fail2ban /var/run/fail2ban /var/run/fail2ban/fail2ban.sock
${pkgs.coreutils}/bin/chmod 660 /var/run/fail2ban/fail2ban.sock
${pkgs.coreutils}/bin/chmod 710 /var/run/fail2ban
'');
};
}

View file

@ -2,48 +2,42 @@
lib, lib,
config, config,
flake-inputs, flake-inputs,
pkgs,
... ...
}: }: let
let
domain = "foundryvtt.${config.services.nginx.domain}"; domain = "foundryvtt.${config.services.nginx.domain}";
in in {
{ imports = [flake-inputs.foundryvtt.nixosModules.foundryvtt];
imports = [ flake-inputs.foundryvtt.nixosModules.foundryvtt ];
services = { services.foundryvtt = {
foundryvtt = { enable = true;
enable = true; hostName = domain;
hostName = domain; minifyStaticFiles = true;
minifyStaticFiles = true; proxySSL = true;
proxySSL = true; proxyPort = 443;
proxyPort = 443;
package = flake-inputs.foundryvtt.packages.${pkgs.system}.foundryvtt_13;
};
nginx.virtualHosts."${domain}" =
let
inherit (config.services.foundryvtt) port;
in
{
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
locations."/" = {
proxyWebsockets = true;
proxyPass = "http://localhost:${toString port}";
};
};
backups.foundryvtt = {
user = "foundryvtt";
paths = [ config.services.foundryvtt.dataDir ];
pauseServices = [ "foundryvtt.service" ];
};
}; };
# Want to start it manually when I need it, not have it constantly # Want to start it manually when I need it, not have it constantly
# running # running
systemd.services.foundryvtt.wantedBy = lib.mkForce [ ]; systemd.services.foundryvtt.wantedBy = lib.mkForce [];
services.nginx.virtualHosts."${domain}" = let
inherit (config.services.foundryvtt) port;
in {
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
locations."/" = {
proxyWebsockets = true;
proxyPass = "http://localhost:${toString port}";
};
};
services.backups.foundryvtt = {
user = "foundryvtt";
paths = [
config.services.foundryvtt.dataDir
];
pauseServices = ["foundryvtt.service"];
};
} }

View file

@ -3,81 +3,93 @@
config, config,
lib, lib,
... ...
}: }: let
let
domain = "gitea.${config.services.nginx.domain}"; domain = "gitea.${config.services.nginx.domain}";
in in {
{ services.forgejo = {
services = { enable = true;
forgejo = { database.type = "postgres";
enable = true;
database.type = "postgres";
settings = { settings = {
server = { server = {
DOMAIN = domain; DOMAIN = domain;
HTTP_ADDR = "127.0.0.1"; HTTP_ADDR = "127.0.0.1";
ROOT_URL = "https://${domain}/"; ROOT_URL = "https://${domain}/";
SSH_PORT = 2222; SSH_PORT = 2222;
};
metrics = {
ENABLED = true;
TOKEN = "#metricstoken#";
};
service.DISABLE_REGISTRATION = true;
session.COOKIE_SECURE = true;
};
};
# Set up SSL
nginx.virtualHosts."${domain}" =
let
httpAddress = config.services.forgejo.settings.server.HTTP_ADDR;
httpPort = config.services.forgejo.settings.server.HTTP_PORT;
in
{
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
locations."/".proxyPass = "http://${httpAddress}:${toString httpPort}";
locations."/metrics" = {
extraConfig = ''
access_log off;
allow 127.0.0.1;
${lib.optionalString config.networking.enableIPv6 "allow ::1;"}
deny all;
'';
};
}; };
backups.forgejo = { metrics = {
user = "forgejo"; ENABLED = true;
paths = [ TOKEN = "#metricstoken#";
"/var/lib/forgejo/forgejo-db.sql"
"/var/lib/forgejo/repositories/"
"/var/lib/forgejo/data/"
"/var/lib/forgejo/custom/"
# Conf is backed up via nix
];
preparation = {
packages = [ config.services.postgresql.package ];
text = "pg_dump ${config.services.forgejo.database.name} --file=/var/lib/forgejo/forgejo-db.sql";
}; };
cleanup = { service.DISABLE_REGISTRATION = true;
packages = [ pkgs.coreutils ]; session.COOKIE_SECURE = true;
text = "rm /var/lib/forgejo/forgejo-db.sql";
};
pauseServices = [ "forgejo.service" ];
}; };
}; };
systemd.services.forgejo.serviceConfig.ExecStartPre = systemd.services.forgejo.serviceConfig.ExecStartPre = let
let replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; secretPath = config.sops.secrets."forgejo/metrics-token".path;
secretPath = config.sops.secrets."forgejo/metrics-token".path; runConfig = "${config.services.forgejo.customDir}/conf/app.ini";
runConfig = "${config.services.forgejo.customDir}/conf/app.ini"; in [
in "+${replaceSecretBin} '#metricstoken#' '${secretPath}' '${runConfig}'"
[ "+${replaceSecretBin} '#metricstoken#' '${secretPath}' '${runConfig}'" ]; ];
# Set up SSL
services.nginx.virtualHosts."${domain}" = let
httpAddress = config.services.forgejo.settings.server.HTTP_ADDR;
httpPort = config.services.forgejo.settings.server.HTTP_PORT;
in {
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
locations."/".proxyPass = "http://${httpAddress}:${toString httpPort}";
locations."/metrics" = {
extraConfig = ''
access_log off;
allow 127.0.0.1;
${lib.optionalString config.networking.enableIPv6 "allow ::1;"}
deny all;
'';
};
};
# Block repeated failed login attempts
#
# TODO(tlater): Update this - we switched to forgejo, who knows what
# the new matches are.
# environment.etc = {
# "fail2ban/filter.d/gitea.conf".text = ''
# [Definition]
# failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
# journalmatch = _SYSTEMD_UNIT=forgejo.service + _COMM=forgejo + SYSLOG_IDENTIFIER=forgejo
# '';
# };
# services.fail2ban.jails = {
# gitea = ''
# enabled = true
# '';
# };
services.backups.forgejo = {
user = "forgejo";
paths = [
"/var/lib/forgejo/forgejo-db.sql"
"/var/lib/forgejo/repositories/"
"/var/lib/forgejo/data/"
"/var/lib/forgejo/custom/"
# Conf is backed up via nix
];
preparation = {
packages = [config.services.postgresql.package];
text = "pg_dump ${config.services.forgejo.database.name} --file=/var/lib/forgejo/forgejo-db.sql";
};
cleanup = {
packages = [pkgs.coreutils];
text = "rm /var/lib/forgejo/forgejo-db.sql";
};
pauseServices = ["forgejo.service"];
};
} }

View file

@ -1,67 +0,0 @@
{
pkgs,
config,
lib,
...
}:
let
hostName = "immich.${config.services.nginx.domain}";
in
{
services = {
immich = {
enable = true;
settings.server.externalDomain = "https://${hostName}";
environment.IMMICH_TELEMETRY_INCLUDE = "all";
};
nginx.virtualHosts.${hostName} =
let
local = "http://${config.services.immich.host}:${toString config.services.immich.port}";
in
{
forceSSL = true;
useACMEHost = "tlater.net";
enableHSTS = true;
locations."/" = {
proxyPass = local;
proxyWebsockets = true;
};
locations."/metrics" = {
extraConfig = ''
access_log off;
allow 127.0.0.1;
${lib.optionalString config.networking.enableIPv6 "allow ::1;"}
deny all;
'';
};
};
backups.immich =
let
db-dump = "${config.services.immich.mediaLocation}/immich-db.sql";
in
{
user = "immich";
paths = [ config.services.immich.mediaLocation ];
preparation = {
packages = [ config.services.postgresql.package ];
text = ''
pg_dump ${config.services.immich.database.name} --clean --if-exists --file=${db-dump}
'';
};
cleanup = {
packages = [ pkgs.coreutils ];
text = "rm ${db-dump}";
};
pauseServices = [
"immich-server.service"
"immich-machine-learning.service"
];
};
};
}

View file

@ -5,6 +5,5 @@
./exporters.nix ./exporters.nix
./grafana.nix ./grafana.nix
./victoriametrics.nix ./victoriametrics.nix
./victorialogs.nix
]; ];
} }

View file

@ -3,49 +3,23 @@
pkgs, pkgs,
lib, lib,
... ...
}: }: let
let yaml = pkgs.formats.yaml {};
yaml = pkgs.formats.yaml { }; in {
in
{
services.prometheus = { services.prometheus = {
exporters = { exporters = {
blackbox = {
enable = true;
listenAddress = "127.0.0.1";
configFile = yaml.generate "blackbox.yaml" {
modules = {
http_2xx = {
prober = "http";
timeout = "5s";
http.preferred_ip_protocol = "ip4";
};
turn_server = {
prober = "tcp";
timeout = "5s";
tcp = {
preferred_ip_protocol = "ip4";
source_ip_address = "116.202.158.55";
tls = true;
};
};
};
};
};
# Periodically check domain registration status # Periodically check domain registration status
domain = { domain = {
enable = true; enable = true;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
extraFlags = extraFlags = let
let conf.domains = [
conf.domains = [ "tlater.net"
"tlater.net" "tlater.com"
"tlater.com" ];
]; in [
in "--config=${yaml.generate "domains.yml" conf}"
[ "--config=${yaml.generate "domains.yml" conf}" ]; ];
}; };
# System statistics # System statistics
@ -74,29 +48,54 @@ in
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
group = "nginx"; group = "nginx";
settings.namespaces = lib.mapAttrsToList (name: _: { settings.namespaces =
inherit name; lib.mapAttrsToList (name: virtualHost: {
metrics_override.prefix = "nginxlog"; inherit name;
namespace_label = "vhost"; metrics_override.prefix = "nginxlog";
namespace_label = "vhost";
format = lib.concatStringsSep " " [ format = lib.concatStringsSep " " [
"$remote_addr - $remote_user [$time_local]" "$remote_addr - $remote_user [$time_local]"
''"$request" $status $body_bytes_sent'' ''"$request" $status $body_bytes_sent''
''"$http_referer" "$http_user_agent"'' ''"$http_referer" "$http_user_agent"''
''rt=$request_time uct="$upstream_connect_time"'' ''rt=$request_time uct="$upstream_connect_time"''
''uht="$upstream_header_time" urt="$upstream_response_time"'' ''uht="$upstream_header_time" urt="$upstream_response_time"''
]; ];
source.files = [ "/var/log/nginx/${name}/access.log" ]; source.files = [
}) config.services.nginx.virtualHosts; "/var/log/nginx/${name}/access.log"
];
})
config.services.nginx.virtualHosts;
};
};
extraExporters = {
fail2ban = let
cfg = config.services.prometheus.extraExporters.fail2ban;
in {
port = 9191;
serviceOpts = {
after = ["fail2ban.service"];
requires = ["fail2ban.service"];
serviceConfig = {
Group = "fail2ban";
RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
ExecStart = lib.concatStringsSep " " [
"${pkgs.local.prometheus-fail2ban-exporter}/bin/fail2ban-prometheus-exporter"
"--collector.f2b.socket=/var/run/fail2ban/fail2ban.sock"
"--web.listen-address='${cfg.listenAddress}:${toString cfg.port}'"
"--collector.f2b.exit-on-socket-connection-error=true"
];
};
};
}; };
}; };
# TODO(tlater): # TODO(tlater):
# - wireguard (?) # - wireguard (?)
# - postgres (?) # - postgres (?)
# - blackbox (?) (curl to see if http and similar is up)
# - ssl_exporter (?) # - ssl_exporter (?)
}; };
services.dbus.implementation = "broker";
} }

View file

@ -1,15 +1,10 @@
{ pkgs, config, ... }: {config, ...}: let
let
domain = "metrics.${config.services.nginx.domain}"; domain = "metrics.${config.services.nginx.domain}";
in in {
{
services.grafana = { services.grafana = {
enable = true; enable = true;
settings = { settings = {
server = { server.http_port = 3001; # Default overlaps with gitea
http_port = 3001; # Default overlaps with gitea
root_url = "https://metrics.tlater.net";
};
security = { security = {
admin_user = "tlater"; admin_user = "tlater";
@ -28,11 +23,6 @@ in
}; };
}; };
declarativePlugins = [
pkgs.grafanaPlugins.victoriametrics-metrics-datasource
pkgs.grafanaPlugins.victoriametrics-logs-datasource
];
provision = { provision = {
enable = true; enable = true;
@ -40,16 +30,7 @@ in
{ {
name = "Victoriametrics - tlater.net"; name = "Victoriametrics - tlater.net";
url = "http://localhost:8428"; url = "http://localhost:8428";
type = "victoriametrics-metrics-datasource"; type = "prometheus";
access = "proxy";
isDefault = true;
}
{
name = "Victorialogs - tlater.net";
url = "http://${config.services.victorialogs.bindAddress}";
type = "victoriametrics-logs-datasource";
access = "proxy";
} }
]; ];
}; };
@ -59,12 +40,7 @@ in
forceSSL = true; forceSSL = true;
useACMEHost = "tlater.net"; useACMEHost = "tlater.net";
enableHSTS = true; enableHSTS = true;
locations = { enableAuthorization = true;
"/".proxyPass = "http://localhost:${toString config.services.grafana.settings.server.http_port}"; locations."/".proxyPass = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
"/api/live" = {
proxyWebsockets = true;
proxyPass = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
};
};
}; };
} }

View file

@ -3,235 +3,202 @@
config, config,
lib, lib,
... ...
}: }: let
let
inherit (lib) types mkOption mkDefault; inherit (lib) types mkOption mkDefault;
yaml = pkgs.formats.yaml { }; yaml = pkgs.formats.yaml {};
in in {
{
options = { options = {
services.prometheus = { services.prometheus = {
extraExporters = mkOption { extraExporters = mkOption {
default = { }; type = types.attrsOf (types.submodule {
type = types.attrsOf ( options = {
types.submodule { port = mkOption {
options = { type = types.int;
port = mkOption { description = "The port on which this exporter listens.";
type = types.int;
description = "The port on which this exporter listens.";
};
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
serviceOpts = mkOption {
type = types.attrs;
description = "An attrset to be merged with the exporter's systemd service.";
};
}; };
} listenAddress = mkOption {
); type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
serviceOpts = mkOption {
type = types.attrs;
description = "An attrset to be merged with the exporter's systemd service.";
};
};
});
}; };
}; };
services.victoriametrics.scrapeConfigs = mkOption { services.victoriametrics.scrapeConfigs = mkOption {
type = types.attrsOf ( type = types.attrsOf (types.submodule ({
types.submodule ( name,
{ name, ... }: self,
{ ...
options = { }: {
job_name = mkOption { options = {
type = types.str; job_name = mkOption {
default = name; type = types.str;
default = name;
};
extraSettings = mkOption {
type = types.anything;
description = ''
Other settings to set for this scrape config.
'';
default = {};
};
targets = mkOption {
type = types.listOf types.str;
description = lib.mdDoc ''
Addresses scrape targets for this config listen on.
Shortcut for `static_configs = lib.singleton {targets = [<targets>];}`
'';
default = [];
};
static_configs = mkOption {
default = [];
type = types.listOf (types.submodule {
options = {
targets = mkOption {
type = types.listOf types.str;
description = lib.mdDoc ''
The addresses scrape targets for this config listen on.
Must in `listenAddress:port` format.
'';
};
labels = mkOption {
type = types.attrsOf types.str;
description = lib.mdDoc ''
Labels to apply to all targets defined for this static config.
'';
default = {};
};
}; };
});
extraSettings = mkOption { };
inherit (pkgs.formats.yaml { }) type; };
description = '' }));
Other settings to set for this scrape config.
'';
default = { };
};
targets = mkOption {
type = types.listOf types.str;
description = lib.mdDoc ''
Addresses scrape targets for this config listen on.
Shortcut for `static_configs = lib.singleton {targets = [<targets>];}`
'';
default = [ ];
};
static_configs = mkOption {
default = [ ];
type = types.listOf (
types.submodule {
options = {
targets = mkOption {
type = types.listOf types.str;
description = lib.mdDoc ''
The addresses scrape targets for this config listen on.
Must in `listenAddress:port` format.
'';
};
labels = mkOption {
type = types.attrsOf types.str;
description = lib.mdDoc ''
Labels to apply to all targets defined for this static config.
'';
default = { };
};
};
}
);
};
};
}
)
);
}; };
}; };
config = { config = {
systemd.services = lib.mkMerge [ systemd.services = lib.mkMerge [
(lib.mapAttrs' ( (lib.mapAttrs' (name: exporter:
name: exporter: lib.nameValuePair "prometheus-${name}-exporter" (lib.mkMerge [
lib.nameValuePair "prometheus-${name}-exporter" ( {
lib.mkMerge [ # Shamelessly copied from upstream because the upstream
{ # module is an intractable mess
# Shamelessly copied from upstream because the upstream wantedBy = ["multi-user.target"];
# module is an intractable mess after = ["network.target"];
wantedBy = [ "multi-user.target" ]; serviceConfig.Restart = mkDefault "always";
after = [ "network.target" ]; serviceConfig.PrivateTmp = mkDefault true;
serviceConfig = { serviceConfig.WorkingDirectory = mkDefault /tmp;
Restart = mkDefault "always"; serviceConfig.DynamicUser = mkDefault true;
PrivateTmp = mkDefault true; # Hardening
WorkingDirectory = mkDefault /tmp; serviceConfig.CapabilityBoundingSet = mkDefault [""];
DynamicUser = mkDefault true; serviceConfig.DeviceAllow = [""];
# Hardening serviceConfig.LockPersonality = true;
CapabilityBoundingSet = mkDefault [ "" ]; serviceConfig.MemoryDenyWriteExecute = true;
DeviceAllow = [ "" ]; serviceConfig.NoNewPrivileges = true;
LockPersonality = true; serviceConfig.PrivateDevices = mkDefault true;
MemoryDenyWriteExecute = true; serviceConfig.ProtectClock = mkDefault true;
NoNewPrivileges = true; serviceConfig.ProtectControlGroups = true;
PrivateDevices = mkDefault true; serviceConfig.ProtectHome = true;
ProtectClock = mkDefault true; serviceConfig.ProtectHostname = true;
ProtectControlGroups = true; serviceConfig.ProtectKernelLogs = true;
ProtectHome = true; serviceConfig.ProtectKernelModules = true;
ProtectHostname = true; serviceConfig.ProtectKernelTunables = true;
ProtectKernelLogs = true; serviceConfig.ProtectSystem = mkDefault "strict";
ProtectKernelModules = true; serviceConfig.RemoveIPC = true;
ProtectKernelTunables = true; serviceConfig.RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
ProtectSystem = mkDefault "strict"; serviceConfig.RestrictNamespaces = true;
RemoveIPC = true; serviceConfig.RestrictRealtime = true;
RestrictAddressFamilies = [ serviceConfig.RestrictSUIDSGID = true;
"AF_INET" serviceConfig.SystemCallArchitectures = "native";
"AF_INET6" serviceConfig.UMask = "0077";
]; }
RestrictNamespaces = true; exporter.serviceOpts
RestrictRealtime = true; ]))
RestrictSUIDSGID = true; config.services.prometheus.extraExporters)
SystemCallArchitectures = "native";
UMask = "0077";
};
}
exporter.serviceOpts
]
)
) config.services.prometheus.extraExporters)
{ {
vmagent-scrape-exporters = vmagent-scrape-exporters = let
let listenAddress = config.services.victoriametrics.listenAddress;
inherit (config.services.victoriametrics) listenAddress; vmAddr = (lib.optionalString (lib.hasPrefix ":" listenAddress) "127.0.0.1") + listenAddress;
vmAddr = (lib.optionalString (lib.hasPrefix ":" listenAddress) "127.0.0.1") + listenAddress; promscrape = yaml.generate "prometheus.yml" {
promscrape = yaml.generate "prometheus.yml" { scrape_configs = lib.mapAttrsToList (_: scrape:
scrape_configs = lib.mapAttrsToList ( lib.recursiveUpdate {
_: scrape: inherit (scrape) job_name;
lib.recursiveUpdate { static_configs =
inherit (scrape) job_name; scrape.static_configs
static_configs = ++ lib.optional (scrape.targets != []) {targets = scrape.targets;};
scrape.static_configs }
++ lib.optional (scrape.targets != [ ]) { inherit (scrape) targets; }; scrape.extraSettings)
} scrape.extraSettings config.services.victoriametrics.scrapeConfigs;
) config.services.victoriametrics.scrapeConfigs;
};
in
{
enable = true;
path = [ pkgs.victoriametrics ];
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"victoriametrics.service"
];
serviceConfig = {
ExecStart = [
(lib.concatStringsSep " " [
"${pkgs.victoriametrics}/bin/vmagent"
"-promscrape.config=${promscrape}"
"-remoteWrite.url=http://${vmAddr}/api/v1/write"
"-remoteWrite.tmpDataPath=%t/vmagent"
])
];
SupplementaryGroups = "metrics";
DynamicUser = true;
RuntimeDirectory = "vmagent";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0077";
};
}; };
in {
enable = true;
path = [pkgs.victoriametrics];
wantedBy = ["multi-user.target"];
after = ["network.target" "victoriametrics.service"];
serviceConfig = {
ExecStart = [
(lib.concatStringsSep " " [
"${pkgs.victoriametrics}/bin/vmagent"
"-promscrape.config=${promscrape}"
"-remoteWrite.url=http://${vmAddr}/api/v1/write"
"-remoteWrite.tmpDataPath=%t/vmagent"
])
];
SupplementaryGroups = "metrics";
DynamicUser = true;
RuntimeDirectory = "vmagent";
CapabilityBoundingSet = [""];
DeviceAllow = [""];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0077";
};
};
} }
]; ];
users.groups.metrics = { }; users.groups.metrics = {};
services.victoriametrics.scrapeConfigs = services.victoriametrics.scrapeConfigs = let
let allExporters =
allExporters = lib.mapAttrs (_: exporter: { inherit (exporter) listenAddress port; }) ( lib.mapAttrs (name: exporter: {
(lib.filterAttrs ( inherit (exporter) listenAddress port;
name: exporter: }) ((lib.filterAttrs (_: exporter: builtins.isAttrs exporter && exporter.enable)
# A bunch of deprecated exporters that need to be ignored config.services.prometheus.exporters)
!(builtins.elem name [ // config.services.prometheus.extraExporters);
"blackbox" in
"minio"
"tor"
"unifi-poller"
])
&& builtins.isAttrs exporter
&& exporter.enable
) config.services.prometheus.exporters)
// config.services.prometheus.extraExporters
);
in
lib.mapAttrs (_: exporter: { lib.mapAttrs (_: exporter: {
targets = [ "${exporter.listenAddress}:${toString exporter.port}" ]; targets = ["${exporter.listenAddress}:${toString exporter.port}"];
}) allExporters; })
allExporters;
}; };
} }

View file

@ -1,30 +0,0 @@
{ config, lib, ... }:
let
cfg = config.services.victorialogs;
in
{
options.services.victorialogs.bindAddress = lib.mkOption {
readOnly = true;
type = lib.types.str;
description = ''
Final address on which victorialogs listens.
'';
};
config = {
services.victorialogs = {
enable = true;
bindAddress =
(lib.optionalString (lib.hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
};
services.journald.upload = {
enable = true;
settings.Upload = {
URL = "http://${cfg.bindAddress}/insert/journald";
NetworkTimeoutSec = "20s";
};
};
systemd.services."systemd-journal-upload".after = [ "victorialogs.service" ];
};
}

View file

@ -1,99 +1,16 @@
{ config, lib, ... }: {config, ...}: {
let
blackbox_host = config.services.prometheus.exporters.blackbox.listenAddress;
blackbox_port = config.services.prometheus.exporters.blackbox.port;
in
{
config.services.victoriametrics = { config.services.victoriametrics = {
enable = true; enable = true;
extraOptions = [ "-storage.minFreeDiskSpaceBytes=5GB" ]; extraOptions = [
"-storage.minFreeDiskSpaceBytes=5GB"
];
scrapeConfigs = { scrapeConfigs = {
forgejo = { forgejo = {
targets = [ "127.0.0.1:${toString config.services.forgejo.settings.server.HTTP_PORT}" ]; targets = ["127.0.0.1:${toString config.services.forgejo.settings.server.HTTP_PORT}"];
extraSettings.authorization.credentials_file = config.sops.secrets."forgejo/metrics-token".path; extraSettings.authorization.credentials_file = config.sops.secrets."forgejo/metrics-token".path;
}; };
coturn.targets = ["127.0.0.1:9641"];
blackbox = {
static_configs = lib.singleton {
targets = lib.mapAttrsToList (vHost: _: "https://${vHost}") config.services.nginx.virtualHosts;
};
extraSettings = {
metrics_path = "/probe";
params.module = [ "http_2xx" ];
relabel_configs = [
{
source_labels = [ "__address__" ];
target_label = "__param_target";
}
{
source_labels = [ "__param_target" ];
target_label = "instance";
}
{
target_label = "__address__";
replacement = "${blackbox_host}:${toString blackbox_port}";
}
];
};
};
blackbox_turn = {
targets = [ "turn.tlater.net:${toString config.services.coturn.tls-listening-port}" ];
extraSettings = {
metrics_path = "/probe";
params.module = [ "turn_server" ];
relabel_configs = [
{
source_labels = [ "__address__" ];
target_label = "__param_target";
}
{
source_labels = [ "__param_target" ];
target_label = "instance";
}
{
target_label = "__address__";
replacement = "${blackbox_host}:${toString blackbox_port}";
}
];
};
};
blackbox_exporter.targets = [ "${blackbox_host}:${toString blackbox_port}" ];
coturn.targets = [ "127.0.0.1:9641" ];
crowdsec.targets =
let
address = config.security.crowdsec.settings.prometheus.listen_addr;
port = config.security.crowdsec.settings.prometheus.listen_port;
in
[ "${address}:${toString port}" ];
csFirewallBouncer.targets =
let
address =
config.security.crowdsec.remediationComponents.firewallBouncer.settings.prometheus.listen_addr;
port =
config.security.crowdsec.remediationComponents.firewallBouncer.settings.prometheus.listen_port;
in
[ "${address}:${toString port}" ];
immich.targets = [
"127.0.0.1:8081"
"127.0.0.1:8082"
];
# Configured in the hookshot listeners, but it's hard to filter
# the correct values out of that config.
matrixHookshot.targets = [ "127.0.0.1:9001" ];
victorialogs.targets = [ config.services.victorialogs.bindAddress ];
}; };
}; };
} }

View file

@ -1,103 +1,105 @@
{ {
pkgs, pkgs,
config, config,
lib,
... ...
}: }: let
let # Update pending on rewrite of nextcloud news, though there is an
nextcloud = pkgs.nextcloud31; # alpha to switch to if it becomes necessary:
# https://github.com/nextcloud/news/issues/2610
nextcloud = pkgs.nextcloud27;
hostName = "nextcloud.${config.services.nginx.domain}"; hostName = "nextcloud.${config.services.nginx.domain}";
in in {
{ services.nextcloud = {
services = { inherit hostName;
nextcloud = {
inherit hostName;
package = nextcloud; package = nextcloud;
phpPackage = lib.mkForce ( enable = true;
pkgs.php.override { maxUploadSize = "2G";
packageOverrides = _: prev: { https = true;
extensions = prev.extensions // {
pgsql = prev.extensions.pgsql.overrideAttrs (_: {
configureFlags = [ "--with-pgsql=${lib.getDev config.services.postgresql.package.pg_config}" ];
});
pdo_pgsql = prev.extensions.pdo_pgsql.overrideAttrs (_: {
configureFlags = [ "--with-pdo-pgsql=${lib.getDev config.services.postgresql.package.pg_config}" ];
});
};
};
}
);
enable = true;
maxUploadSize = "2G";
https = true;
configureRedis = true; configureRedis = true;
config = { config = {
dbtype = "pgsql"; dbtype = "pgsql";
dbhost = "/run/postgresql"; dbhost = "/run/postgresql";
adminuser = "tlater"; adminuser = "tlater";
adminpassFile = config.sops.secrets."nextcloud/tlater".path; adminpassFile = config.sops.secrets."nextcloud/tlater".path;
};
settings = {
default_phone_region = "AT";
overwriteprotocol = "https";
};
phpOptions = {
"opcache.interned_strings_buffer" = "16";
};
extraApps = {
inherit (config.services.nextcloud.package.packages.apps)
calendar
contacts
cookbook
news
;
};
}; };
# Set up SSL settings = {
nginx.virtualHosts."${hostName}" = { default_phone_region = "AT";
forceSSL = true; overwriteprotocol = "https";
useACMEHost = "tlater.net";
# The upstream module already adds HSTS
}; };
backups.nextcloud = { phpOptions = {
user = "nextcloud"; "opcache.interned_strings_buffer" = "16";
paths = [ };
"/var/lib/nextcloud/nextcloud-db.sql"
"/var/lib/nextcloud/data/" extraApps = {
"/var/lib/nextcloud/config/config.php" inherit (pkgs.local) bookmarks calendar contacts cookbook news notes;
];
preparation = {
packages = [
config.services.postgresql.package
config.services.nextcloud.occ
];
text = ''
nextcloud-occ maintenance:mode --on
pg_dump ${config.services.nextcloud.config.dbname} --file=/var/lib/nextcloud/nextcloud-db.sql
'';
};
cleanup = {
packages = [
pkgs.coreutils
config.services.nextcloud.occ
];
text = ''
rm /var/lib/nextcloud/nextcloud-db.sql
nextcloud-occ maintenance:mode --off
'';
};
}; };
}; };
# Ensure that this service doesn't start before postgres is ready # Ensure that this service doesn't start before postgres is ready
systemd.services.nextcloud-setup.after = [ "postgresql.service" ]; systemd.services.nextcloud-setup.after = ["postgresql.service"];
# Set up SSL
services.nginx.virtualHosts."${hostName}" = {
forceSSL = true;
useACMEHost = "tlater.net";
# The upstream module already adds HSTS
};
# Block repeated failed login attempts
environment.etc = {
"fail2ban/filter.d/nextcloud.conf".text = ''
[Definition]
_groupsre = (?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)
failregex = \{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Trusted domain error.
datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"
journalmatch = SYSLOG_IDENTIFIER=Nextcloud
'';
};
services.fail2ban.jails = {
nextcloud = ''
enabled = true
# Nextcloud does some throttling already, so we need to set
# these to something bigger.
findtime = 43200
bantime = 86400
'';
};
services.backups.nextcloud = {
user = "nextcloud";
paths = [
"/var/lib/nextcloud/nextcloud-db.sql"
"/var/lib/nextcloud/data/"
"/var/lib/nextcloud/config/config.php"
];
preparation = {
packages = [
config.services.postgresql.package
config.services.nextcloud.occ
];
text = ''
nextcloud-occ maintenance:mode --on
pg_dump ${config.services.nextcloud.config.dbname} --file=/var/lib/nextcloud/nextcloud-db.sql
'';
};
cleanup = {
packages = [
pkgs.coreutils
config.services.nextcloud.occ
];
text = ''
rm /var/lib/nextcloud/nextcloud-db.sql
nextcloud-occ maintenance:mode --off
'';
};
};
} }

View file

@ -1,5 +1,8 @@
{ pkgs, ... }:
{ {
config,
pkgs,
...
}: {
services.postgresql = { services.postgresql = {
package = pkgs.postgresql_14; package = pkgs.postgresql_14;
enable = true; enable = true;
@ -25,11 +28,16 @@
name = "nextcloud"; name = "nextcloud";
ensureDBOwnership = true; ensureDBOwnership = true;
} }
{
name = config.services.authelia.instances.main.user;
ensureDBOwnership = true;
}
]; ];
ensureDatabases = [ ensureDatabases = [
"grafana" "grafana"
"nextcloud" "nextcloud"
config.services.authelia.instances.main.user
]; ];
}; };
} }

View file

@ -1,14 +1,16 @@
{ pkgs, lib, ... }:
let
inherit (lib) concatStringsSep;
in
{ {
pkgs,
lib,
...
}: let
inherit (lib) concatStringsSep;
in {
# Sadly, steam-run requires some X libs # Sadly, steam-run requires some X libs
environment.noXlibs = false; environment.noXlibs = false;
systemd.services.starbound = { systemd.services.starbound = {
description = "Starbound"; description = "Starbound";
after = [ "network.target" ]; after = ["network.target"];
serviceConfig = { serviceConfig = {
ExecStart = "${pkgs.local.starbound}/bin/launch-starbound ${./configs/starbound.json}"; ExecStart = "${pkgs.local.starbound}/bin/launch-starbound ${./configs/starbound.json}";
@ -65,7 +67,7 @@ in
# Game servers shouldn't use cgroups themselves either # Game servers shouldn't use cgroups themselves either
ProtectControlGroups = true; ProtectControlGroups = true;
# Most game servers will never need other socket types # Most game servers will never need other socket types
RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ]; RestrictAddressFamilies = ["AF_UNIX AF_INET AF_INET6"];
# Also a no-brainer, no game server should ever need this # Also a no-brainer, no game server should ever need this
LockPersonality = true; LockPersonality = true;
# Some game servers will probably try to set this, but they # Some game servers will probably try to set this, but they
@ -111,7 +113,9 @@ in
services.backups.starbound = { services.backups.starbound = {
user = "root"; user = "root";
paths = [ "/var/lib/private/starbound/storage/universe/" ]; paths = [
pauseServices = [ "starbound.service" ]; "/var/lib/private/starbound/storage/universe/"
];
pauseServices = ["starbound.service"];
}; };
} }

View file

@ -1,8 +1,6 @@
{ config, ... }: {config, ...}: let
let domain = config.services.nginx.domain;
inherit (config.services.nginx) domain; in {
in
{
services.tlaternet-webserver = { services.tlaternet-webserver = {
enable = true; enable = true;
listen = { listen = {
@ -12,17 +10,15 @@ in
}; };
# Set up SSL # Set up SSL
services.nginx.virtualHosts."${domain}" = services.nginx.virtualHosts."${domain}" = let
let inherit (config.services.tlaternet-webserver.listen) addr port;
inherit (config.services.tlaternet-webserver.listen) addr port; in {
in serverAliases = ["www.${domain}"];
{
serverAliases = [ "www.${domain}" ];
forceSSL = true; forceSSL = true;
useACMEHost = "tlater.net"; useACMEHost = "tlater.net";
enableHSTS = true; enableHSTS = true;
locations."/".proxyPass = "http://${addr}:${toString port}"; locations."/".proxyPass = "http://${addr}:${toString port}";
}; };
} }

View file

@ -1,5 +1,4 @@
{ config, ... }: {config, ...}: {
{
# iptables needs to permit forwarding from wg0 to wg0 # iptables needs to permit forwarding from wg0 to wg0
networking.firewall.extraCommands = '' networking.firewall.extraCommands = ''
iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
@ -24,10 +23,20 @@
}; };
wireguardPeers = [ wireguardPeers = [
# yui
{ {
AllowedIPs = [ "10.45.249.2/32" ]; # yui
PublicKey = "5mlnqEVJWks5OqgeFA2bLIrvST9TlCE81Btl+j4myz0="; wireguardPeerConfig = {
AllowedIPs = ["10.45.249.2/32"];
PublicKey = "5mlnqEVJWks5OqgeFA2bLIrvST9TlCE81Btl+j4myz0=";
};
}
{
# yuanyuan
wireguardPeerConfig = {
AllowedIPs = ["10.45.249.10/32"];
PublicKey = "0UsFE2atz/O5P3OKQ8UHyyyGQNJbp1MeIWUJLuoerwE=";
};
} }
]; ];
}; };
@ -38,23 +47,23 @@
matchConfig.Name = "wg0"; matchConfig.Name = "wg0";
networkConfig = { networkConfig = {
Description = "VLAN";
Address = [ Address = [
"10.45.249.1/32" "10.45.249.1/32"
# TODO(tlater): Add IPv6 whenever that becomes relevant # TODO(tlater): Add IPv6 whenever that becomes relevant
]; ];
IPv4Forwarding = "yes"; IPForward = "yes";
IPv4ProxyARP = "yes"; IPv4ProxyARP = "yes";
}; };
routes = [ routes = [
{ {
Source = "10.45.249.0/24"; routeConfig = {
Destination = "10.45.249.0/24"; Source = "10.45.249.0/24";
Gateway = "10.45.249.1"; Destination = "10.45.249.0/24";
GatewayOnLink = "no"; Gateway = "10.45.249.1";
GatewayOnLink = "no";
};
} }
]; ];

View file

@ -3,9 +3,15 @@
defaultSopsFile = ../keys/production.yaml; defaultSopsFile = ../keys/production.yaml;
secrets = { secrets = {
"battery-manager/email" = { }; "battery-manager/email" = {
owner = "battery-manager";
group = "battery-manager";
};
"battery-manager/password" = { }; "battery-manager/password" = {
owner = "battery-manager";
group = "battery-manager";
};
# Gitea # Gitea
"forgejo/metrics-token" = { "forgejo/metrics-token" = {
@ -25,12 +31,12 @@
}; };
# Heisenbridge # Heisenbridge
"heisenbridge/as-token" = { }; "heisenbridge/as-token" = {};
"heisenbridge/hs-token" = { }; "heisenbridge/hs-token" = {};
# Matrix-hookshot "hetzner-api" = {
"matrix-hookshot/as-token" = { }; owner = "acme";
"matrix-hookshot/hs-token" = { }; };
# Nextcloud # Nextcloud
"nextcloud/tlater" = { "nextcloud/tlater" = {
@ -38,14 +44,6 @@
group = "nextcloud"; group = "nextcloud";
}; };
# Porkbub/ACME
"porkbun/api-key" = {
owner = "acme";
};
"porkbun/secret-api-key" = {
owner = "acme";
};
# Restic # Restic
"restic/local-backups" = { "restic/local-backups" = {
owner = "root"; owner = "root";
@ -64,10 +62,10 @@
}; };
# Steam # Steam
"steam/tlater" = { }; "steam/tlater" = {};
# Turn # Turn
"turn/env" = { }; "turn/env" = {};
"turn/secret" = { "turn/secret" = {
owner = "turnserver"; owner = "turnserver";
}; };

960
flake.lock generated

File diff suppressed because it is too large Load diff

252
flake.nix
View file

@ -2,7 +2,8 @@
description = "tlater.net host configuration"; description = "tlater.net host configuration";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05-small"; nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
disko = { disko = {
url = "github:nix-community/disko"; url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -12,6 +13,10 @@
url = "github:Mic92/sops-nix"; url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nvfetcher = {
url = "github:berberman/nvfetcher";
inputs.nixpkgs.follows = "nixpkgs";
};
tlaternet-webserver = { tlaternet-webserver = {
url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git"; url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -22,146 +27,131 @@
}; };
sonnenshift = { sonnenshift = {
url = "git+ssh://git@github.com/sonnenshift/battery-manager"; url = "git+ssh://git@github.com/sonnenshift/battery-manager?ref=tlater/implement-nix-module";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = outputs = {
{ self,
self, nixpkgs,
nixpkgs, sops-nix,
sops-nix, nvfetcher,
deploy-rs, deploy-rs,
... ...
}@inputs: } @ inputs: let
let system = "x86_64-linux";
system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system};
pkgs = nixpkgs.legacyPackages.${system}; in {
##################
vm = nixpkgs.lib.nixosSystem { # Configurations #
##################
nixosConfigurations = {
# The actual system definition
hetzner-1 = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
specialArgs.flake-inputs = inputs; specialArgs.flake-inputs = inputs;
modules = [ modules = [
./configuration ./configuration
./configuration/hardware-specific/vm.nix ./configuration/hardware-specific/hetzner
];
};
in
{
##################
# Configurations #
##################
nixosConfigurations = {
# The actual system definition
hetzner-1 = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs.flake-inputs = inputs;
modules = [
./configuration
./configuration/hardware-specific/hetzner
];
};
};
############################
# Deployment configuration #
############################
deploy.nodes = {
hetzner-1 = {
hostname = "116.202.158.55";
profiles.system = {
user = "root";
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.hetzner-1;
};
sshUser = "tlater";
sshOpts = [
"-p"
"2222"
"-o"
"ForwardAgent=yes"
];
};
};
#########
# Tests #
#########
checks.${system} = import ./checks (inputs // { inherit system; });
###########################
# Garbage collection root #
###########################
packages.${system} =
let
localPkgs = import ./pkgs { inherit pkgs; };
in
{
default = vm.config.system.build.vm;
crowdsec-hub = localPkgs.crowdsec.hub;
crowdsec-firewall-bouncer = localPkgs.crowdsec.firewall-bouncer;
};
###################
# Utility scripts #
###################
apps.${system} = {
default = self.apps.${system}.run-vm;
run-vm = {
type = "app";
program =
(pkgs.writeShellScript "" ''
${vm.config.system.build.vm.outPath}/bin/run-testvm-vm
'').outPath;
};
update-crowdsec-packages =
let
git = pkgs.lib.getExe pkgs.git;
nvfetcher = pkgs.lib.getExe pkgs.nvfetcher;
in
{
type = "app";
program =
(pkgs.writeShellScript "update-crowdsec-packages" ''
cd "$(${git} rev-parse --show-toplevel)"
cd ./pkgs/crowdsec
${nvfetcher}
echo 'Remember to update the vendorHash of any go packages!'
'').outPath;
};
};
###########################
# Development environment #
###########################
devShells.${system}.default = nixpkgs.legacyPackages.${system}.mkShell {
sopsPGPKeyDirs = [
"./keys/hosts/"
"./keys/users/"
];
nativeBuildInputs = [ sops-nix.packages.${system}.sops-import-keys-hook ];
packages = with pkgs; [
sops-nix.packages.${system}.sops-init-gpg-key
deploy-rs.packages.${system}.default
nixpkgs-fmt
cargo
clippy
rustc
rustfmt
rust-analyzer
pkg-config
openssl
]; ];
}; };
}; };
############################
# Deployment configuration #
############################
deploy.nodes = {
hetzner-1 = {
hostname = "116.202.158.55";
profiles.system = {
user = "root";
path = deploy-rs.lib.${system}.activate.nixos self.nixosConfigurations.hetzner-1;
};
sshUser = "tlater";
sshOpts = ["-p" "2222" "-o" "ForwardAgent=yes"];
};
};
#########
# Tests #
#########
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
###################
# Utility scripts #
###################
apps.${system} = {
default = self.apps.${system}.run-vm;
run-vm = {
type = "app";
program = let
vm = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs.flake-inputs = inputs;
modules = [
./configuration
./configuration/hardware-specific/vm.nix
];
};
in
(pkgs.writeShellScript "" ''
${vm.config.system.build.vm.outPath}/bin/run-testvm-vm
'')
.outPath;
};
update-pkgs = {
type = "app";
program = let
nvfetcher-bin = "${nvfetcher.packages.${system}.default}/bin/nvfetcher";
in
(pkgs.writeShellScript "update-pkgs" ''
cd "$(git rev-parse --show-toplevel)/pkgs"
${nvfetcher-bin} -o _sources_pkgs -c nvfetcher.toml
'')
.outPath;
};
update-nextcloud-apps = {
type = "app";
program = let
nvfetcher-bin = "${nvfetcher.packages.${system}.default}/bin/nvfetcher";
in
(pkgs.writeShellScript "update-nextcloud-apps" ''
cd "$(git rev-parse --show-toplevel)/pkgs"
${nvfetcher-bin} -o _sources_nextcloud -c nextcloud-apps.toml
'')
.outPath;
};
};
###########################
# Development environment #
###########################
devShells.${system}.default = nixpkgs.legacyPackages.${system}.mkShell {
sopsPGPKeyDirs = ["./keys/hosts/" "./keys/users/"];
nativeBuildInputs = [
sops-nix.packages.${system}.sops-import-keys-hook
];
packages = with pkgs; [
sops-nix.packages.${system}.sops-init-gpg-key
deploy-rs.packages.${system}.default
cargo
clippy
rustc
rustfmt
rust-analyzer
pkg-config
openssl
];
};
};
} }

View file

@ -1,6 +1,6 @@
porkbun: authelia:
api-key: ENC[AES256_GCM,data:p3lqvGc8m2U/12rBPjoNR7hxQyD52CyEen/V8q59k5CSJZSqzZS8M5vEXFBsUMjz2lrmKM4pgtz4wa2fWK6Ty4LJCaI=,iv:OQC3FpwTtPmqHvDbA41mWF7LGYwC/jD2ZMBsE8ktNOg=,tag:kq5hUR7TBgczuGcXpsdu2A==,type:str] storageEncryptionKey: ENC[AES256_GCM,data:OUCC+6Gcr6U7Mub1+DaIyswTV6da1wd1u0WGEm4wpJ8L0mi7WSpEmVjH79YyRhp7AmiZhdFFDXFeEYthBb2AZl+xoS9gqs6rWyfU4ezaCbXBiS/dIhsA5foPg13wq5A33qJWtPTy7DJEgqHaIonnaBuVJIBwH3wzPTHc3bDvBo4=,iv:intiZzngz5cMTtjEI9rTKMW0Xv3KB3ZEgtYN3amwKCE=,tag:AKxfbeZlPs54esHCsVnNCg==,type:str]
secret-api-key: ENC[AES256_GCM,data:zV5PTKf45Zab8uW8mbuXmPNzciq6tV9OF0wUND7YnRk/DjZneYWItAsNBVoM+iHA+XsUPDoeKo6hoJiGkH/cCQ8WvuM=,iv:yr1M5DlgI8k6BgzNz3HRnqspHOrQuf2PmoZS1HGp0v8=,tag:JkNNziMMfKFZV2hnx5lXRg==,type:str] sessionSecret: ENC[AES256_GCM,data:GEMWhBltOIOs0g9FsWk3OQGs6dMcbwz3ZuhlyBFYROylsIZb4xTXWLgNwIpHwQukQU3TgvIxbCW/fGRWiALPanE2koSVAHNx0UU0hj1mVNRFQGK4H3EL10tPp7l4PofrcdeCbLPrOwM/xLOuPt+52sKlcbL2Awz5/MmpUVpCKXc=,iv:kWX2ptOpTgW3obBgri0MvVv6gCEPR3o77sldOXFQeks=,tag:je4pqLcEOhuBTQkoZHYNCw==,type:str]
battery-manager: battery-manager:
email: ENC[AES256_GCM,data:rYLUACXR/n+bLBmZ,iv:sUBEkh2+7qGjHZ5R23e/hoCiyTA7GTL4bJvXmxjZ5Sw=,tag:fdPMllaQQfRgX0WZKIre4g==,type:str] email: ENC[AES256_GCM,data:rYLUACXR/n+bLBmZ,iv:sUBEkh2+7qGjHZ5R23e/hoCiyTA7GTL4bJvXmxjZ5Sw=,tag:fdPMllaQQfRgX0WZKIre4g==,type:str]
password: ENC[AES256_GCM,data:7cokZa6Q6ahSeiFPz+cV,iv:vz405P0IcG9FsAQXlY7mi78GuushQUKJm2irG6buGzc=,tag:JLHG2jTkJDGbinAq9dXRsQ==,type:str] password: ENC[AES256_GCM,data:7cokZa6Q6ahSeiFPz+cV,iv:vz405P0IcG9FsAQXlY7mi78GuushQUKJm2irG6buGzc=,tag:JLHG2jTkJDGbinAq9dXRsQ==,type:str]
@ -16,9 +16,6 @@ steam:
heisenbridge: heisenbridge:
as-token: ENC[AES256_GCM,data:+2yo6T18j34622H8ZWblAFB2phLw1q0k0vUQEZ5sFj7dQaRnkEiAMi0R3p17Zq0pOtGEC0RRZuPLYkcZ1oKP0w==,iv:lGwrQYp//FufpmJocrLIVyy9RK7lEEVcpAi0wmkjr34=,tag:yV06UbhAYJQz36O2XdhY+A==,type:str] as-token: ENC[AES256_GCM,data:+2yo6T18j34622H8ZWblAFB2phLw1q0k0vUQEZ5sFj7dQaRnkEiAMi0R3p17Zq0pOtGEC0RRZuPLYkcZ1oKP0w==,iv:lGwrQYp//FufpmJocrLIVyy9RK7lEEVcpAi0wmkjr34=,tag:yV06UbhAYJQz36O2XdhY+A==,type:str]
hs-token: ENC[AES256_GCM,data:u52WpkQFd/J7JFoE/rfNluebyZQLOokvkVdL7+AEAvrhJhrkJli1ztkD79lbC+6tGUH4tT3T+nX9wvGKnrRUQg==,iv:as+9fVuvMg2IoE2WIKD9mHi+znhNcWRh5Zq+yr0xcDQ=,tag:mZ7fh7U0MfgI8hyq/28Bcg==,type:str] hs-token: ENC[AES256_GCM,data:u52WpkQFd/J7JFoE/rfNluebyZQLOokvkVdL7+AEAvrhJhrkJli1ztkD79lbC+6tGUH4tT3T+nX9wvGKnrRUQg==,iv:as+9fVuvMg2IoE2WIKD9mHi+znhNcWRh5Zq+yr0xcDQ=,tag:mZ7fh7U0MfgI8hyq/28Bcg==,type:str]
matrix-hookshot:
as-token: ENC[AES256_GCM,data:nXTanPhDyDF7R3AllLqpM5dzljBrHwlh1KJnTGIi5PhbDY2lPj4+uXkMEwvm1u+hQjPyM7vKZPfK+0/dms6Y7A==,iv:fSakJN+yai0gfOJKFxxaxgyUtk0pNmIeqVgrdq92/24=,tag:Qc7+SUnm5/Nq5+QIScR9kQ==,type:str]
hs-token: ENC[AES256_GCM,data:Bwyj0JTTN0NNnwOs1zA8CqbtZSNcvlINeT7QVc2eJiHda92J6vQk7bSxy6KuqCN9DxlUsK13ggYjNORY2vic5w==,iv:Npnp8arYQ3Yb6CXrnKgE03hD7ZjGINPa/DwFI8D+5tA=,tag:FqNE6yI0nF4puEUw9MGAjQ==,type:str]
wireguard: wireguard:
server-key: ENC[AES256_GCM,data:mXb7ZznJHf5CgV8rI4uzPBATMRbmd7LimgtCkQM9kAjbIaGwUBqJZBN3fXs=,iv:3Po1Orinzov9rnEm9cLzgJY1PeD+5Jl9115MriABHh8=,tag:E/2CjDO1JCvJzxCnqKcNyw==,type:str] server-key: ENC[AES256_GCM,data:mXb7ZznJHf5CgV8rI4uzPBATMRbmd7LimgtCkQM9kAjbIaGwUBqJZBN3fXs=,iv:3Po1Orinzov9rnEm9cLzgJY1PeD+5Jl9115MriABHh8=,tag:E/2CjDO1JCvJzxCnqKcNyw==,type:str]
restic: restic:
@ -37,63 +34,63 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2025-02-07T19:44:49Z" lastmodified: "2024-04-11T23:38:56Z"
mac: ENC[AES256_GCM,data:+0hpd/E7GxK/27f2Itf0hDV+3Ga4gHb8xxLutJ32HLBWLZ5Y+dN03xgkz8jBTiM+BeHwS4gz70Cs9X3zLMHbosWVuIV9DLuRaHRq/IU9KiADwqmCySZALqCf3+T5QKZr3Qs4AZJHwaAXkRX9HbnRFriIAFDJW/BGdIHdoROquxY=,iv:TeXI8LGqHVa5wo61sGdNbZ2nJvSlPdgn9R3Lq5qUggU=,tag:TFort5wxVTdi9LMlMeT/DQ==,type:str] mac: ENC[AES256_GCM,data:GjIB0EbWsh4o+QoFSyIXgGYnNhRlvfSmue1LyTt6oUlIjNgODhdIB8px8LnRo0rmm/f1YHbDq2MFOxlgdm3PTNaqm/MoKyW3r/wuAeWADsYayQszLNxyhTMXcjWtfm6zCRIuc/+YyM44pXRfVrOZRAin9B6pmJZsRJwBAZpogbU=,iv:r/ZQZvrP0E9dOW5fhBH2I21Z0uv2e3njdEGmadxEALg=,tag:iZvbGTvRJFo80n8aoKSSmQ==,type:str]
pgp: pgp:
- created_at: "2025-01-21T17:55:44Z" - created_at: "2024-03-18T04:02:00Z"
enc: |- enc: |-
-----BEGIN PGP MESSAGE----- -----BEGIN PGP MESSAGE-----
hQEMA7x7stsXx45CAQf6AjhhYCHhRvJx4xxiXPMTfpIpAvseB1CXVfuhfj2r2yrf hQEMA7x7stsXx45CAQf7BjF+HR3WKdMyAV6R1M0+lqDz6hBHKyGH7YBB/QZBqRbK
3HfJnNOSDBcmHdp9fiLJattqfsykcGisUDVIplCeA7cIJjH3sf8MuIJXDTLmvE0Z 3hdABIwWUsqpHjleEOp/Gj0VhZqwagqHxK4Fp5G0r3QBupbAO8u/+DNI8wll0Nva
BmV8LuwP/UPQSZzY3w1eTAoYT/by35gfJm4ofipft2qyIjQuBgOlrg/0swvBl7LK dlOh0Jqp4E17TkERMQL02rrQ1ZmpOYmPkCd2//xkmWAQ1LatHWeRVSRxQBuMtPQi
47mI6mVdds8RutHw/xhJZeNjKF/5tADPJ2CHjOHbCQhLji13Kpm3yObOnM8K2SfV btrefcQNjQCvS9/60dp8oTu8nxlFA4iHCBQKNIKVGqQH7jkdIfMPdUILjCkCiyCc
P9uVudCFN/ZBiTlVkB7PsuitwZx1fW0SR3jcWxbRd17M2k2RAQQDUCqPKaoJ3T3f h+OxlHZZnpU6U9A+hjMBinvCzebSkZh48VX/T33Kr+4b0CBr1gR9MSXKG9f2MPQP
r2ExwmyO4j7G2vkFv1RgQnhAoHqqRZ1nSjNw1+27MNJeAbT1ddia2TC5Q2zjRZY9 PMl6rPvqSqG6ddN9QDI+0HEHYaRvxPIV8uDS36tVxNJeAQHB5/6Lt7hJdYWgwf5E
tRJi2pNZH9A424lpIBLnkPl5rpCR/UZ+bqhaQ9C2kFMldSldPn8dMiBy8XG0Trji TLgbZ0IxB17++6K++GlaG8WHO65l1jzmkPlN+ZGcwnhibDxnZjP6kqGqDFcZP4ge
B6X44Q/0RCsJD3FS61GASIjaWdEX0DdSOdhtbtBLLQ== cnV0KnhYcC59IooQYrWKzAJex9rnwPo7MGKV6XwZOQ==
=yLOx =Hy9T
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: B9C7AE554602D82B1AC2CFD0BC7BB2DB17C78E42! fp: 535B61015823443941C744DD12264F6BBDFABA89
- created_at: "2025-01-21T17:55:44Z" - created_at: "2024-03-18T04:02:00Z"
enc: |- enc: |-
-----BEGIN PGP MESSAGE----- -----BEGIN PGP MESSAGE-----
hQIMA9ahl2ynTH87AQ/9F/lyLXn60X5Slcd9I7VPdM6x4IjFI0UwMAhMVv0GeimD hQIMA9ahl2ynTH87AQ/+LNXxC3acjs2+c38gHZRW6Am4XFx1t/4tfxIgaaK/Boq8
9Mj1vKW+l1v0gFtqCGjANEEYpXMr109CES+m6z7dNXcIQeR6pyjW6uCTiPUK/ZO/ PGU5CFNOMDGv8u/cwyDbfNM7GuL5g7vrLmBXzSV5ErZqc6bJ0+ZCNPTRIxP1Vxph
ZQyhJR0zsT15BTHc6c9Eso9YedsggpRAbjjVIlmrTJx52LfqYYGoRA0XqcLE6LUS tWiDIyTwuqUzxWpOlSzii2Sqhlp8CyiWzBe95eIr96XzDCCtfzyCZ0BYyKgpHHxB
6v6KR/299zzSHsTFVon6wac9wY7i1XvL0Q7JiYSNV2cNADiCJdCIBw1UkaSVH1uz BltH0/+0JZFiR19zvf6M99AHwM8OddRQkXav+mRIJQpA87ovVZcAv5skYGJgNCqN
sTVdoEoQ1m7hu9pgg4UksUzi9IV3xBpte82tlEK9p0M+RXK/02fjMTWcx3q5fnjN 55fbskuYmqEnloQCZVJ2+2ZXK5Qn/uq4fLJCiIdZm4YsctJnV5spzZIL+dcOty65
pBu3HkVzLpAjAQHMQ/O2cgbjFmwgjp5APUhn3IBx5M3F2ypGICnAvsrjfzalTChN Plk77BWzLaU5UOKCBAJWrK8oZSTGOrp4VZqb62DuqMRejG0JXmneIVk7p79yn5eA
nkCmTfLynNIL1bX7PViABX2Q/yXGzPqFDe96pPw2hGjOXmxjKxjDoLT2IHixYp4D ANVMGRF4b8RP9YUhzE8HACFzQebKpUU8XKv9+qsmO9Le5jUhU3UQeCSSzT/T5Dr8
cxb5519/WmOMrFd4SJeGiUR2Ph/VBZBVGafRfGAKMzxi666igWjwSU0YSDYeEUW/ kLDNtmW+mliQnxFlKcVWq2JIG+HaQD1KLOAl0JBNCOSLif2ofaHahuZ15agbYeis
BalkkWoz/KQZ5HgQwL9dyp26/cjDtpIwe8jLKVNI6aWhZ2ZmBxFwNEB6fE1txOjf hyrBY92EhzqYXHk/Kzv4ff4r+WUs9NN7R4Gg+wfWvMcTtVfbi4Ht+pjjTtCZwK1C
ceIJAfm8y2qIolw3TpBAFk6s53jir17SrEux9VzfiEfFeQ0g5q7cjAs5HfhTOMfh M8JebQn0NZSpVi3e7Xaz1fQ5Tqrg8PHZtkYGoIHLRPJQwLn9PHYtGzC3rFAk+Fqq
iuUnHcCFy27wd+8bPxEaRYR57u7hneenTn3BMuu1CZputDFJWRvweMZk3cH8tiXS 5WWHELxfcsZ6DakAGSXPK/80QhEZkpGmKizTwrEde+7fpEPxjdzUqlmH3rv7mFzS
WAGpXw7aKGIOpxe7Ye5X+T3xvYCBN77aFQKkrOmHMWFCbkr2QfnwVanxmmL9BwxF WAGSiBIMjLR6ofb65vpghbwh6gXkpCtgUyINRhx/D+Kj5Z4lGD1u1I05DT1xD6VJ
rf8pG+H0URxBAsy9RZzSC+dXugnwnNBse3wupXf5YkipLx3rX9gtz4Y= FAbnH7oZ3PJecoAXgRT05FndFA1xfPMCkugmec8ML/sEZt+c3kbrXaA=
=a7xO =MqS3
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b fp: 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b
- created_at: "2025-01-21T17:55:44Z" - created_at: "2024-03-18T04:02:00Z"
enc: |- enc: |-
-----BEGIN PGP MESSAGE----- -----BEGIN PGP MESSAGE-----
hQIMA0f3HaPlrXn5ARAAph6bpRqnO1TKEE7K6cMCxCA9k3xj7hknBdHN1KlIQkeb hQIMA0f3HaPlrXn5AQ/+N5NK5UJdtw5e7O9T4hfIhtMXci/og1cJiI64daSyNeDH
LUOW9lvxh96q/fQ3NvZXpEQqZoEgdhnYTHgr/Hqx7VSksSicOkJLorrBLELjP1Xo jq+CPJ8e73yiTPwu6wHHqfuEhlEuI6sJY0ZJVFU+h4SIBtG21veGEz7GzlYgBCJm
FKTxRYQmgZmgmBc0u7+lF2t0VLey6DfsC9ehY2LQcRuY9WxxCp2nDjjR4wrfmMKi xvJHXjtM8AprqnFVO7Fj9QA/ik5QBP6ZpkOY7j8/qf1G7alOIne/MYRALXDvvIMH
i/SbpaM9Q29DpdNpwkSlTe2sKr+uMDVA2rCrjpisSDiUBgPe2eJEYPQJ7DHFxB4N HTWE+Y2N57yZK55Pokmdw10hawbrn/N1nt2Y7sa1+5TlRNtuA/+zLkXtEjRr5U3N
26NHS+QtUgZhAA1DOGYZvrXzqcZSeAPGk8WXY44KA7o2iRFw+TczpF/VaMHRR5vN DId+hqCKgXDqKLBMkh4mZUTGOGsk8eeKAWwyPp9+8A5/0rfy+xOJYEjjHICXQMSE
M62IoXGqnMIBau+tsiE1JEVg60DIHpBjsEY/WUgbNX02zfaHcp1OenxU4p7W19Xn zfe6qvj/fRJKGzT5lEzD+ZKHlR0zHEwGRfHqrVUTdPcPdKj3DZILjsoe5ba4VlAp
Y8psPm7JNzmi3+nItB2i3+OLQrumk6VaT021ykterc52tR/3ejCzQCsfbqDLnc/6 sS0CAYTg3YuWMT4iHuOQlY5IoQxHHrn7k8ox5iZULFecg58f6r6iJL3AepDYWAey
r2SvNVDiKpxQ/iFyHb0uLHWy0Jx0lYwmMUrThe1f4k/+m9fEPrA5v/1kTGY5o1Iy gtQXYBeaeCm5Ddwmd6TBVz8Q4bCVYIrHbVeAhSDkxfrWLc5UORggvLEWiXilGDJi
aTuHE19VtB6V+g1H6ZRPr8g8wn/3pg1nC2vUoRzT+oUudxrijrUH8SSlwhu59y1z DzAv0MVHE2Wa3eOJLq05K2/LBqRBD1XYM3dcS6JSdFxWWMzvLdUOB4dAuPt9gpl9
7SrdMmtqA1/JsGNMawCLWd63u+/3GC9LmZzK2h/bV/R4DG5f8tsCpy9BrFAHqTMs liaA13Blw/ev+U4ADxptrl+QuYRbWz3z6rniYpluSrTbVCKFRoHXSGFPy5u8/N6O
lZ3SONUUeM3uMOggQ/JT26EVxECmNGYIX1TYLOxcqisRXLbfco5LEc6T8PJvdivS QyjfoovIBxXKnbUq2kMoFa/qFpc1pDUn0sjQNsUBdtorAu3Up4icyoih7qwx2J3S
WAFr6kBMdoeWhUT8MdT5AJBm9mo95A0WM8I14pGrszaezfgo9zc2zs0ebdLEjhI4 WAGB1jHWMfcsBJqPwjRYkqBf6MuwHAZWdd+zvj/fKfft9jtxLcCGOIM6QdfiWbl0
jlTf4tXgK3RG455CRWOd9OA3ukR83W7UW6LYjoNaDWHw2RXZlb+hv8E= Wq4gHdH7OhSy+ZgRnaBRt/GAkzkHvfG68HfulviHZ1h2mrQN1y3mxpg=
=+MFT =RCYB
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: 0af7641adb8aa843136cf6d047f71da3e5ad79f9 fp: 0af7641adb8aa843136cf6d047f71da3e5ad79f9
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.2 version: 3.8.1

View file

@ -1,6 +1,7 @@
porkbun: authelia:
api-key: ENC[AES256_GCM,data:A5J1sqwq6hs=,iv:77Mar3IX7mq7z7x6s9sSeGNVYc1Wv78HptJElEC7z3Q=,tag:eM/EF9TxKu+zcbJ1SYXiuA==,type:str] storageEncryptionKey: ENC[AES256_GCM,data:8X6Zvq7ct1MkMH+lc5kTTHXOo6rGyhSzc3aWxGRA5tnBet+TGcENo0RYTYmactsPGVpTIUGGplaG7B7dqRPhkdDHhbCCZCm2nLaYjpVJ241DrpUNKHn8lvg/bMxUQ/Dvw76ByYuWN6bREr3XRaBztBSPzld8zTSYx71I0CKY7vk=,iv:cJSwfuVWO39qqKCGt2Mvw7pN8+hD6kRH9v4c/u4hLuk=,tag:YhdlXuX2ETxjb443RI8MsA==,type:str]
secret-api-key: ENC[AES256_GCM,data:8Xv+jWYaWMI=,iv:li4tdY0pch5lksftMmfMVS729caAwfaacoztaQ49az0=,tag:KhfElBGzVH4ByFPfuQsdhw==,type:str] sessionSecret: ENC[AES256_GCM,data:dnoWmc4HND62w3jMXL+akncAEb61c/I70DgRytx55Wxcn4rMiswp6zCkRdsP4CkouTQ1lyAcQrubp5I8M9Kyow/KBMYz9dPkr4+2xJ9w0SEmAVhyPe2DFvYos3x0Uvx5S0B3o1mXoXqbg78e4w5yEIbALiJT8VPGrWK8Cl4nVPo=,iv:FHDXUW2DWUmEZzWUYkYduogdVOtvMlRH4/fVg05cZaI=,tag:u282WQnHpBsZGYJH7mFFKA==,type:str]
jwtSecret: ENC[AES256_GCM,data:0M3AyoMp+orrljl5NsxmthzrHMmu0REcz7+9fpFKbwwqV6KqlpgGddjYZIsTpHEWEq9zhZ2YWLJkMxKdDgROVHUFZGKut28JPSAjjY+1V0wxNBnfSCnxEv5BUw2+cCxcpCwYQyNfRK6SotTt8aqpxvda4oRXpzxV6SW7ogDjc6E=,iv:D57SynZkW2JuFyX6bpZYkxpR2KtkOmKaySg1Bxim0r8=,tag:JCPGZaumdHrtgcH16A7b+g==,type:str]
battery-manager: battery-manager:
email: ENC[AES256_GCM,data:LM/EGzWHfVQ=,iv:jFaoUQuUfuGoOyj/GFpdI8TerH/c8D9fjvio+IEt2Tc=,tag:IWLiN011JEnHRLIXWQgfmA==,type:str] email: ENC[AES256_GCM,data:LM/EGzWHfVQ=,iv:jFaoUQuUfuGoOyj/GFpdI8TerH/c8D9fjvio+IEt2Tc=,tag:IWLiN011JEnHRLIXWQgfmA==,type:str]
password: ENC[AES256_GCM,data:SUxjqS7SJHM=,iv:LvdKk88S+nSImh6/ZezbFGLCUBu1Lpdu+neF2xyHdBg=,tag:rcMyZuW4FVNbcbz00wQKBg==,type:str] password: ENC[AES256_GCM,data:SUxjqS7SJHM=,iv:LvdKk88S+nSImh6/ZezbFGLCUBu1Lpdu+neF2xyHdBg=,tag:rcMyZuW4FVNbcbz00wQKBg==,type:str]
@ -16,9 +17,6 @@ steam:
heisenbridge: heisenbridge:
as-token: ENC[AES256_GCM,data:tXbOeo7nv8I=,iv:wJAKcOXX9nGIw4n38ThOoj29u7dUWhsxSQG/p79JlEw=,tag:rTVaGS2UuWcea1uBa8YX2g==,type:str] as-token: ENC[AES256_GCM,data:tXbOeo7nv8I=,iv:wJAKcOXX9nGIw4n38ThOoj29u7dUWhsxSQG/p79JlEw=,tag:rTVaGS2UuWcea1uBa8YX2g==,type:str]
hs-token: ENC[AES256_GCM,data:VBwvwomv0Xg=,iv:q6INtJ+rg+QiXj8uBdBzQYQZUBBXp+9odxDHwvu8Jxc=,tag:XKhm8nxygAkKaiVPJ2Fcdg==,type:str] hs-token: ENC[AES256_GCM,data:VBwvwomv0Xg=,iv:q6INtJ+rg+QiXj8uBdBzQYQZUBBXp+9odxDHwvu8Jxc=,tag:XKhm8nxygAkKaiVPJ2Fcdg==,type:str]
matrix-hookshot:
as-token: ENC[AES256_GCM,data:uSUOo4f2KqA=,iv:Xb9G8Ecv6m59m51kDw2bOfq3SMJt4g9/6/EdH74R+KM=,tag:K9MSfO2c2Y4rlf0eYrmTnw==,type:str]
hs-token: ENC[AES256_GCM,data:0KsyA06InL4=,iv:zAR0Y1fk8SyodcSLBHlQ8I+BAmttz9Hkd8Q3OREFqs4=,tag:t1Et8N/3seq95DeGoUd7Sw==,type:str]
wireguard: wireguard:
server-key: ENC[AES256_GCM,data:FvY897XdKoa/mckE8JQLCkklsnYD6Wz1wpsu5t3uhEnW3iarnDQxF9msuYU=,iv:jqGXfekM+Vs+J9b5nlZ5Skd1ZKHajoUo2Dc4tMYPm1w=,tag:EehikjI/FCU8wqtpvJRamQ==,type:str] server-key: ENC[AES256_GCM,data:FvY897XdKoa/mckE8JQLCkklsnYD6Wz1wpsu5t3uhEnW3iarnDQxF9msuYU=,iv:jqGXfekM+Vs+J9b5nlZ5Skd1ZKHajoUo2Dc4tMYPm1w=,tag:EehikjI/FCU8wqtpvJRamQ==,type:str]
restic: restic:
@ -37,43 +35,43 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2025-02-07T17:43:24Z" lastmodified: "2024-04-12T01:00:31Z"
mac: ENC[AES256_GCM,data:akmD/bfgeTyFzW1quvM16cdj0fC6+CbJ8WyX9173H11yKGxvE1USQYcErpl1SHOx9Jk8LVb7f+MsUm2fjQF1MEq6xaWI74jem12lZ9CGXFaTL7e87JvfbK7pV+aKpxSBBNFyJgbYm30ibdUwxwKmNVfPb1e0HT9qwenvoV7RobM=,iv:mKqOW0ULXL711uczUbRf9NPo6uPTQoS/IbR46S+JID4=,tag:vE6NYzYLbQHDImov1XGTcg==,type:str] mac: ENC[AES256_GCM,data:fVnMwfvGi7vtP1Fg4NLrhGvLF2PcIgZPOcwk4Ssm4iw5iSj0K1npOX3pd5BWzyszqchfYYRHY99GllAump0bZmprVAld9rf70B2HZIVvowBPuUXfc9Cz/5q0z+s8bQ5vCdElW1Bh7h8W/POePdc8cFGAyBS4i1ZVNheIDOHdDjI=,iv:Bi6rekXOx3/dwwPRryF3CoAoQi3D06ABysRF1oBeG5A=,tag:0TCra+AkhBDczj4uvAzKMw==,type:str]
pgp: pgp:
- created_at: "2025-01-21T17:55:30Z" - created_at: "2023-12-29T15:25:27Z"
enc: |- enc: |
-----BEGIN PGP MESSAGE----- -----BEGIN PGP MESSAGE-----
hQEMA7x7stsXx45CAQf9ELnm9TdXCIO6fTPiSCkKthx0tSHqBWX/s63158k4IUu7 hQEMA7x7stsXx45CAQf/RWxP6z7xjV5TqiA6lFhtygjrH9x3y1DUWG9aUb/dO+xH
v0WWgQy0SKFU3AwIFuVaAYEXB32SaOWKq2WbVAbFZU+xhyUmNe9asg9Fl24+zjGI zDbGMYqGe9RPlgi5sWPstdKXvCgs+AKNj93qJYMwEtaasJOinYXCGeAQmzg90+pt
oYnPzv3lz/5vcI6Q9rZi8F2uIi2GQZnbscS1XfjA5u17uOalQpb0hjUXr0LMaUvU bS6SoBHhGIxAvvLKKPtYx0V50I2reYR+32ux9bcrnzwIsV0P7/SSp1Cl8H+sotB8
Sggm1ZMKE1o1mHAWK6ZT9SrTMIroWFArJRZLS1eY/vI9ja7I5YR3z0MkLqIvdIp+ yf+0ULXcpC+SYECmZqzR9qQ3S+3I6/+QS+QgWj4NsyF+apxnE9oQDcBLdYP4aKgR
B4DsOXLlqAqtVoPGcK1CixQiXzzwyQAYHyJc3JFDpaF9Y4S5/bkMLGyQVMA259a+ JHERA9HYfDTKoS137pFHxgINqHkFRY6lhoZdz1yDzOjiPxd8YVfPdKyf022Rg+cX
W7ge+EngdJdXV8Unj4s7ndB1e1iM87Jc+4YOcA7jC9JeAR98n8GL+MN9vE8q2AsB J/Q2P+OhNZEG3gapNATp6wH3niovA89KwZKSmbTZOdJeAZ6NV6TiUP+TgGg5+CmV
qSOXiGSwmAkhq7+ZwJHWlivlB1In0coyJ4eMd/yhyBuc2NrstO0t595HlA93GcLN pSLaGel2NZRnFVNdDFi0dsOwhHv3FpKhIpALJh08/jsmAAslfE7vVlcEnaoUJPTS
5JsXIFMqklqGSzE2KgEXhxa2aUoJxcpApVz2BLFPvg== 3v86AACUC5D/gUxmFrrED1qoxbELCmZ17xTwjQzxwg==
=X72N =KzdF
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: B9C7AE554602D82B1AC2CFD0BC7BB2DB17C78E42! fp: 535B61015823443941C744DD12264F6BBDFABA89
- created_at: "2025-01-21T17:55:30Z" - created_at: "2023-12-29T15:25:27Z"
enc: |- enc: |
-----BEGIN PGP MESSAGE----- -----BEGIN PGP MESSAGE-----
hQIMA/3lh+ZzfS28AQ//TPVk5x8wZtKnNcbZjqmXUvi7FzaDMGYFNtiX+bQ17Lkm hQIMA/3lh+ZzfS28ARAAm729dMouF7juUeHAb+aHMoyZVKsXapxnxebkjE/LSIbz
qdo9TcH8gZI+xni6wlROQBVmu5MAVkT+NWnZIyxN7pokQPmb2v9zo7oyUPV70owU IEZwegTNrtxQJLclV4Km2gUaBTcE4vLJCpB7YxZvk7JV9OdVKi97o9PcXUXbz9ej
dD/LDNVqgyFTVzWKco4wr8CUwQWhOJI9wM0sjTCOTTbCT5hYVnJOLe630sNG9G5b /WomnEvFyyxTZGTiHU+L4kNudl8UAKhTt3P4fR3PLpTily75Kn53tzLFJuCO8fAY
cJYvaQFxKJeHKJq/DL1I9wT02gOE7vu2xh2OyEozEz3SlB7Bp2mGTLNSiOZBhbzh I/YwQAzayxhPcxk3FuPsD/ONiG7mW8n2ZwfwgOkKXwnrlJv7DreKJRYzu/EeuvX/
DNwFRTeDc1Z/ACQJyoGEXkmj1VLyBFuCfXu4UvUQSmlfyCXKTD1/CUo2uiw1N44W d4oz+k+xofniOeZmQjZllzR7/++MBg/e1U9VocN1EAWpWHP5taLiThfnVSGDhlQM
sYh5UpIrCU2eVAAXAiD1nB6dHxiau3QlapNQfbY2sOZVSnsB21yIja9C3aQN/0Q5 +4WT5ezH6EuUQlAyQNpDaCincBvCHInhrNlUPOpW51nHMb0y3n4x2hMtZA0JbYEu
gRrKYcwULzLZ0Z3oQqQxQG9acU8L9CwjKJ1vOKgPVF7hcWkba2bLVsMMaK6seVNc mkWTYDe65cHjImHXQk9oO2/v4oIyq7ywHX7g2hqVbbiLHZqqTaGfV8lP30+r6/UQ
jazp9gDAj440S2aE86CdQvgcOEsfgPhBZrulYglbhW8ZaIN2SdjDN/xP1Tn5PadP 29iAdWac1hY5HDzwbqpY6b38i60j4bkiS83xqrGYBy037bCFk1oHJqwxp5P7vrzr
gS2DVgqILKPRMF4VavzV3uhEA77QF39Hr18SeToNWcDmfqNPNNf7HpnogstGPf3y rTv5NBr95BlwF+s8xPEPZneaEu7N3UnhhSzDWp1jgsCxN9b/XHarchNt70xEt2VS
xrFAysLbD8IClU4LqI5M19akODON8qeQa5QD+jHOJmAYnMYNRmY/IwHb/SC5WyIm xpgs9GEXhsJcbrFNPYqTkFb8vjLFI+poGPTfadW17j4Pp5ftIBRNdKvDG0ni/AIp
EPNZg5E+q9cNrTKtEIuWec0SObqpaUz2E/Vt9+dge0uVgTA/QqPvMP7x19XBrRDS K98R/nvaHEFuX31SkL8ZUIRqhJm3JVqilFxLAJrqGuSN3jA6wKrimUYpK+t+64jS
WAFiKkv7MxImNgcqqe7D3StZoeNm+RXJiULaxxuR8qmMnmvaC+L7ggI4QR3TPQw2 WAEN9jHYFQDTVHix3g15S5YTGh5ROyqxouDhvSDFTmGtbm5W/HYgnkZmh53TgVeJ
mSoi7SkdjQUCa8ut15UwNHTGO+smbRs9aonGP4G7c1cOH90YYvR+BTs= Rph/O9QptculzTN+nEqshBhbjhl/uDsLsjLYo/O1AyCwTUSd3OKn6uU=
=q2IG =zThh
-----END PGP MESSAGE----- -----END PGP MESSAGE-----
fp: 2f5caa73e7ceea4fcc8d2881fde587e6737d2dbc fp: 2f5caa73e7ceea4fcc8d2881fde587e6737d2dbc
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.9.2 version: 3.8.1

View file

@ -11,230 +11,62 @@ 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 PhCB2Dz76iQ5nDY4MQ==
6/gEa+M6kuTyRYMokA8VO3w8eeZk0bqkPDP1/HmHiuz15/MOVNg0IIdQBIH0Ky2z =R7Pm
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
GmeDneCrlseO
=ncr2
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,383 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.security.crowdsec;
settingsFormat = pkgs.formats.yaml { };
hub = pkgs.fetchFromGitHub {
owner = "crowdsecurity";
repo = "hub";
rev = "7a3b4753f4577257c0cbeb8f8f90c7f17d2ae008";
hash = "sha256-HB4jHyhiO8gjBkLmpo6bDbwhfm5m5nAtNlKhDkZjt2I=";
};
cscli = pkgs.writeShellScriptBin "cscli" ''
export PATH="$PATH:${cfg.package}/bin/"
sudo=exec
if [ "$USER" != "crowdsec" ]; then
sudo='exec /run/wrappers/bin/sudo -u crowdsec'
fi
$sudo ${cfg.package}/bin/cscli "$@"
'';
acquisitions = ''
---
${lib.concatMapStringsSep "\n---\n" builtins.toJSON cfg.acquisitions}
---
'';
in
{
imports = [ ./remediations ];
options.security.crowdsec =
let
inherit (lib.types)
nullOr
listOf
package
path
str
;
in
{
enable = lib.mkEnableOption "crowdsec";
package = lib.mkOption {
type = package;
default = pkgs.crowdsec;
};
stateDirectory = lib.mkOption {
type = path;
readOnly = true;
description = ''
The state directory of the crowdsec instance. Cannot be
changed, but is exposed for downstream use.
'';
};
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
description = ''
The crowdsec configuration. Refer to
<https://docs.crowdsec.net/docs/next/configuration/crowdsec_configuration/>
for details on supported values.
'';
};
parserWhitelist = lib.mkOption {
type = listOf str;
default = [ ];
description = ''
Set of IP addresses to add to a parser-based whitelist.
Addresses can be specified either as plain IP addresses or
in CIDR notation.
'';
};
acquisitions = lib.mkOption {
type = listOf settingsFormat.type;
default = [ ];
description = ''
Log acquisitions.
'';
};
extraGroups = lib.mkOption {
type = listOf str;
default = [ ];
description = ''
Additional groups to make the service part of.
Required to permit reading from various log sources.
'';
};
hubConfigurations = {
collections = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec collections to install.
'';
};
scenarios = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec scenarios to install.
'';
};
parsers = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec parsers to install.
'';
};
postoverflows = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec postoverflows to install.
'';
};
appsecConfigs = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec appsec configurations to install.
'';
};
appsecRules = lib.mkOption {
type = listOf str;
description = ''
List of pre-made crowdsec appsec rules to install.
'';
};
};
centralApiCredentials = lib.mkOption {
type = nullOr path;
default = null;
description = ''
The API key to access crowdsec's central API - this is
required to access any of the shared blocklists.
Use of this feature is optional, entering no API key (the
default) turns all sharing or receiving of blocked IPs off.
Note that adding the API key by itself does not enable
sharing of blocked IPs with the central API. This limits the
types of blocklists this instance can access.
To also turn sharing blocked IPs on, set
`api.server.online_client.sharing = true;`.
'';
};
ctiApiKey = lib.mkOption {
type = nullOr path;
default = null;
description = ''
The API key for crowdsec's CTI offering.
'';
};
};
config = lib.mkIf cfg.enable {
# Set up default settings; anything that *shouldn't* be changed is
# set to the default priority so that users need to use
# `lib.mkForce`.
security.crowdsec = {
stateDirectory = "/var/lib/crowdsec";
settings = {
common = {
daemonize = true;
# The default logs to files, which isn't the preferred way
# on NixOS
log_media = "stdout";
};
config_paths = {
config_dir = "${cfg.stateDirectory}/config/";
data_dir = "${cfg.stateDirectory}/data/";
# This "config" file is intended to be written to using the
# cscli tool, so you can temporarily make it so rules don't
# do anything but log what they *would* do for
# experimentation.
simulation_path = "${cfg.stateDirectory}/config/simulation.yaml";
pattern_dir = lib.mkDefault "${cfg.package}/share/crowdsec/config/patterns";
hub_dir = hub;
index_path = "${hub}/.index.json";
# Integrations aren't supported for now
notification_dir = lib.mkDefault "/var/empty/";
plugin_dir = lib.mkDefault "/var/empty/";
};
crowdsec_service.acquisition_path =
# Using an if/else here because `mkMerge` does not work in
# YAML-type options
if cfg.acquisitions == [ ] then
"${cfg.package}/share/crowdsec/config/acquis.yaml"
else
pkgs.writeText "acquis.yaml" acquisitions;
cscli = {
prometheus_uri = lib.mkDefault "127.0.0.1:6060";
};
db_config = {
type = lib.mkDefault "sqlite";
db_path = lib.mkDefault "${cfg.stateDirectory}/data/crowdsec.db";
use_wal = lib.mkDefault true;
flush = {
max_items = lib.mkDefault 5000;
max_age = lib.mkDefault "7d";
};
};
api = {
cti = {
enabled = cfg.ctiApiKey != null;
key = cfg.ctiApiKey;
};
client.credentials_path = "${cfg.stateDirectory}/local_credentials.yaml";
server = {
listen_uri = lib.mkDefault "127.0.0.1:8080";
profiles_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/profiles.yaml";
console_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/console.yaml";
online_client = {
# By default, we don't let crowdsec phone home, since
# this is usually within NixOS users' concerns.
sharing = lib.mkDefault false;
credentials_path = cfg.centralApiCredentials;
};
};
};
# We enable prometheus by default, since cscli relies on it
# for metrics
prometheus = {
enabled = lib.mkDefault true;
level = lib.mkDefault "full";
listen_addr = lib.mkDefault "127.0.0.1";
listen_port = lib.mkDefault 6060;
};
};
};
systemd.packages = [ cfg.package ];
environment = {
systemPackages = [
# To add completions; sadly need to hand-roll this since
# neither `symlinkJoin` nor `buildEnv` have collision
# handling.
(pkgs.runCommandNoCCLocal "cscli" { } ''
mkdir -p $out
ln -s ${cscli}/bin $out/bin
ln -s ${cfg.package}/share $out/share
'')
];
etc."crowdsec/config.yaml".source = settingsFormat.generate "crowdsec-settings.yaml" cfg.settings;
};
systemd = {
tmpfiles.settings."10-crowdsec" = {
"${cfg.stateDirectory}".d = {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
# This must be created for the setup service to work
"${cfg.stateDirectory}/config".d = {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
"${cfg.stateDirectory}/config/parsers".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
"${cfg.stateDirectory}/config/parsers/s02-enrich".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
user = "crowdsec";
group = "crowdsec";
mode = "0700";
};
"${cfg.stateDirectory}/config/parsers/s02-enrich/nixos-whitelist.yaml" =
lib.mkIf (cfg.parserWhitelist != [ ])
{
"L+".argument =
(settingsFormat.generate "crowdsec-nixos-whitelist.yaml" {
name = "nixos/parser-whitelist";
description = "Parser whitelist generated by the crowdsec NixOS module";
whitelist = {
reason = "Filtered by NixOS whitelist";
ip = lib.lists.filter (ip: !(lib.hasInfix "/" ip)) cfg.parserWhitelist;
cidr = lib.lists.filter (ip: lib.hasInfix "/" ip) cfg.parserWhitelist;
};
}).outPath;
};
};
services = {
crowdsec-setup = {
# TODO(tlater): Depend on tmpfiles path for
# /var/lib/crowdsec/config
description = "Crowdsec database and config preparation";
script = ''
if [ ! -e '${cfg.settings.config_paths.simulation_path}' ]; then
cp '${cfg.package}/share/crowdsec/config/simulation.yaml' '${cfg.settings.config_paths.simulation_path}'
fi
if [ ! -e '${cfg.settings.api.client.credentials_path}' ]; then
${cfg.package}/bin/cscli machines add --auto --file '${cfg.settings.api.client.credentials_path}'
fi
'';
serviceConfig = {
User = "crowdsec";
Group = "crowdsec";
StateDirectory = "crowdsec";
Type = "oneshot";
RemainAfterExit = true;
};
};
# Note that the service basics are already defined upstream
crowdsec = {
enable = true;
after = [ "crowdsec-setup.service" ];
bindsTo = [ "crowdsec-setup.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "crowdsec";
Group = "crowdsec";
SupplementaryGroups = cfg.extraGroups;
StateDirectory = "crowdsec";
};
};
};
};
users = {
users.crowdsec = {
isSystemUser = true;
home = cfg.stateDirectory;
group = "crowdsec";
};
groups = {
crowdsec = { };
};
};
};
}

View file

@ -1,87 +0,0 @@
{
flake-inputs,
pkgs,
lib,
config,
...
}:
let
inherit (flake-inputs.self.packages.${pkgs.system}) crowdsec-firewall-bouncer;
crowdsecCfg = config.security.crowdsec;
cfg = crowdsecCfg.remediationComponents.firewallBouncer;
settingsFormat = pkgs.formats.yaml { };
in
{
options.security.crowdsec.remediationComponents.firewallBouncer = {
enable = lib.mkEnableOption "cs-firewall-bouncer";
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
description = ''
The bouncer configuration. Refer to
<https://docs.crowdsec.net/u/bouncers/firewall/> for details
on supported values.
'';
};
};
config = lib.mkIf cfg.enable {
security.crowdsec.remediationComponents.firewallBouncer.settings = {
mode = lib.mkDefault "${if config.networking.nftables.enable then "nftables" else "iptables"}";
log_mode = "stdout";
iptables_chains = [ "nixos-fw" ];
# Don't let users easily override this; unfortunately we need to
# set up this key through substitution at runtime.
api_key = lib.mkForce "\${API_KEY}";
api_url = lib.mkDefault "http://${crowdsecCfg.settings.api.server.listen_uri}";
};
systemd = {
packages = [ crowdsec-firewall-bouncer ];
services = {
crowdsec-firewall-bouncer-setup = {
description = "Crowdsec firewall bouncer config preparation";
script = ''
if [ ! -e '${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml' ]; then
${crowdsecCfg.package}/bin/cscli -oraw bouncers add "cs-firewall-bouncer-$(${pkgs.coreutils}/bin/date +%s)" > \
${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml
fi
# Stdout redirection is deliberately used to forcibly
# overwrite the file if it exists
API_KEY="$(<${crowdsecCfg.stateDirectory}/firewall_bouncer_credentials.yaml)" \
${lib.getExe pkgs.envsubst} \
-i ${settingsFormat.generate "crowdsec-firewall-bouncer.yaml" cfg.settings} \
> /var/lib/crowdsec/config/crowdsec-firewall-bouncer.yaml
'';
serviceConfig = {
User = "crowdsec";
Group = "crowdsec";
Type = "oneshot";
RemainAfterExit = true;
};
};
crowdsec-firewall-bouncer = {
enable = true;
after = [ "crowdsec-firewall-bouncer-setup.service" ];
bindsTo = [ "crowdsec-firewall-bouncer-setup.service" ];
requiredBy = [ "crowdsec.service" ];
path =
lib.optionals (cfg.settings.mode == "ipset" || cfg.settings.mode == "iptables") [ pkgs.ipset ]
++ lib.optional (cfg.settings.mode == "iptables") pkgs.iptables
++ lib.optional (cfg.settings.mode == "nftables") pkgs.nftables;
};
};
};
};
}

View file

@ -1 +0,0 @@
{ imports = [ ./cs-firewall-bouncer.nix ]; }

View file

@ -1,6 +1,5 @@
{ {
imports = [ imports = [
./crowdsec
./nginxExtensions.nix ./nginxExtensions.nix
]; ];
} }

View file

@ -3,57 +3,156 @@
pkgs, pkgs,
lib, lib,
... ...
}: }: {
{
options = { options = {
services.nginx.domain = lib.mkOption { services.nginx.domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "The base domain name to append to virtual domain names"; description = "The base domain name to append to virtual domain names";
}; };
services.nginx.virtualHosts = services.nginx.virtualHosts = let
let autheliaDomain = "auth.${config.services.nginx.domain}";
extraVirtualHostOptions = extraLocationOptions = {config, ...}: {
{ name, config, ... }: options = {
{ enableAutheliaProxy = lib.mkEnableOption "Enable recommended authelia proxy settings";
options = { enableAuthorization = lib.mkEnableOption "Enable authorization via authelia";
enableHSTS = lib.mkEnableOption "Enable HSTS"; };
addAccessLog = lib.mkOption { config = {
type = lib.types.bool; recommendedProxySettings = lib.mkIf config.enableAutheliaProxy false;
default = true;
description = ''
Add special logging to `/var/log/nginx/''${serverName}`
'';
};
};
config = { extraConfig = lib.concatStringsSep "\n" [
extraConfig = lib.concatStringsSep "\n" [ (lib.optionalString config.enableAutheliaProxy ''
(lib.optionalString config.enableHSTS '' proxy_set_header Host $host;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
'') proxy_set_header X-Forwarded-Proto $scheme;
(lib.optionalString config.addAccessLog '' proxy_set_header X-Forwarded-Host $http_host;
access_log /var/log/nginx/${name}/access.log upstream_time; proxy_set_header X-Forwarded-URI $request_uri;
'') proxy_set_header X-Forwarded-Ssl on;
]; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
client_body_buffer_size 128k;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
proxy_buffers 64 256k;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
send_timeout 5m;
proxy_read_timeout 360;
proxy_send_timeout 360;
proxy_connect_timeout 360;
'')
(lib.optionalString config.enableAuthorization ''
auth_request /authelia;
set_escape_uri $target_url $scheme://$http_host$request_uri;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Email $email;
proxy_set_header Remote-Name $name;
error_page 401 =302 https://${autheliaDomain}/?rd=$target_url;
'')
];
};
};
extraVirtualHostOptions = {
name,
config,
...
}: {
options = {
enableAuthorization = lib.mkEnableOption "Enable authorization via authelia";
enableHSTS = lib.mkEnableOption "Enable HSTS";
addAccessLog = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Add special logging to `/var/log/nginx/''${serverName}`
'';
};
locations = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule extraLocationOptions);
};
};
config = {
extraConfig = lib.concatStringsSep "\n" [
(lib.optionalString config.enableHSTS ''
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
'')
(lib.optionalString config.addAccessLog ''
access_log /var/log/nginx/${name}/access.log upstream_time;
'')
];
locations = lib.mkIf config.enableAuthorization {
"/".enableAuthorization = true;
"/authelia" = {
proxyPass = "http://127.0.0.1:9091/api/verify";
recommendedProxySettings = false;
extraConfig = ''
internal;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
proxy_pass_request_body off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
proxy_buffers 4 32k;
client_body_buffer_size 128k;
send_timeout 5m;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_connect_timeout 240;
'';
}; };
}; };
in };
lib.mkOption { type = lib.types.attrsOf (lib.types.submodule extraVirtualHostOptions); }; };
in
lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule extraVirtualHostOptions);
};
}; };
config = { config = {
# Don't attempt to run acme if the domain name is not tlater.net # Don't attempt to run acme if the domain name is not tlater.net
systemd.services = systemd.services = let
let confirm = ''[[ "tlater.net" = ${config.services.nginx.domain} ]]'';
confirm = ''[[ "tlater.net" = ${config.services.nginx.domain} ]]''; in
in lib.mapAttrs' (cert: _:
lib.mapAttrs' (
cert: _:
lib.nameValuePair "acme-${cert}" { lib.nameValuePair "acme-${cert}" {
serviceConfig.ExecCondition = ''${pkgs.runtimeShell} -c '${confirm}' ''; serviceConfig.ExecCondition = ''${pkgs.runtimeShell} -c '${confirm}' '';
} })
) config.security.acme.certs; config.security.acme.certs;
}; };
} }

View file

@ -0,0 +1,86 @@
{
"bookmarks": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "bookmarks",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-JXNQNnWXoii71QhtKktuEBEIqzmONVetULBhpSjM9xo=",
"type": "tarball",
"url": "https://github.com/nextcloud/bookmarks/releases/download/v13.1.3/bookmarks-13.1.3.tar.gz"
},
"version": "13.1.3"
},
"calendar": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "calendar",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-hZfjWAMi/0qs5xMMgOlcoSXG6kcZ2aeDaez+NqSZFKI=",
"type": "tarball",
"url": "https://github.com/nextcloud-releases/calendar/releases/download/v4.6.7/calendar-v4.6.7.tar.gz"
},
"version": "v4.6.7"
},
"contacts": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "contacts",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-HCEjiAqn6sTNXKW6O5X6Ta9Ll4ehvzmGZUj1c0ue2Xc=",
"type": "tarball",
"url": "https://github.com/nextcloud-releases/contacts/releases/download/v5.5.3/contacts-v5.5.3.tar.gz"
},
"version": "v5.5.3"
},
"cookbook": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "cookbook",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-TE/w8SgyIPaGl5wZUAsG234nxoPj25QoRPF3zjbMoRk=",
"type": "tarball",
"url": "https://github.com/christianlupus-nextcloud/cookbook-releases/releases/download/v0.10.5/Cookbook-0.10.5.tar.gz"
},
"version": "0.10.5"
},
"news": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "news",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-cfJkKRNSz15L4E3w1tnEb+t4MrVwVzb8lb6vCOA4cK4=",
"type": "tarball",
"url": "https://github.com/nextcloud/news/releases/download/24.0.0/news.tar.gz"
},
"version": "24.0.0"
},
"notes": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "notes",
"passthru": null,
"pinned": false,
"src": {
"sha256": "sha256-ydpxatwuZUz7XIgK8FMklZlxNQklpsP8Uqpxvt3iK0k=",
"type": "tarball",
"url": "https://github.com/nextcloud/notes/releases/download/v4.10.0/notes.tar.gz"
},
"version": "v4.10.0"
}
}

View file

@ -0,0 +1,52 @@
# This file was generated by nvfetcher, please do not modify it manually.
{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
{
bookmarks = {
pname = "bookmarks";
version = "13.1.3";
src = fetchTarball {
url = "https://github.com/nextcloud/bookmarks/releases/download/v13.1.3/bookmarks-13.1.3.tar.gz";
sha256 = "sha256-JXNQNnWXoii71QhtKktuEBEIqzmONVetULBhpSjM9xo=";
};
};
calendar = {
pname = "calendar";
version = "v4.6.7";
src = fetchTarball {
url = "https://github.com/nextcloud-releases/calendar/releases/download/v4.6.7/calendar-v4.6.7.tar.gz";
sha256 = "sha256-hZfjWAMi/0qs5xMMgOlcoSXG6kcZ2aeDaez+NqSZFKI=";
};
};
contacts = {
pname = "contacts";
version = "v5.5.3";
src = fetchTarball {
url = "https://github.com/nextcloud-releases/contacts/releases/download/v5.5.3/contacts-v5.5.3.tar.gz";
sha256 = "sha256-HCEjiAqn6sTNXKW6O5X6Ta9Ll4ehvzmGZUj1c0ue2Xc=";
};
};
cookbook = {
pname = "cookbook";
version = "0.10.5";
src = fetchTarball {
url = "https://github.com/christianlupus-nextcloud/cookbook-releases/releases/download/v0.10.5/Cookbook-0.10.5.tar.gz";
sha256 = "sha256-TE/w8SgyIPaGl5wZUAsG234nxoPj25QoRPF3zjbMoRk=";
};
};
news = {
pname = "news";
version = "24.0.0";
src = fetchTarball {
url = "https://github.com/nextcloud/news/releases/download/24.0.0/news.tar.gz";
sha256 = "sha256-cfJkKRNSz15L4E3w1tnEb+t4MrVwVzb8lb6vCOA4cK4=";
};
};
notes = {
pname = "notes";
version = "v4.10.0";
src = fetchTarball {
url = "https://github.com/nextcloud/notes/releases/download/v4.10.0/notes.tar.gz";
sha256 = "sha256-ydpxatwuZUz7XIgK8FMklZlxNQklpsP8Uqpxvt3iK0k=";
};
};
}

View file

@ -0,0 +1,21 @@
{
"prometheus-fail2ban-exporter": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "prometheus-fail2ban-exporter",
"passthru": null,
"pinned": false,
"src": {
"deepClone": false,
"fetchSubmodules": false,
"leaveDotGit": false,
"name": null,
"rev": "v0.10.1",
"sha256": "sha256-zGEhDy3uXIbvx4agSA8Mx7bRtiZZtoDZGbNbHc9L+yI=",
"type": "git",
"url": "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter"
},
"version": "v0.10.1"
}
}

View file

@ -0,0 +1,16 @@
# This file was generated by nvfetcher, please do not modify it manually.
{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
{
prometheus-fail2ban-exporter = {
pname = "prometheus-fail2ban-exporter";
version = "v0.10.1";
src = fetchgit {
url = "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter";
rev = "v0.10.1";
fetchSubmodules = false;
deepClone = false;
leaveDotGit = false;
sha256 = "sha256-zGEhDy3uXIbvx4agSA8Mx7bRtiZZtoDZGbNbHc9L+yI=";
};
};
}

1430
pkgs/afvalcalendar/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
[package]
name = "afvalcalendar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
icalendar = "0.16.0"
serde = { version = "1.0.195", features = ["derive"] }
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.11", features = ["cookies", "json"] }
chrono = { version = "0.4.34", features = ["serde"] }
serde_json = "1.0.114"
serde_repr = "0.1.18"
hostname = "0.3.1"

View file

@ -0,0 +1,20 @@
{
pkgs,
rustPlatform,
...
}:
rustPlatform.buildRustPackage {
pname = "afvalcalendar";
version = "0.1.0";
src = ./.;
nativeBuildInputs = with pkgs; [
pkg-config
];
buildInputs = with pkgs; [
openssl
];
cargoHash = "sha256-JXx6aUKdKbUTBCwlBw5i1hZy8ofCfSrhLCwFzqdA8cI=";
}

View file

@ -0,0 +1,43 @@
use chrono::{Duration, NaiveDate};
use icalendar::{Alarm, Calendar, Component, Event, EventLike, Property};
use crate::trash::TrashType;
pub(crate) fn calendar_from_pickup_dates(dates: Vec<(TrashType, NaiveDate)>) -> Calendar {
let mut ical = Calendar::new();
ical.name("Twente Milieu Afvalkalender");
let events = dates.iter().map(|date| {
let description = match date.0 {
TrashType::Grey => "Restafval wordt opgehaald",
TrashType::Green => "GFT wordt opgehaald",
TrashType::Paper => "Papier wordt opgehaald",
TrashType::Packages => "Verpakkingen worden opgehaald",
};
let color = Property::new(
"COLOR",
match date.0 {
TrashType::Grey => "darkgray",
TrashType::Green => "darkgreen",
TrashType::Paper => "royalblue",
TrashType::Packages => "darkorange",
},
);
let reminder = Alarm::display(description, -Duration::hours(5));
Event::new()
.all_day(date.1)
.summary(description)
.append_property(color)
.alarm(reminder)
.done()
});
for event in events {
ical.push(event);
}
ical.done()
}

View file

@ -0,0 +1,15 @@
mod calendar;
mod trash;
#[tokio::main]
async fn main() {
match trash::get_pickup_dates().await {
Ok(dates) => {
let calendar = calendar::calendar_from_pickup_dates(dates);
calendar.print().unwrap();
}
Err(error) => {
eprintln!("{}", error);
}
}
}

View file

@ -0,0 +1,59 @@
use chrono::{Months, NaiveDate, NaiveDateTime, Utc};
use serde::Deserialize;
use serde_repr::Deserialize_repr;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct CalendarAPIDatum {
pickup_dates: Vec<NaiveDateTime>,
pickup_type: TrashType,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct CalendarAPIResponse {
data_list: Vec<CalendarAPIDatum>,
}
#[derive(Copy, Clone, Deserialize_repr, Debug)]
#[repr(u8)]
pub(crate) enum TrashType {
Grey = 0,
Green = 1,
Paper = 2,
Packages = 10,
}
pub(crate) async fn get_pickup_dates() -> Result<Vec<(TrashType, NaiveDate)>, reqwest::Error> {
let today = Utc::now().date_naive();
let next_month = (today + Months::new(1)).to_string();
let today = today.to_string();
let client = reqwest::Client::new();
let params = [
("companyCode", "8d97bb56-5afd-4cbc-a651-b4f7314264b4"),
("uniqueAddressID", "1300002485"),
("startDate", &today),
("endDate", &next_month),
];
let calendar = client
.post("https://twentemilieuapi.ximmio.com/api/GetCalendar")
.form(&params)
.send()
.await?
.json::<CalendarAPIResponse>()
.await?;
Ok(calendar
.data_list
.iter()
.flat_map(|datum| {
datum
.pickup_dates
.iter()
.map(|date| (datum.pickup_type, NaiveDate::from(*date)))
})
.collect::<Vec<(TrashType, NaiveDate)>>())
}

View file

@ -0,0 +1,4 @@
POST https://twentemilieuapi.ximmio.com/api/GetCalendar
Content-Type: application/x-www-form-urlencoded
companyCode=8d97bb56-5afd-4cbc-a651-b4f7314264b4&uniqueAddressID=1300002485&startDate=2024-02-01&endDate=2024-02-29

View file

@ -1,42 +0,0 @@
{
"crowdsec-firewall-bouncer": {
"cargoLocks": null,
"date": null,
"extract": null,
"name": "crowdsec-firewall-bouncer",
"passthru": null,
"pinned": false,
"src": {
"deepClone": false,
"fetchSubmodules": false,
"leaveDotGit": false,
"name": null,
"owner": "crowdsecurity",
"repo": "cs-firewall-bouncer",
"rev": "v0.0.31",
"sha256": "sha256-59MWll8v00CF4WA53gjHZSTFc8hpYaHENg9O7LgTCrA=",
"type": "github"
},
"version": "v0.0.31"
},
"crowdsec-hub": {
"cargoLocks": null,
"date": "2025-05-17",
"extract": null,
"name": "crowdsec-hub",
"passthru": null,
"pinned": false,
"src": {
"deepClone": false,
"fetchSubmodules": false,
"leaveDotGit": false,
"name": null,
"owner": "crowdsecurity",
"repo": "hub",
"rev": "850614b9fcd4298f559b422c5ac685a69aa2e5ff",
"sha256": "sha256-96MMwFN5KongQA3YJVSuk7Kanbr1gR94CCyiflmez2k=",
"type": "github"
},
"version": "850614b9fcd4298f559b422c5ac685a69aa2e5ff"
}
}

View file

@ -1,27 +0,0 @@
# This file was generated by nvfetcher, please do not modify it manually.
{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
{
crowdsec-firewall-bouncer = {
pname = "crowdsec-firewall-bouncer";
version = "v0.0.31";
src = fetchFromGitHub {
owner = "crowdsecurity";
repo = "cs-firewall-bouncer";
rev = "v0.0.31";
fetchSubmodules = false;
sha256 = "sha256-59MWll8v00CF4WA53gjHZSTFc8hpYaHENg9O7LgTCrA=";
};
};
crowdsec-hub = {
pname = "crowdsec-hub";
version = "850614b9fcd4298f559b422c5ac685a69aa2e5ff";
src = fetchFromGitHub {
owner = "crowdsecurity";
repo = "hub";
rev = "850614b9fcd4298f559b422c5ac685a69aa2e5ff";
fetchSubmodules = false;
sha256 = "sha256-96MMwFN5KongQA3YJVSuk7Kanbr1gR94CCyiflmez2k=";
};
date = "2025-05-17";
};
}

View file

@ -1,9 +0,0 @@
{ pkgs }:
let
sources = pkgs.callPackage ./_sources/generated.nix { };
callPackage = pkgs.lib.callPackageWith (pkgs // { inherit sources; });
in
{
hub = callPackage ./hub.nix { };
firewall-bouncer = callPackage ./firewall-bouncer.nix { };
}

View file

@ -1,26 +0,0 @@
{
lib,
sources,
buildGoModule,
envsubst,
coreutils,
}:
let
envsubstBin = lib.getExe envsubst;
in
buildGoModule {
inherit (sources.crowdsec-firewall-bouncer) pname version src;
vendorHash = "sha256-7Jxvg8UEjUxnIz1llvXyI2AefJ31OVdNzhWD/C8wU/Y=";
postInstall = ''
mkdir -p $out/lib/systemd/system
CFG=/var/lib/crowdsec/config BIN=$out/bin/cs-firewall-bouncer ${envsubstBin} \
-i ./config/crowdsec-firewall-bouncer.service \
-o $out/lib/systemd/system/crowdsec-firewall-bouncer.service
substituteInPlace $out/lib/systemd/system/crowdsec-firewall-bouncer.service \
--replace-fail /bin/sleep ${coreutils}/bin/sleep
'';
}

View file

@ -1 +0,0 @@
{ sources }: sources.crowdsec-hub.src

View file

@ -1,7 +0,0 @@
[crowdsec-hub]
src.git = "https://github.com/crowdsecurity/hub.git"
fetch.github = "crowdsecurity/hub"
[crowdsec-firewall-bouncer]
src.github = "crowdsecurity/cs-firewall-bouncer"
fetch.github = "crowdsecurity/cs-firewall-bouncer"

View file

@ -1,5 +1,22 @@
{ pkgs }:
{ {
crowdsec = import ./crowdsec { inherit pkgs; }; pkgs,
starbound = pkgs.callPackage ./starbound { }; lib,
} }: let
inherit (builtins) fromJSON mapAttrs readFile;
inherit (pkgs) callPackage;
in
{
starbound = callPackage ./starbound {};
prometheus-fail2ban-exporter = callPackage ./prometheus/fail2ban-exporter.nix {
sources = pkgs.callPackage ./_sources_pkgs/generated.nix {};
};
afvalcalendar = callPackage ./afvalcalendar {};
}
// (
# Add nextcloud apps
let
mkNextcloudApp = pkgs.callPackage ./mkNextcloudApp.nix {};
sources = fromJSON (readFile ./_sources_nextcloud/generated.json);
in
mapAttrs (_: source: mkNextcloudApp source) sources
)

9
pkgs/mkNextcloudApp.nix Normal file
View file

@ -0,0 +1,9 @@
{
fetchNextcloudApp,
lib,
}: source:
fetchNextcloudApp {
url = source.src.url;
sha256 = source.src.sha256;
license = "unlicense"; # Blatant lie
}

31
pkgs/nextcloud-apps.toml Normal file
View file

@ -0,0 +1,31 @@
[bookmarks]
# src.github = "nextcloud/bookmarks"
src.prefix = "v"
src.manual = "v13.1.3"
fetch.tarball = "https://github.com/nextcloud/bookmarks/releases/download/v$ver/bookmarks-$ver.tar.gz"
[calendar]
# src.github = "nextcloud-releases/calendar"
src.manual = "v4.6.7"
fetch.tarball = "https://github.com/nextcloud-releases/calendar/releases/download/$ver/calendar-$ver.tar.gz"
[contacts]
# src.github = "nextcloud-releases/contacts"
src.manual = "v5.5.3"
fetch.tarball = "https://github.com/nextcloud-releases/contacts/releases/download/$ver/contacts-$ver.tar.gz"
[cookbook]
# src.github = "christianlupus-nextcloud/cookbook-releases"
src.prefix = "v"
src.manual = "0.10.5"
fetch.tarball = "https://github.com/christianlupus-nextcloud/cookbook-releases/releases/download/v$ver/Cookbook-$ver.tar.gz"
[news]
# src.github = "nextcloud/news"
# Update to 25 when angular rewrite is done/the alpha when I need to switch to nextcloud 28+
src.manual = "24.0.0"
fetch.tarball = "https://github.com/nextcloud/news/releases/download/$ver/news.tar.gz"
[notes]
src.github = "nextcloud/notes"
fetch.tarball = "https://github.com/nextcloud/notes/releases/download/$ver/notes.tar.gz"

3
pkgs/nvfetcher.toml Normal file
View file

@ -0,0 +1,3 @@
[prometheus-fail2ban-exporter]
src.manual = "v0.10.1" # No gitlab support in nvfetcher
fetch.git = "https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter"

View file

@ -0,0 +1,8 @@
{
buildGoModule,
sources,
}:
buildGoModule {
inherit (sources.prometheus-fail2ban-exporter) pname src version;
vendorHash = "sha256-5o8p5p0U/c0WAIV5dACnWA3ThzSh2tt5LIFMb59i9GY=";
}

View file

@ -5,33 +5,30 @@
patchelf, patchelf,
steamPackages, steamPackages,
replace-secret, replace-secret,
}: }: let
let
# Use the directory in which starbound is installed so steamcmd # Use the directory in which starbound is installed so steamcmd
# doesn't have to be reinstalled constantly (we're using DynamicUser # doesn't have to be reinstalled constantly (we're using DynamicUser
# with StateDirectory to persist this). # with StateDirectory to persist this).
steamcmd = steamPackages.steamcmd.override { steamRoot = "/var/lib/starbound/.steamcmd"; }; steamcmd = steamPackages.steamcmd.override {
wrapperPath = lib.makeBinPath [ steamRoot = "/var/lib/starbound/.steamcmd";
patchelf };
steamcmd wrapperPath = lib.makeBinPath [patchelf steamcmd replace-secret];
replace-secret
];
in in
stdenv.mkDerivation { stdenv.mkDerivation {
name = "starbound-update-script"; name = "starbound-update-script";
nativeBuildInputs = [ makeWrapper ]; nativeBuildInputs = [makeWrapper];
dontUnpack = true; dontUnpack = true;
patchPhase = '' patchPhase = ''
interpreter="$(cat $NIX_CC/nix-support/dynamic-linker)" interpreter="$(cat $NIX_CC/nix-support/dynamic-linker)"
substitute ${./launch-starbound.sh} launch-starbound --subst-var interpreter substitute ${./launch-starbound.sh} launch-starbound --subst-var interpreter
''; '';
installPhase = '' installPhase = ''
mkdir -p $out/bin mkdir -p $out/bin
cp launch-starbound $out/bin/launch-starbound cp launch-starbound $out/bin/launch-starbound
chmod +x $out/bin/launch-starbound chmod +x $out/bin/launch-starbound
''; '';
postFixup = '' postFixup = ''
wrapProgram $out/bin/launch-starbound \ wrapProgram $out/bin/launch-starbound \
--prefix PATH : "${wrapperPath}" --prefix PATH : "${wrapperPath}"
''; '';
} }