first commit

This commit is contained in:
2026-05-05 08:30:51 +02:00
commit c0e781bf00
107 changed files with 7024 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{
config,
lib,
pkgs,
...
}: {
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
./disko-config.nix
];
boot.loader.grub = {
efiSupport = true;
efiInstallAsRemovable = true;
};
swapDevices = [
{
device = "/var/lib/swapfile";
size = 16 * 1024;
}
];
networking.hostName = "AZ-CLD-1"; # Define your hostname.
# Pick only one of the below networking options.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Set your time zone.
time.timeZone = "Europe/Berlin";
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Select internationalisation properties.
i18n.defaultLocale = "de_DE.UTF-8";
# console = {
# font = "Lat2-Terminus16";
# keyMap = "us";
# useXkbConfig = true; # use xkb.options in tty.
# };
# Enable the X11 windowing system.
# services.xserver.enable = true;
# Configure keymap in X11
# services.xserver.xkb.layout = "us";
# services.xserver.xkb.options = "eurosign:e,caps:escape";
# Enable CUPS to print documents.
# services.printing.enable = true;
# Enable sound.
# services.pulseaudio.enable = true;
# OR
# services.pipewire = {
# enable = true;
# pulse.enable = true;
# };
# Enable touchpad support (enabled default in most desktopManager).
# services.libinput.enable = true;
# Define a user account. Don't forget to set a password with passwd.
# users.users.alice = {
# isNormalUser = true;
# extraGroups = [ "wheel" ]; # Enable sudo for the user.
# packages = with pkgs; [
# tree
# ];
# };
# programs.firefox.enable = true;
# List packages installed in system profile.
# You can use https://search.nixos.org/ to find more packages (and options).
environment.systemPackages = with pkgs; [
neovim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
git
];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
};
# List services that you want to enable:
# Enable the OpenSSH daemon.
services.openssh = {
enable = true;
ports = [2022];
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [587];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment?
}

View File

@@ -0,0 +1,12 @@
{
imports = [
../common
./configuration.nix
./secrets.nix
./services
];
extraServices = {
podman.enable = true;
};
}

View File

@@ -0,0 +1,39 @@
{
disko.devices = {
disk = {
main = {
type = "disk";
device = "/dev/vda"; # CHANGE ME
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02"; # for GRUB MBR
priority = 1;
};
esp = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = ["defaults" "umask=0077"];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = ["noatime" "nodiratime" "discard"];
};
};
};
};
};
};
};
}

View File

@@ -0,0 +1,28 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
pkgs,
modulesPath,
...
}: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = ["ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod"];
boot.initrd.kernelModules = [];
boot.kernelModules = [];
boot.extraModulePackages = [];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens18.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}

View File

@@ -0,0 +1,81 @@
{
age = {
secrets = {
traefik-env = {
file = ../../secrets/traefik-env.age;
};
baserow-env = {
file = ../../secrets/baserow-env.age;
};
librechat = {
file = ../../secrets/librechat.age;
};
librechat-env = {
file = ../../secrets/librechat-env.age;
};
librechat-env-dev = {
file = ../../secrets/librechat-env-dev.age;
};
librechat-env-prod = {
file = ../../secrets/librechat-env-prod.age;
};
litellm-env = {
file = ../../secrets/litellm-env.age;
};
metabase-env = {
file = ../../secrets/metabase-env.age;
};
n8n-env = {
file = ../../secrets/n8n-env.age;
};
netbird-auth-secret = {
file = ../../secrets/netbird-auth-secret.age;
};
netbird-db-password = {
file = ../../secrets/netbird-db-password.age;
};
netbird-encryption-key = {
file = ../../secrets/netbird-encryption-key.age;
};
netbird-dashboard-env = {
file = ../../secrets/netbird-dashboard-env.age;
};
netbird-server-env = {
file = ../../secrets/netbird-server-env.age;
};
netbird-proxy-env = {
file = ../../secrets/netbird-proxy-env.age;
};
outline-env = {
file = ../../secrets/outline-env.age;
owner = "outline";
};
pgadmin-pw = {
file = ../../secrets/pgadmin-pw.age;
owner = "pgadmin";
};
vaultwarden-env = {
file = ../../secrets/vaultwarden-env.age;
};
hetzner-s3-az-intern-secret-key = {
file = ../../secrets/hetzner-s3-az-intern-secret-key.age;
owner = "outline";
};
hetzner-s3-az-intern-access-key = {
file = ../../secrets/hetzner-s3-az-intern-access-key.age;
};
zammad-pw = {
file = ../../secrets/zammad-pw.age;
};
zammad-secret = {
file = ../../secrets/zammad-secret.age;
};
zammad-hr-env-prod = {
file = ../../secrets/zammad-hr-env-prod.age;
};
zammad-hr-env = {
file = ../../secrets/zammad-hr-env.age;
};
};
};
}

View File

@@ -0,0 +1,70 @@
{config, ...}: let
serviceName = "baserow";
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers.${serviceName} = {
image = "docker.io/baserow/baserow:2.1.6";
environment = {
BASEROW_AMOUNT_OF_GUNICORN_WORKERS = "4";
BASEROW_AMOUNT_OF_WORKERS = "2";
DATABASE_CONN_MAX_AGE = "60";
# Proxy: tell Django the connection is HTTPS so cookies get Secure flag
BASEROW_ENABLE_SECURE_PROXY_SSL_HEADER = "yes";
# Published apps run on different origins — allow cross-origin cookie delivery
BASEROW_FRONTEND_SAME_SITE_COOKIE = "none";
# Valid base domain for published app subdomains
BASEROW_BUILDER_DOMAINS = "az-gruppe.com";
# Disable Caddy's on_demand TLS — Traefik handles TLS termination
BASEROW_CADDY_GLOBAL_CONF = "auto_https off";
};
environmentFiles = [config.age.secrets.baserow-env.path];
ports = ["127.0.0.1:${toString servicePort}:80"];
volumes = ["baserow_data:/baserow/data"];
extraOptions = ["--add-host=postgres:10.89.0.1" "--ip=10.89.0.10" "--network=web"];
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
middlewares."${serviceName}-headers".headers = {
customRequestHeaders = {
X-Forwarded-Proto = "https";
X-Forwarded-Port = "443";
};
};
routers.${serviceName} = {
rule = "Host(`br.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
middlewares = ["${serviceName}-headers"];
};
routers.azubi = {
rule = "Host(`azubi.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
middlewares = ["${serviceName}-headers"];
};
routers.ausbilder = {
rule = "Host(`ausbilder.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
middlewares = ["${serviceName}-headers"];
};
};
}

