This commit is contained in:
Daniel Bulant 2026-09-02 13:21:55 +02:00
parent 49c9a6290e
commit 6c5b7e5a87
No known key found for this signature in database
2 changed files with 495 additions and 3 deletions

View file

@ -220,7 +220,7 @@
# Export sysbox NixOS module for external use # Export sysbox NixOS module for external use
nixosModules.sysbox = import ./modules/sysbox.nix; nixosModules.sysbox = import ./modules/sysbox.nix;
nixosModules.tuwunel-admin = import ./modules/tuwunel-admin.nix; nixosModules.tuwunel-admin = import ./modules/tuwunel-admin.nix;
#nixosModules.budkyber = import ./modules/budkyber.nix; #nixosModules.adctf = import ./modules/adctf.nix;
packages.x86_64-linux = rec { packages.x86_64-linux = rec {
tuwunel-admin = tuwunel-admin =
@ -237,10 +237,10 @@
paseo.nixosModules.paseo paseo.nixosModules.paseo
determinate.nixosModules.default determinate.nixosModules.default
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
#./modules/budkyber.nix #./modules/adctf.nix
{ {
services.hypr-kdeconnect-fix.enable = true; services.hypr-kdeconnect-fix.enable = true;
#services.budkyber.enable = true; #services.adctf.enable = true;
home-manager.extraSpecialArgs = attrs; home-manager.extraSpecialArgs = attrs;
home-manager.useGlobalPkgs = true; home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true; home-manager.useUserPackages = true;

492
modules/adctf.nix Normal file
View file

@ -0,0 +1,492 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.adctf;
inherit (lib)
concatMapStringsSep
escapeShellArg
mapAttrs'
mkEnableOption
mkIf
mkMerge
mkOption
nameValuePair
optionalAttrs
types
;
compose = lib.getExe pkgs.docker-compose;
portOverlays = {
collector = pkgs.writeText "adctf-collector-ports.yml" ''
services:
collector:
ports: [ "127.0.0.1:6256:6256" ]
'';
grafana = pkgs.writeText "adctf-grafana-ports.yml" ''
services:
grafana:
ports: [ "127.0.0.1:6003:6003" ]
'';
loki = pkgs.writeText "adctf-loki-ports.yml" ''
services:
loki:
ports: [ "127.0.0.1:6004:6004" ]
alloy:
ports: [ "127.0.0.1:6005:6005" ]
'';
prometheus = pkgs.writeText "adctf-prometheus-ports.yml" ''
services:
prometheus:
ports: [ "127.0.0.1:9090:9090" ]
'';
cloudbeaver = pkgs.writeText "adctf-cloudbeaver-ports.yml" ''
services:
bober:
ports: [ "127.0.0.1:8978:8978" ]
'';
};
statekOverlay = pkgs.writeText "adctf-statek-overlay.yml" ''
services:
db:
networks:
statek:
cct: { aliases: [ statek_db ] }
cct6: { aliases: [ statek_db ] }
scoreboard:
networks:
statek:
cct: { aliases: [ statek_scoreboard ] }
cct6: { aliases: [ statek_scoreboard ] }
attackinfo:
networks:
statek:
cct: { aliases: [ statek_attackinfo ] }
cct6: { aliases: [ statek_attackinfo ] }
submitter:
networks:
statek:
cct: { aliases: [ statek_submitter ] }
cct6: { aliases: [ statek_submitter ] }
api:
networks:
statek:
aliases: [ api ]
cct: { aliases: [ statek_api ] }
cct6: { aliases: [ statek_api ] }
frontend:
networks:
statek:
cct: { aliases: [ statek_frontend ] }
cct6: { aliases: [ statek_frontend ] }
networks:
cct: { name: cct, external: true }
cct6: { name: cct6, external: true }
'';
tulipOverlay = pkgs.writeText "adctf-tulip-overlay.yml" ''
services:
timescale:
networks:
internal:
cct: { aliases: [ tulip_timescale ] }
cct6: { aliases: [ tulip_timescale ] }
frontend:
ports: !override [ "127.0.0.1:3000:3000" ]
networks:
internal:
cct: { aliases: [ tulip_frontend ] }
cct6: { aliases: [ tulip_frontend ] }
api:
networks:
internal:
cct: { aliases: [ tulip_api ] }
cct6: { aliases: [ tulip_api ] }
flagids:
networks:
internal:
cct: { aliases: [ tulip_flagids ] }
cct6: { aliases: [ tulip_flagids ] }
assembler:
networks:
internal:
cct: { aliases: [ tulip_assembler ] }
cct6: { aliases: [ tulip_assembler ] }
enricher:
networks:
internal:
cct: { aliases: [ tulip_enricher ] }
cct6: { aliases: [ tulip_enricher ] }
networks:
cct: { name: cct, external: true }
cct6: { name: cct6, external: true }
'';
infrastructureStacks = {
collector = {
directory = "${cfg.infrastructureRoot}/collector";
files = [
"${cfg.infrastructureRoot}/collector/docker-compose.yml"
portOverlays.collector
];
};
grafana = {
directory = "${cfg.infrastructureRoot}/grafana";
files = [
"${cfg.infrastructureRoot}/grafana/docker-compose.yml"
portOverlays.grafana
];
};
loki = {
directory = "${cfg.infrastructureRoot}/loki-adctf";
files = [
"${cfg.infrastructureRoot}/loki-adctf/docker-compose.yml"
portOverlays.loki
];
};
prometheus = {
directory = "${cfg.infrastructureRoot}/prometheus";
files = [
"${cfg.infrastructureRoot}/prometheus/docker-compose.yml"
portOverlays.prometheus
];
};
suricata = {
directory = "${cfg.infrastructureRoot}/suricata";
files = [ "${cfg.infrastructureRoot}/suricata/docker-compose.yml" ];
};
};
applicationStacks = {
statek = {
directory = cfg.statekRoot;
files = [
"${cfg.statekRoot}/compose.yml"
statekOverlay
];
};
tulip = {
directory = cfg.tulipRoot;
files = [
"${cfg.tulipRoot}/compose.yml"
tulipOverlay
];
environment = {
TRAFFIC_DIR_HOST = "${cfg.infrastructureRoot}/traffic";
TRAFFIC_DIR_DOCKER = "/traffic";
};
};
};
cloudbeaverStack = {
cloudbeaver = {
directory = "${cfg.infrastructureRoot}/other/cloudbeaver";
files = [
"${cfg.infrastructureRoot}/other/cloudbeaver/docker-compose.yml"
portOverlays.cloudbeaver
];
};
};
stacks =
infrastructureStacks
// applicationStacks
// optionalAttrs cfg.cloudbeaver.enable cloudbeaverStack;
proxyPorts = {
collector = 6256;
grafana = 6003;
loki = 6004;
alloy = 6005;
prometheus = 9090;
statek = 5173;
statek-api = 8080;
tulip = 3000;
}
// optionalAttrs cfg.cloudbeaver.enable { cloudbeaver = 8978; };
proxyHosts = mapAttrs' (
name: port:
nameValuePair "${name}.${cfg.proxy.baseDomain}:80" {
extraConfig = "reverse_proxy http://127.0.0.1:${toString port}";
}
) proxyPorts;
composeCommand = name: stack:
"${compose} --project-name ${escapeShellArg "adctf-${name}"} "
+ concatMapStringsSep " " (file: "-f ${escapeShellArg file}") stack.files;
mkComposeService = name: stack:
nameValuePair "adctf-${name}" {
description = "adctf ${name} containers";
wantedBy = [ "multi-user.target" ];
requires = [
"docker.service"
"adctf-networks.service"
];
after = [
"docker.service"
"adctf-networks.service"
"network-online.target"
];
wants = [ "network-online.target" ];
path = [ pkgs.coreutils ];
environment = stack.environment or { };
script = ''
test -f ${escapeShellArg (builtins.head stack.files)}
${composeCommand name stack} up --detach --build --remove-orphans
'';
preStop = ''
${composeCommand name stack} down
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
WorkingDirectory = stack.directory;
TimeoutStartSec = "infinity";
TimeoutStopSec = "5min";
};
};
in
{
options.services.adctf = {
enable = mkEnableOption "the adctf attack-defense infrastructure";
user = mkOption {
type = types.str;
default = "dan";
description = "Local user allowed to manage the container and VM runtimes.";
};
infrastructureRoot = mkOption {
type = types.str;
default = "/home/dan/projects/ad-infrastructure-private";
description = "Absolute path to the ad-infrastructure-private checkout.";
};
statekRoot = mkOption {
type = types.str;
default = "/home/dan/projects/statek";
description = "Absolute path to the Statek checkout.";
};
tulipRoot = mkOption {
type = types.str;
default = "/home/dan/projects/tulip-private";
description = "Absolute path to the Tulip checkout.";
};
cloudbeaver.enable = mkOption {
type = types.bool;
default = true;
description = "Start the optional CloudBeaver database UI.";
};
proxy = {
enable = mkOption {
type = types.bool;
default = true;
description = "Publish adctf HTTP services through Caddy.";
};
baseDomain = mkOption {
type = types.str;
default = "fern.danbulant.cloud";
description = "Base domain below which each service gets its own subdomain.";
};
};
cgroup = {
reserveCpuPercent = mkOption {
type = types.ints.between 0 99;
default = 50;
description = "Percentage of one logical CPU reserved outside docker.slice.";
};
reserveMemoryMiB = mkOption {
type = types.ints.positive;
default = 1024;
description = "Physical memory reserved outside docker.slice.";
};
reserveSwapMiB = mkOption {
type = types.ints.unsigned;
default = 1024;
description = "Swap reserved outside docker.slice when swap is available.";
};
};
virtualMachines = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable libvirt/QEMU management for a qcow vulnbox image.";
};
virtualbox.enable = mkOption {
type = types.bool;
default = true;
description = "Enable VirtualBox as a fallback for VirtualBox-formatted vulnbox images.";
};
};
};
config = mkIf cfg.enable (mkMerge [
{
assertions = [
{
assertion = lib.hasPrefix "/" cfg.infrastructureRoot;
message = "services.adctf.infrastructureRoot must be an absolute path";
}
{
assertion = lib.hasPrefix "/" cfg.statekRoot;
message = "services.adctf.statekRoot must be an absolute path";
}
{
assertion = lib.hasPrefix "/" cfg.tulipRoot;
message = "services.adctf.tulipRoot must be an absolute path";
}
];
virtualisation.docker = {
enable = true;
daemon.settings."cgroup-parent" = "docker.slice";
};
environment.systemPackages = [
pkgs.docker-compose
];
systemd.tmpfiles.rules = [
"d ${cfg.infrastructureRoot}/traffic 0775 ${cfg.user} users -"
];
users.users.${cfg.user}.extraGroups = [ "docker" ];
systemd.slices.docker = {
description = "Docker container resource budget";
sliceConfig = {
CPUAccounting = true;
MemoryAccounting = true;
};
};
systemd.services = {
docker = {
requires = [ "adctf-cgroup-limits.service" ];
after = [ "adctf-cgroup-limits.service" ];
wantedBy = [ "multi-user.target" ];
};
adctf-cgroup-limits = {
description = "Reserve host resources outside docker.slice";
before = [ "docker.service" ];
path = [
pkgs.coreutils
pkgs.systemd
];
script = ''
logical_cpus="$(nproc --all)"
cpu_quota="$((logical_cpus * 100 - ${toString cfg.cgroup.reserveCpuPercent}))"
memory_total_kib=0
swap_total_kib=0
while read -r key value _; do
case "$key" in
MemTotal:) memory_total_kib="$value" ;;
SwapTotal:) swap_total_kib="$value" ;;
esac
done < /proc/meminfo
reserve_memory_kib=$((${toString cfg.cgroup.reserveMemoryMiB} * 1024))
if ((memory_total_kib <= reserve_memory_kib)); then
echo "Cannot reserve ${toString cfg.cgroup.reserveMemoryMiB} MiB from $((memory_total_kib / 1024)) MiB of physical memory" >&2
exit 1
fi
memory_max_kib="$((memory_total_kib - reserve_memory_kib))"
reserve_swap_kib=$((${toString cfg.cgroup.reserveSwapMiB} * 1024))
if ((swap_total_kib > reserve_swap_kib)); then
swap_max_kib="$((swap_total_kib - reserve_swap_kib))"
else
swap_max_kib=0
fi
systemctl set-property --runtime docker.slice \
CPUQuota="''${cpu_quota}%" \
MemoryMax="''${memory_max_kib}K" \
MemorySwapMax="''${swap_max_kib}K"
'';
preStop = ''
systemctl set-property --runtime docker.slice \
CPUQuota=infinity MemoryMax=infinity MemorySwapMax=infinity
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
adctf-networks = {
description = "Create adctf container networks";
requires = [ "docker.service" ];
after = [ "docker.service" ];
path = [ config.virtualisation.docker.package ];
script = ''
docker network inspect cct >/dev/null 2>&1 || \
docker network create \
--gateway 10.66.0.1 \
--ip-range 10.66.0.0/16 \
--subnet 10.66.0.0/16 \
cct
docker network inspect cct6 >/dev/null 2>&1 || \
docker network create \
--ipv6 \
--subnet 2001:db8:1::/64 \
cct6
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
}
// mapAttrs' mkComposeService stacks;
services.caddy = mkIf cfg.proxy.enable {
enable = true;
virtualHosts = proxyHosts;
};
}
(mkIf cfg.virtualMachines.enable {
virtualisation.libvirtd.enable = true;
programs.virt-manager.enable = true;
users.users.${cfg.user}.extraGroups = [
"kvm"
"libvirtd"
];
environment.systemPackages = with pkgs; [
qemu
quickemu
virt-viewer
];
})
(mkIf cfg.virtualMachines.virtualbox.enable {
virtualisation.virtualbox.host = {
enable = true;
enableKvm = true;
addNetworkInterface = false;
};
users.users.${cfg.user}.extraGroups = [ "vboxusers" ];
})
]);
}