View File

@@ -0,0 +1,20 @@
{lib, ...}: {
imports = [
./baserow.nix
./it-tools.nix
./librechat.nix
./litellm.nix
./librechat-dev.nix
./netbird.nix
./portainer.nix
./zammad-hr.nix
];
system.activationScripts.createPodmanNetworkWeb = lib.mkAfter ''
if ! /run/current-system/sw/bin/podman network exists web; then
/run/current-system/sw/bin/podman network create web --subnet=10.89.0.0/24 --internal
fi
if ! /run/current-system/sw/bin/podman network exists web-dev; then
/run/current-system/sw/bin/podman network create web-dev --subnet=10.89.1.0/24 --internal
fi
'';
}

View File

@@ -0,0 +1,27 @@
{config, ...}: let
serviceName = "it-tools";
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers.${serviceName} = {
image = "docker.io/sharevb/it-tools:latest";
ports = ["127.0.0.1:${toString servicePort}:8080"];
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`tools.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,133 @@
{
config,
pkgs,
...
}: let
serviceName = "librechat-dev";
servicePort = config.m3ta.ports.get serviceName;
ragApiDevServiceName = "rag-api-dev";
ragApiDevPort = config.m3ta.ports.get ragApiDevServiceName;
envFileDev = config.age.secrets.librechat-env-dev.path;
envFileCommon = config.age.secrets.librechat.path;
in {
virtualisation.oci-containers = {
containers.meilisearch-dev = {
image = "getmeili/meilisearch:v1.12.3";
autoStart = false;
volumes = ["librechat_dev_meili:/meili_data"];
environment = {
MEILI_HTTP_ADDR = "0.0.0.0:7700";
MEILI_NO_ANALYTICS = "true";
};
environmentFiles = [envFileDev envFileCommon];
extraOptions = ["--ip=10.89.1.20" "--network=web-dev"];
};
containers.rag_api-dev = {
image = "ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest";
autoStart = false;
environment = {
RAG_PORT = "8000";
DB_HOST = "10.89.1.1";
DB_PORT = "5432";
};
environmentFiles = [envFileDev envFileCommon];
dependsOn = ["meilisearch-dev"];
extraOptions = ["--add-host=postgres:10.89.1.1" "--ip=10.89.1.21" "--network=web-dev"];
ports = ["127.0.0.1:${toString ragApiDevPort}:8000"];
};
containers.mongodb-dev = {
image = "mongo:7";
autoStart = false;
volumes = [
"librechat_dev_mongo:/data/db"
"/var/backup/mongodb-dev:/data/backups"
];
extraOptions = ["--ip=10.89.1.22" "--network=web-dev"];
};
containers.${serviceName} = {
image = "ghcr.io/danny-avila/librechat-dev-api:latest";
autoStart = false;
ports = ["127.0.0.1:${toString servicePort}:3080"];
dependsOn = ["mongodb-dev" "rag_api-dev" "meilisearch-dev"];
environment = {
HOST = "0.0.0.0";
NODE_ENV = "development";
MONGO_URI = "mongodb://mongodb-dev:27017/LibreChatDev";
MEILI_HOST = "http://meilisearch-dev:7700";
RAG_PORT = "8000";
RAG_API_URL = "http://rag_api-dev:8000";
};
environmentFiles = [envFileDev envFileCommon];
volumes = [
"/var/lib/librechat-dev/librechat.yaml:/app/librechat.yaml:ro"
"librechat_dev_images:/app/client/public/images"
"librechat_dev_uploads:/app/uploads"
"librechat_dev_logs:/app/api/logs"
];
extraOptions = ["--ip=10.89.1.23" "--network=web-dev"];
};
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`chat-dev.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
environment.systemPackages = [
(pkgs.writeShellScriptBin "librechat-dev" ''
#!/usr/bin/env bash
set -e
SERVICES=(
podman-meilisearch-dev
podman-mongodb-dev
podman-rag_api-dev
podman-librechat-dev
)
case "$1" in
up)
echo "🚀 Starte LibreChat-Dev-Umgebung..."
for svc in "''${SERVICES[@]}"; do
sudo systemctl start "$svc"
done
;;
down)
echo "🛑 Stoppe LibreChat-Dev-Umgebung..."
for svc in "''${SERVICES[@]}"; do
sudo systemctl stop "$svc"
done
;;
restart)
echo "🔄 Neustart der LibreChat-Dev-Umgebung..."
for svc in "''${SERVICES[@]}"; do
sudo systemctl restart "$svc"
done
;;
status)
systemctl status "''${SERVICES[@]}"
;;
*)
echo "Usage: librechat-dev {up|down|restart|status}"
exit 1
;;
esac
'')
];
}

View File

@@ -0,0 +1,169 @@
{
config,
pkgs,
...
}: let
serviceName = "librechat";
servicePort = config.m3ta.ports.get serviceName;
ragApiServiceName = "rag-api";
ragApiPort = config.m3ta.ports.get ragApiServiceName;
envFileProd = config.age.secrets.librechat-env-prod.path;
envFileCommon = config.age.secrets.librechat.path;
in {
virtualisation.oci-containers = {
containers.meilisearch = {
image = "getmeili/meilisearch:v1.35.1";
autoStart = true;
volumes = ["librechat_meili:/meili_data"];
environment = {
MEILI_HTTP_ADDR = "0.0.0.0:7700";
MEILI_NO_ANALYTICS = "true";
};
environmentFiles = [envFileCommon envFileProd];
extraOptions = ["--ip=10.89.0.20" "--network=web"];
};
containers.rag_api = {
image = "registry.librechat.ai/danny-avila/librechat-rag-api-dev-lite:latest";
autoStart = true;
environment = {
RAG_PORT = "8000";
DB_HOST = "10.89.0.1";
DB_PORT = "5432";
};
environmentFiles = [envFileCommon envFileProd];
dependsOn = ["meilisearch"];
extraOptions = ["--add-host=postgres:10.89.0.1" "--ip=10.89.0.21" "--network=web"];
ports = ["127.0.0.1:${toString ragApiPort}:8000"];
};
containers.mongodb = {
image = "mongo:8.0.17";
autoStart = true;
volumes = [
"librechat_mongo:/data/db"
"/var/backup/mongodb:/data/backups"
];
# Enable auth once users exist; see Mongo auth doc.
# command = [ "mongod", "--auth" ];
extraOptions = ["--ip=10.89.0.22" "--network=web"];
};
containers.${serviceName} = {
image = "registry.librechat.ai/danny-avila/librechat-dev:latest";
autoStart = true;
user = "1000:1000";
ports = ["127.0.0.1:${toString servicePort}:3080"];
dependsOn = ["mongodb" "rag_api" "meilisearch"];
environment = {
HOST = "0.0.0.0";
NODE_ENV = "production";
# Mongo URI (start without auth; switch to mongodb://user:pass@mongodb:27017/LibreChat after Step 4)
MONGO_URI = "mongodb://mongodb:27017/LibreChat";
MEILI_HOST = "http://meilisearch:7700";
RAG_PORT = "8000";
RAG_API_URL = "http://rag_api:8000";
};
environmentFiles = [envFileCommon envFileProd];
volumes = [
# Config file still needs to be a bind mount for host management
"/var/lib/librechat/librechat.yaml:/app/librechat.yaml:ro"
# Use named volumes for application data
"librechat_images:/app/client/public/images"
"librechat_uploads:/app/uploads"
"librechat_logs:/app/api/logs"
];
extraOptions = ["--ip=10.89.0.23" "--network=web" "--dns=8.8.8.8" "--dns=8.8.4.4"];
};
};
systemd.services."mongo-backup" = {
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
BACKUP_DIR="/var/backup/mongodb"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TEMP_BACKUP="mongodb_backup_$TIMESTAMP"
ARCHIVE_NAME="mongodb_backup_$TIMESTAMP.tar.gz"
# Ensure backup directory exists with proper permissions
mkdir -p "$BACKUP_DIR"
chown root:root "$BACKUP_DIR"
chmod 750 "$BACKUP_DIR"
echo "Starting MongoDB backup at $(date)"
# Create the backup dump in container
if ${pkgs.podman}/bin/podman exec mongodb mongodump --out "/data/backups/$TEMP_BACKUP"; then
echo "MongoDB dump completed successfully"
# Create compressed archive from the backup
cd "$BACKUP_DIR"
if [ -d "$TEMP_BACKUP" ]; then
echo "Creating compressed archive: $ARCHIVE_NAME"
${pkgs.gnutar}/bin/tar --use-compress-program=${pkgs.gzip}/bin/gzip -cf "$ARCHIVE_NAME" -C . "$TEMP_BACKUP"
# Remove the uncompressed backup directory
rm -rf "$TEMP_BACKUP"
# Verify archive was created
if [ -f "$ARCHIVE_NAME" ]; then
ARCHIVE_SIZE=$(${pkgs.coreutils}/bin/du -sh "$ARCHIVE_NAME" | cut -f1)
echo "Compressed backup created: $ARCHIVE_NAME (Size: $ARCHIVE_SIZE)"
# Keep only the 2 most recent backup archives
ls -1t mongodb_backup_*.tar.gz | tail -n +3 | xargs -r rm -f
echo "Old backup archives cleaned up, keeping 2 most recent"
# List current backups
echo "Current backups:"
ls -lah mongodb_backup_*.tar.gz 2>/dev/null || echo "No previous backups found"
else
echo "ERROR: Failed to create compressed archive" >&2
exit 1
fi
else
echo "ERROR: Backup directory not found at $BACKUP_DIR/$TEMP_BACKUP" >&2
exit 1
fi
else
echo "ERROR: MongoDB backup failed" >&2
exit 1
fi
echo "MongoDB backup completed successfully at $(date)"
'';
};
systemd.timers."mongo-backup" = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "30m";
Persistent = true;
};
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`chat.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,37 @@
{config, ...}: let
serviceName = "litellm";
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers.${serviceName} = {
#image = "ghcr.io/berriai/litellm:v1.78.5-stable";
image = "docker.litellm.ai/berriai/litellm:v1.82.3-stable";
ports = ["127.0.0.1:${toString servicePort}:4000"];
environmentFiles = [config.age.secrets.litellm-env.path];
environment = {
ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True";
SCARF_NO_ANALYTICS = "True";
STORE_MODEL_IN_DB = "True";
};
volumes = ["/var/lib/litellm/config.yaml:/app/config.yaml"];
extraOptions = ["--add-host=postgres:10.89.0.1" "--ip=10.89.0.30" "--network=web"];
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`llm.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,243 @@
{
config,
pkgs,
...
}: let
serviceName = "netbird";
servicePort = config.m3ta.ports.get serviceName;
domain = "v.az-gruppe.com";
proxyDomain = "p.az-gruppe.com";
ipBase = "10.89.0";
ipOffset = 50;
# Derived IPs
gatewayIp = "${ipBase}.1";
dashboardIp = "${ipBase}.${toString ipOffset}";
serverIp = "${ipBase}.${toString (ipOffset + 1)}";
proxyIp = "${ipBase}.${toString (ipOffset + 2)}";
# Database configuration
dbName = "netbird";
dbUser = "netbird";
dbHost = gatewayIp;
# NetBird config as Nix attribute set
netbirdConfig = {
server = {
listenAddress = ":80";
exposedAddress = "https://${domain}:443";
stunPorts = [3478];
metricsPort = 9090;
healthcheckAddress = ":9000";
logLevel = "info";
logFile = "console";
dataDir = "/var/lib/netbird";
auth = {
issuer = "https://${domain}/oauth2";
localAuthDisabled = true;
signKeyRefreshEnabled = true;
dashboardRedirectURIs = [
"https://${domain}/nb-auth"
"https://${domain}/nb-silent-auth"
];
cliRedirectURIs = ["http://localhost:53000/"];
};
reverseProxy = {
trustedHTTPProxies = ["${gatewayIp}/32"];
};
# Proxy Feature
proxy = {
enabled = true;
domain = proxyDomain;
};
store = {
engine = "postgres";
postgres = {
host = dbHost;
port = 5432;
database = dbName;
username = dbUser;
};
};
};
};
# Generate YAML config
yamlFormat = pkgs.formats.yaml {};
configYamlBase = yamlFormat.generate "netbird-config-base.yaml" netbirdConfig;
# Script to inject secrets at runtime
configGenScript = pkgs.writeShellScript "netbird-gen-config" ''
set -euo pipefail
AUTH_SECRET=$(cat "$1")
DB_PASSWORD=$(cat "$2")
ENCRYPTION_KEY=$(cat "$3")
${pkgs.yq-go}/bin/yq eval "
.server.authSecret = \"$AUTH_SECRET\" |
.server.store.encryptionKey = \"$ENCRYPTION_KEY\" |
.server.store.postgres.password = \"$DB_PASSWORD\"
" ${configYamlBase}
'';
in {
age.secrets."${serviceName}-auth-secret".file = ../../../../secrets/${serviceName}-auth-secret.age;
age.secrets."${serviceName}-db-password".file = ../../../../secrets/${serviceName}-db-password.age;
age.secrets."${serviceName}-encryption-key".file = ../../../../secrets/${serviceName}-encryption-key.age;
age.secrets."${serviceName}-dashboard-env".file = ../../../../secrets/${serviceName}-dashboard-env.age;
age.secrets."${serviceName}-server-env".file = ../../../../secrets/${serviceName}-server-env.age;
age.secrets."${serviceName}-proxy-env".file = ../../../../secrets/${serviceName}-proxy-env.age;
# Systemd oneshot service to generate config with secrets
systemd.services."${serviceName}-config" = {
description = "Generate NetBird config with secrets";
wantedBy = ["multi-user.target"];
before = ["podman-${serviceName}-server.service"];
requiredBy = ["podman-${serviceName}-server.service"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "netbird-write-config" ''
mkdir -p /var/lib/${serviceName}
${configGenScript} \
${config.age.secrets."${serviceName}-auth-secret".path} \
${config.age.secrets."${serviceName}-db-password".path} \
${config.age.secrets."${serviceName}-encryption-key".path} \
> /var/lib/${serviceName}/config.yaml
chmod 600 /var/lib/${serviceName}/config.yaml
'';
};
};
virtualisation.oci-containers.containers = {
"${serviceName}-dashboard" = {
image = "netbirdio/dashboard:latest";
autoStart = true;
ports = ["127.0.0.1:${toString servicePort}:80"];
environmentFiles = [config.age.secrets."${serviceName}-dashboard-env".path];
extraOptions = [
"--ip=${dashboardIp}"
"--network=web"
];
};
"${serviceName}-server" = {
image = "netbirdio/netbird-server:latest";
autoStart = true;
ports = ["3478:3478/udp"];
environmentFiles = [config.age.secrets."${serviceName}-server-env".path];
volumes = [
"${serviceName}_data:/var/lib/netbird"
"/var/lib/${serviceName}/config.yaml:/etc/netbird/config.yaml:ro"
];
cmd = ["--config" "/etc/netbird/config.yaml"];
extraOptions = [
"--ip=${serverIp}"
"--network=web"
];
};
"${serviceName}-proxy" = {
image = "netbirdio/reverse-proxy:latest";
autoStart = true;
ports = ["51820:51820/udp"];
volumes = [
"${serviceName}_proxy_certs:/certs"
];
environmentFiles = [config.age.secrets."${serviceName}-proxy-env".path];
cmd = [
"--domain=${proxyDomain}"
"--mgmt=https://${domain}:443"
"--addr=:8443"
"--cert-dir=/certs"
"--acme-certs"
"--trusted-proxies=${gatewayIp}/32"
];
dependsOn = ["${serviceName}-server"];
extraOptions = [
"--ip=${proxyIp}"
"--network=web"
];
};
};
services.traefik.dynamicConfigOptions = {
# HTTP services and routers
http = {
services = {
"${serviceName}-dashboard".loadBalancer.servers = [
{url = "http://localhost:${toString servicePort}/";}
];
"${serviceName}-server".loadBalancer.servers = [
{url = "http://${serverIp}:80/";}
];
"${serviceName}-server-h2c".loadBalancer.servers = [
{url = "h2c://${serverIp}:80";}
];
};
routers = {
# gRPC (Signal + Management)
"${serviceName}-grpc" = {
rule = "Host(`${domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))";
entrypoints = "websecure";
tls.certResolver = "ionos";
service = "${serviceName}-server-h2c";
priority = 100;
};
# Backend (relay, WebSocket, API, OAuth2)
"${serviceName}-backend" = {
rule = "Host(`${domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))";
entrypoints = "websecure";
tls.certResolver = "ionos";
service = "${serviceName}-server";
priority = 100;
};
# Dashboard (catch-all, lowest priority)
"${serviceName}-dashboard" = {
rule = "Host(`${domain}`)";
entrypoints = "websecure";
tls.certResolver = "ionos";
service = "${serviceName}-dashboard";
priority = 1;
};
};
};
# TCP for proxy TLS passthrough
tcp = {
services."${serviceName}-proxy-tls".loadBalancer.servers = [
{address = "${proxyIp}:8443";}
];
routers."${serviceName}-proxy-passthrough" = {
entryPoints = ["websecure"];
rule = "HostSNI(`*`)";
service = "${serviceName}-proxy-tls";
priority = 1;
tls.passthrough = true;
};
};
# ServersTransport for proxy protocol v2 (optional)
serversTransports."pp-v2" = {
proxyProtocol.version = 2;
};
};
networking.firewall.allowedUDPPorts = [
3478 # STUN
51820 # WireGuard for proxy
];
}

View File

@@ -0,0 +1,32 @@
{config, ...}: let
serviceName = "portainer";
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers.${serviceName} = {
image = "docker.io/portainer/portainer-ce:latest";
ports = ["127.0.0.1:${toString servicePort}:9000"];
volumes = [
"/etc/localtime:/etc/localtime:ro"
"/run/podman/podman.sock:/var/run/docker.sock:ro"
"portainer_data:/data"
];
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`pt.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,296 @@
{
config,
pkgs,
...
}: let
instanceName = "hr";
serviceName = "zammad-${instanceName}";
servicePort = config.m3ta.ports.get serviceName;
elasticsearchServiceName = "${serviceName}-elasticsearch";
elasticsearchPort = config.m3ta.ports.get elasticsearchServiceName;
envFileProd = config.age.secrets."${serviceName}-env-prod".path;
envFileCommon = config.age.secrets."${serviceName}-env".path;
zammadVersion = "6.5.2-22";
zammadImage = "ghcr.io/zammad/zammad:${zammadVersion}";
ipBase = "10.89.0";
ipOffset = 40;
# Domain-Konfiguration
zammadDomain = "hr-ticket.az-gruppe.com";
sharedEnvironment = {
MEMCACHE_SERVERS = "zammad-memcached:11211";
POSTGRESQL_DB = "zammad_${instanceName}";
POSTGRESQL_HOST = "10.89.0.1";
POSTGRESQL_USER = "zammad_${instanceName}";
POSTGRESQL_PORT = "5432";
POSTGRESQL_OPTIONS = "?pool=50";
REDIS_URL = "redis://zammad-redis:6379";
TZ = "Europe/Berlin";
BACKUP_DIR = "/var/tmp/zammad";
BACKUP_TIME = "03:00";
HOLD_DAYS = "10";
ELASTICSEARCH_ENABLED = "true";
ELASTICSEARCH_HOST = "zammad-elasticsearch";
ELASTICSEARCH_PORT = "9200";
ELASTICSEARCH_NAMESPACE = "zammad_${instanceName}";
NGINX_PORT = "8080";
# CSRF & Reverse Proxy Settings
NGINX_SERVER_SCHEME = "https";
NGINX_SERVER_NAME = zammadDomain;
ZAMMAD_HTTP_TYPE = "https";
ZAMMAD_FQDN = zammadDomain;
RAILS_TRUSTED_PROXIES = "['127.0.0.1', '::1', '10.89.0.0/24']";
};
in {
virtualisation.oci-containers = {
containers."${serviceName}-elasticsearch" = {
image = "elasticsearch:8.19.6";
autoStart = true;
volumes = ["${serviceName}_elasticsearch:/usr/share/elasticsearch/data"];
environment = {
"discovery.type" = "single-node";
"xpack.security.enabled" = "false";
ES_JAVA_OPTS = "-Xms1g -Xmx1g";
};
extraOptions = [
"--ip=${ipBase}.${toString ipOffset}"
"--network=web"
"--network-alias=zammad-elasticsearch"
];
ports = ["127.0.0.1:${toString elasticsearchPort}:9200"];
};
containers."${serviceName}-memcached" = {
image = "memcached:1.6.39-alpine";
autoStart = true;
cmd = ["memcached" "-m" "256M"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 1)}"
"--network=web"
"--network-alias=zammad-memcached"
];
};
containers."${serviceName}-redis" = {
image = "redis:7.4.6-alpine";
autoStart = true;
volumes = ["${serviceName}_redis:/data"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 2)}"
"--network=web"
"--network-alias=zammad-redis"
];
};
containers."${serviceName}-railsserver" = {
image = zammadImage;
autoStart = true;
cmd = ["zammad-railsserver"];
environment = sharedEnvironment;
environmentFiles = [envFileCommon envFileProd];
volumes = ["${serviceName}_storage:/opt/zammad/storage"];
dependsOn = ["${serviceName}-memcached" "${serviceName}-redis" "${serviceName}-elasticsearch"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 4)}"
"--network=web"
"--add-host=postgres:10.89.0.1"
"--network-alias=zammad-railsserver"
];
};
containers."${serviceName}-scheduler" = {
image = zammadImage;
autoStart = true;
cmd = ["zammad-scheduler"];
environment = sharedEnvironment;
environmentFiles = [envFileCommon envFileProd];
volumes = ["${serviceName}_storage:/opt/zammad/storage"];
dependsOn = ["${serviceName}-memcached" "${serviceName}-redis"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 5)}"
"--network=web"
"--add-host=postgres:10.89.0.1"
];
};
containers."${serviceName}-websocket" = {
image = zammadImage;
autoStart = true;
cmd = ["zammad-websocket"];
environment = sharedEnvironment;
environmentFiles = [envFileCommon envFileProd];
volumes = ["${serviceName}_storage:/opt/zammad/storage"];
dependsOn = ["${serviceName}-memcached" "${serviceName}-redis"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 6)}"
"--network=web"
"--add-host=postgres:10.89.0.1"
"--network-alias=zammad-websocket"
];
};
containers."${serviceName}-nginx" = {
image = zammadImage;
autoStart = true;
cmd = ["zammad-nginx"];
environment = sharedEnvironment;
environmentFiles = [envFileCommon envFileProd];
volumes = ["${serviceName}_storage:/opt/zammad/storage"];
dependsOn = ["${serviceName}-railsserver"];
ports = ["127.0.0.1:${toString servicePort}:8080"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 7)}"
"--network=web"
"--add-host=postgres:10.89.0.1"
];
};
containers."${serviceName}-backup" = {
image = zammadImage;
autoStart = true;
cmd = ["zammad-backup"];
environment = sharedEnvironment;
environmentFiles = [envFileCommon envFileProd];
volumes = [
"${serviceName}_storage:/opt/zammad/storage:ro"
"/var/backup/${serviceName}:/var/tmp/zammad:rw"
];
dependsOn = ["${serviceName}-memcached" "${serviceName}-redis"];
extraOptions = [
"--ip=${ipBase}.${toString (ipOffset + 8)}"
"--network=web"
"--add-host=postgres:10.89.0.1"
"--user=0:0"
];
};
};
# Init als oneshot systemd-Service
systemd.services."${serviceName}-init" = {
description = "Zammad ${instanceName} Database Initialization";
after = [
"podman-${serviceName}-memcached.service"
"podman-${serviceName}-redis.service"
"podman-${serviceName}-elasticsearch.service"
];
requires = [
"podman-${serviceName}-memcached.service"
"podman-${serviceName}-redis.service"
];
wantedBy = [];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
echo "Starting Zammad ${instanceName} database initialization..."
${pkgs.podman}/bin/podman run --rm \
--name ${serviceName}-init-oneshot \
--network web \
--ip ${ipBase}.${toString (ipOffset + 3)} \
--add-host=postgres:10.89.0.1 \
--user 0:0 \
--env-file ${envFileCommon} \
--env-file ${envFileProd} \
--env MEMCACHE_SERVERS=zammad-memcached:11211 \
--env POSTGRESQL_DB=zammad_${instanceName} \
--env POSTGRESQL_HOST=10.89.0.1 \
--env POSTGRESQL_USER=zammad_${instanceName} \
--env POSTGRESQL_PORT=5432 \
--env POSTGRESQL_OPTIONS='?pool=50' \
--env REDIS_URL=redis://zammad-redis:6379 \
--env TZ=Europe/Berlin \
--env ELASTICSEARCH_ENABLED=true \
--env ELASTICSEARCH_HOST=zammad-elasticsearch \
--env ELASTICSEARCH_PORT=9200 \
--env ELASTICSEARCH_NAMESPACE=zammad_${instanceName} \
--env NGINX_SERVER_SCHEME=https \
--env NGINX_SERVER_NAME=${zammadDomain} \
--env ZAMMAD_HTTP_TYPE=https \
--env ZAMMAD_FQDN=${zammadDomain} \
-v ${serviceName}_storage:/opt/zammad/storage \
${zammadImage} \
zammad-init
echo "Zammad ${instanceName} initialization completed successfully"
'';
};
# Backup retention service
systemd.services."${serviceName}-backup-cleanup" = {
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
BACKUP_DIR="/var/backup/${serviceName}"
HOLD_DAYS=10
echo "Starting ${serviceName} backup cleanup at $(date)"
mkdir -p "$BACKUP_DIR"
chown root:root "$BACKUP_DIR"
chmod 750 "$BACKUP_DIR"
${pkgs.findutils}/bin/find "$BACKUP_DIR" -type f -name "*.gz" -mtime +$HOLD_DAYS -delete
echo "Current backups:"
ls -lah "$BACKUP_DIR" || echo "No backups found"
echo "${serviceName} backup cleanup completed at $(date)"
'';
};
systemd.timers."${serviceName}-backup-cleanup" = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = "*-*-* 04:00:00";
RandomizedDelaySec = "30m";
Persistent = true;
};
};
# Traefik configuration with proper headers
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
middlewares."${serviceName}-headers".headers = {
customRequestHeaders = {
X-Forwarded-Proto = "https";
X-Forwarded-Port = "443";
X-Forwarded-Host = zammadDomain;
X-Real-IP = "";
};
};
routers.${serviceName} = {
rule = "Host(`${zammadDomain}`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
middlewares = ["${serviceName}-headers"];
};
};
}

View File

@@ -0,0 +1,18 @@
{
imports = [
./containers
./gitea.nix
./gotenberg.nix
./metabase.nix
./n8n.nix
./netbird.nix
./ntfy.nix
./outline.nix
./postgres.nix
./traefik.nix
./vaultwarden.nix
./zugferd.nix
# ./zammad.nix
];
}

View File

@@ -0,0 +1,41 @@
{config, ...}: let
serviceName = "gitea";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
settings = {
server = {
ROOT_URL = "https://git.az-gruppe.com";
HTTP_PORT = servicePort;
};
mailer.SENDMAIL_PATH = "/run/wrappers/bin/sendmail";
service.DISABLE_REGISTRATION = true;
};
lfs.enable = true;
dump = {
enable = true;
type = "tar.gz";
interval = "03:30:00";
backupDir = "/var/backup/gitea";
};
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`git.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,10 @@
{config, ...}: let
serviceName = "gotenberg";
servicePort = config.m3ta.ports.get serviceName;
in {
services.gotenberg = {
enable = true;
port = servicePort;
bindIP = "127.0.0.1";
};
}

View File

@@ -0,0 +1,31 @@
{config, ...}: let
serviceName = "metabase";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
listen.port = servicePort;
};
systemd.services.${serviceName}.serviceConfig = {
EnvironmentFile = config.age.secrets.metabase-env.path;
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`kpi.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,31 @@
{config, ...}: let
serviceName = "n8n";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
environment.WEBHOOK_URL = "https://wf.az-gruppe.com";
};
systemd.services.${serviceName}.serviceConfig = {
EnvironmentFile = config.age.secrets.n8n-env.path;
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`wf.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,31 @@
{pkgs, ...}: {
services.netbird = {
enable = true;
package = pkgs.unstable.netbird;
};
systemd.services.netbird = {
environment = {
NB_DISABLE_SSH_CONFIG = "true";
};
path = [
pkgs.shadow
pkgs.util-linux
];
};
programs.ssh.extraConfig = ''
Match exec "${pkgs.netbird}/bin/netbird ssh detect %h %p"
PreferredAuthentications password,publickey,keyboard-interactive
PasswordAuthentication yes
PubkeyAuthentication yes
BatchMode no
ProxyCommand ${pkgs.netbird}/bin/netbird ssh proxy %h %p
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
CheckHostIP no
LogLevel ERROR
'';
networking.firewall.checkReversePath = "loose";
}

View File

@@ -0,0 +1,32 @@
{config, ...}: let
serviceName = "ntfy-sh";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
settings = {
base-url = "https://ping.az-gruppe.com";
listen-http = ":${toString servicePort}";
auth-file = "/var/lib/ntfy-sh/user.db";
auth-default-access = "deny-all";
};
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`ping.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,41 @@
{config, ...}: let
serviceName = "outline";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
port = servicePort;
publicUrl = "https://wiki.az-gruppe.com";
databaseUrl = "postgresql://outline:outline@127.0.0.1:5432/outline";
storage = {
storageType = "s3";
region = "eu-central";
uploadBucketUrl = "https://nbg1.your-objectstorage.com";
uploadBucketName = "az-wiki";
secretKeyFile = config.age.secrets.hetzner-s3-az-intern-secret-key.path;
accessKey = "CRT7V4HR5CG9NHICD2WW";
};
};
systemd.services.${serviceName}.serviceConfig = {
EnvironmentFile = config.age.secrets.outline-env.path;
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`wiki.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,182 @@
{
config,
pkgs,
...
}: let
serviceName = "pgadmin";
pgadminPort = config.m3ta.ports.get serviceName;
in {
services.postgresql = {
enable = true;
enableTCPIP = true;
package = pkgs.postgresql_17;
settings = {
ssl = true;
max_connections = 200;
shared_buffers = "4GB";
superuser_reserved_connections = 5;
idle_in_transaction_session_timeout = "10min";
idle_session_timeout = "2h";
tcp_keepalives_idle = 60;
tcp_keepalives_interval = 10;
tcp_keepalives_count = 6;
deadlock_timeout = "1s";
authentication_timeout = "30s";
log_connections = true;
log_disconnections = true;
log_lock_waits = true;
};
extensions = with pkgs.postgresql17Packages; [
pgvector
];
initialScript = pkgs.writeText "backend-initScript" ''
CREATE USER baserow WITH ENCRYPTED PASSWORD 'baserow';
CREATE DATABASE baserow;
ALTER DATABASE baserow OWNER to baserow;
ALTER DATABASE baserow CONNECTION LIMIT 60;
CREATE USER kestra WITH ENCRYPTED PASSWORD 'kestra';
CREATE DATABASE kestra;
ALTER DATABASE kestra OWNER to kestra;
ALTER DATABASE kestra CONNECTION LIMIT 10;
CREATE USER librechat_rag WITH ENCRYPTED PASSWORD 'librechat_rag';
CREATE DATABASE librechat_rag;
ALTER DATABASE librechat_rag OWNER to librechat_rag;
ALTER DATABASE librechat_rag CONNECTION LIMIT 20;
CREATE USER librechat_rag_dev WITH ENCRYPTED PASSWORD 'librechat_rag_dev';
CREATE DATABASE librechat_rag_dev;
ALTER DATABASE librechat_rag_dev OWNER to librechat_rag_dev;
ALTER DATABASE librechat_rag_dev CONNECTION LIMIT 10;
CREATE USER metabase WITH ENCRYPTED PASSWORD 'metabase';
CREATE DATABASE metabase;
ALTER DATABASE metabase OWNER to metabase;
ALTER DATABASE metabase CONNECTION LIMIT 15;
CREATE USER n8n WITH ENCRYPTED PASSWORD 'n8n';
CREATE DATABASE n8n;
ALTER DATABASE n8n OWNER to n8n;
ALTER DATABASE n8n CONNECTION LIMIT 5;
CREATE USER outline WITH ENCRYPTED PASSWORD 'outline';
CREATE DATABASE outline;
ALTER DATABASE outline OWNER to outline;
ALTER DATABASE outline CONNECTION LIMIT 5;
CREATE USER vaultwarden WITH ENCRYPTED PASSWORD 'vaultwarden';
CREATE DATABASE vaultwarden;
ALTER DATABASE vaultwarden OWNER to vaultwarden;
ALTER DATABASE vaultwarden CONNECTION LIMIT 20;
CREATE USER zammad-hr WITH ENCRYPTED PASSWORD 'zammad-hr';
CREATE DATABASE zammad-hr;
ALTER DATABASE zammad-hr OWNER to zammad-hr;
ALTER DATABASE zammad-hr CONNECTION LIMIT 50;
-- Group roles (NOLOGIN, for permission management)
CREATE ROLE admin NOLOGIN;
CREATE ROLE dba NOLOGIN;
-- Personal login roles
CREATE USER sascha_koenig WITH ENCRYPTED PASSWORD 'sascha_koenig';
GRANT admin TO sascha_koenig;
CREATE USER jannik_mueller WITH ENCRYPTED PASSWORD 'jannik_mueller';
GRANT admin TO jannik_mueller;
'';
authentication = pkgs.lib.mkOverride 10 ''
# Local connections (Unix socket)
local all postgres peer
local all sascha_koenig scram-sha-256
local all jannik_mueller scram-sha-256
local az_test az_test scram-sha-256
local metabase,az_kpi_raw metabase scram-sha-256
local n8n n8n scram-sha-256
local outline outline scram-sha-256
local vaultwarden vaultwarden scram-sha-256
local zammad zammad scram-sha-256
# Localhost connections (IPv4 and IPv6)
host all postgres 127.0.0.1/32 scram-sha-256
host all postgres ::1/128 scram-sha-256
host all sascha_koenig 127.0.0.1/32 scram-sha-256
host all sascha_koenig ::1/128 scram-sha-256
host all jannik_mueller 127.0.0.1/32 scram-sha-256
host all jannik_mueller ::1/128 scram-sha-256
host az_test az_test 127.0.0.1/32 scram-sha-256
host az_test az_test ::1/128 scram-sha-256
host outline outline 127.0.0.1/32 scram-sha-256
host outline outline ::1/128 scram-sha-256
host metabase,az_kpi_raw metabase 127.0.0.1/32 scram-sha-256
host metabase,az_kpi_raw metabase ::1/128 scram-sha-256
host n8n n8n 127.0.0.1/32 scram-sha-256
host n8n n8n ::1/128 scram-sha-256
host vaultwarden vaultwarden 127.0.0.1/32 scram-sha-256
host vaultwarden vaultwarden ::1/128 scram-sha-256
host zammad zammad 127.0.0.1/32 scram-sha-256
host zammad zammad ::1/128 scram-sha-256
# Podman network connections for Baserow
host baserow baserow 10.89.0.0/24 scram-sha-256
host kestra kestra 10.89.0.0/24 scram-sha-256
host librechat_rag librechat_rag 10.89.0.0/24 scram-sha-256
host librechat_rag_dev librechat_rag_dev 10.89.1.0/24 scram-sha-256
host zammad_hr zammad_hr 10.89.0.0/24 scram-sha-256
host postgres zammad_hr 10.89.0.0/24 scram-sha-256
host litellm litellm 10.89.0.0/24 scram-sha-256
host netbird netbird 10.89.0.0/24 scram-sha-256
# Netbird network connections
host az_kpi_raw kestra_prm 100.91.49.26/32 scram-sha-256
# Deny all other connections
local all all reject
host all all 0.0.0.0/0 reject
host all all ::/0 reject
'';
};
services.postgresqlBackup = {
enable = true;
startAt = "03:10:00";
databases = ["az_kpi_raw" "baserow" "kestra" "librechat_rag" "litellm" "metabase" "n8n" "outline" "vaultwarden" "zammad" "zammad_hr"];
};
services.pgadmin = {
enable = true;
initialPasswordFile = "${config.age.secrets.pgadmin-pw.path}";
initialEmail = "sascha.koenig@azintec.com";
};
# Traefik configuration specific to pgadmin
services.traefik.dynamicConfigOptions.http = {
services.pgadmin.loadBalancer.servers = [{url = "http://localhost:${toString pgadminPort}/";}];
routers.pgadmin = {
rule = "Host(`pg.az-gruppe.com`)";
tls.certResolver = "ionos";
service = "pgadmin";
entrypoints = "websecure";
};
};
networking.firewall = {
extraCommands = ''
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 5432 -j ACCEPT
iptables -A INPUT -p tcp -s 10.89.0.0/24 --dport 5432 -j ACCEPT
iptables -A INPUT -p tcp -s 10.89.1.0/24 --dport 5432 -j ACCEPT
iptables -A INPUT -p tcp -s 100.91.49.26/32 --dport 5432 -j ACCEPT
'';
};
}

View File

@@ -0,0 +1,77 @@
{config, ...}: let
httpPort = config.m3ta.ports.get "traefik";
httpsPort = config.m3ta.ports.get "traefik-ssl";
in {
services.traefik = {
enable = true;
staticConfigOptions = {
log = {level = "WARN";};
certificatesResolvers = {
ionos = {
acme = {
email = "sascha.koenig@azintec.com";
storage = "/var/lib/traefik/acme.json";
caserver = "https://acme-v02.api.letsencrypt.org/directory";
dnsChallenge = {
provider = "ionos";
resolvers = ["1.1.1.1:53" "8.8.8.8:53"];
propagation = {
delayBeforeChecks = 60;
disableChecks = true;
};
};
};
};
};
api = {};
entryPoints = {
web = {
address = ":${toString httpPort}";
http.redirections.entryPoint = {
to = "websecure";
scheme = "https";
};
};
websecure = {
address = ":${toString httpsPort}";
};
};
};
dynamicConfigOptions = {
http = {
services = {
dummy = {
loadBalancer.servers = [
{url = "http://192.168.0.1";} # Diese URL wird nie verwendet
];
};
};
middlewares = {
auth = {
basicAuth = {
users = ["sascha.koenig:$apr1$1xqdta2b$DIVNvvp5iTUGNccJjguKh."];
};
};
};
routers = {
api = {
rule = "Host(`r.az-gruppe.com`)";
service = "api@internal";
middlewares = ["auth"];
entrypoints = ["websecure"];
tls = {
certResolver = "ionos";
};
};
};
};
};
};
systemd.services.traefik.serviceConfig = {
EnvironmentFile = ["${config.age.secrets.traefik-env.path}"];
};
networking.firewall.allowedTCPPorts = [httpPort httpsPort];
}

View File

@@ -0,0 +1,32 @@
{config, ...}: let
serviceName = "vaultwarden";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
dbBackend = "postgresql";
config = {
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = servicePort;
};
environmentFile = config.age.secrets.vaultwarden-env.path;
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`pw.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,35 @@
{config, ...}: let
serviceName = "zammad";
servicePort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
openPorts = false;
port = servicePort;
secretKeyBaseFile = config.age.secrets.zammad-secret.path;
database = {
createLocally = false;
port = 5432;
host = "127.0.0.1";
passwordFile = config.age.secrets.zammad-pw.path;
};
};
# Traefik configuration
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [
{
url = "http://localhost:${toString servicePort}/";
}
];
routers.${serviceName} = {
rule = "Host(`help.az-gruppe.com`)";
tls = {
certResolver = "ionos";
};
service = serviceName;
entrypoints = "websecure";
};
};
}

View File

@@ -0,0 +1,10 @@
{config, ...}: let
serviceName = "zugferd-service";
zugferdPort = config.m3ta.ports.get serviceName;
in {
services.${serviceName} = {
enable = true;
port = zugferdPort;
host = "127.0.0.1";
};
}