hswaw/machines/customs: check in code.hackerspace.pl/vuko/customs

Change-Id: Ic698cce2ef0060a54b195cf90574696b8be1eb0f
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1162
Reviewed-by: informatic <informatic@hackerspace.pl>
diff --git a/hswaw/machines/OWNERS b/hswaw/machines/OWNERS
new file mode 100644
index 0000000..c1ce290
--- /dev/null
+++ b/hswaw/machines/OWNERS
@@ -0,0 +1,4 @@
+inherited: false
+owners:
+  - informatic
+  - vuko
diff --git a/hswaw/machines/customs.hackerspace.pl/checkinator-repo.json b/hswaw/machines/customs.hackerspace.pl/checkinator-repo.json
new file mode 100644
index 0000000..d870142
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/checkinator-repo.json
@@ -0,0 +1,5 @@
+{
+    "url": "http://code.hackerspace.pl/checkinator",
+    "rev": "713c7e6c1a8fd6147522c1a5e3067898a1d8bf7a",
+    "sha256": "1vhz9jd0hfa0d1hihgkarf6w7z8yqvz4dzk42wzwk0rs25qlcavi"
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix b/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix
new file mode 100644
index 0000000..44ab46d
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix
@@ -0,0 +1,71 @@
+{ pkgs, ... }:
+
+let
+  old-pkgs = import (fetchTarball {
+    sha256 = "0kdx3pz0l422d0vvvj3h8mnq65jcg2scb13dc1z1lg2a8cln842z";
+    url = https://api.github.com/repos/NixOS/nixpkgs/tarball/0bf298df24f721a7f85c580339fb7eeff64b927c;
+  }) { config = pkgs.config; };
+
+  repo = pkgs.fetchgit (builtins.fromJSON
+    (builtins.readFile ./checkinator-repo.json));
+  checkinator = old-pkgs.callPackage "${repo}/default.nix" {};
+
+  name = "checkinator-tracker";
+  user = name;
+  group = name;
+  socket_dir = "/run/${name}/";
+
+  prepare = pkgs.writeShellScriptBin "${name}-prepare" ''
+    rm -rf /mnt/secrets/${name}
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=500 --directory /mnt/secrets/${name}
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=400 -t /mnt/secrets/${name} \
+      /etc/nixos/secrets/${name}/ca.pem \
+      /etc/nixos/secrets/${name}/cert.pem \
+      /etc/nixos/secrets/${name}/key.pem
+
+    rm -rf ${socket_dir}
+    mkdir --mode=700 ${socket_dir}
+    ${pkgs.acl}/bin/setfacl -m "u:${user}:rwx" ${socket_dir}
+    ${pkgs.acl}/bin/setfacl -m "u:checkinator-web:rx" ${socket_dir}
+  '';
+  config = builtins.toFile "${name}-config.yaml" (pkgs.lib.generators.toYAML {} {
+    # path to dhcpd lease file 
+    LEASE_FILE = "/var/lib/dhcp/dhcpd.leases";
+
+    # timeout for old leases
+    TIMEOUT = 1500;
+
+    # optional - local trusted socket
+    GRPC_UNIX_SOCKET = "${socket_dir}/checkinator.sock";
+
+    # optional - remote authenticated (TLS cert) socket 
+    GRPC_TLS_CERT_DIR = "/mnt/secrets/checkinator-tracker";
+    GRPC_TLS_CA_CERT = "/mnt/secrets/checkinator-tracker/ca.pem";
+    GRPC_TLS_ADDRESS = "[::]:2847";
+  });
+in {
+  users.users."${user}" = {
+    group           = "${group}";
+    useDefaultShell = true;
+  };
+  users.groups."${group}" = {};
+
+  systemd.services."${name}" = {
+    description = "Hackerspace Checkinator";
+    wantedBy    = [ "multi-user.target" ];
+
+    serviceConfig.User = "${user}";
+    serviceConfig.Type = "simple";
+      
+    serviceConfig.ExecStartPre = [
+      ''!${prepare}/bin/${name}-prepare''
+    ];
+    serviceConfig.ExecStart = "${checkinator}/bin/checkinator-tracker ${config}";
+    serviceConfig.ExecStopPost = [
+      ''!${pkgs.coreutils}/bin/rm -rf /mnt/secrets/${name}''
+      ''!${pkgs.coreutils}/bin/rm -rf ${socket_dir}''
+    ];
+
+  };
+  environment.systemPackages = [ checkinator ];
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/checkinator-web.nix b/hswaw/machines/customs.hackerspace.pl/checkinator-web.nix
new file mode 100644
index 0000000..c8b2542
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/checkinator-web.nix
@@ -0,0 +1,134 @@
+{ pkgs, ... }:
+
+let
+  old-pkgs = import (fetchTarball {
+    sha256 = "0kdx3pz0l422d0vvvj3h8mnq65jcg2scb13dc1z1lg2a8cln842z";
+    url = https://api.github.com/repos/NixOS/nixpkgs/tarball/0bf298df24f721a7f85c580339fb7eeff64b927c;
+  }) { config = pkgs.config; };
+
+  repo = pkgs.fetchgit (builtins.fromJSON
+    (builtins.readFile ./checkinator-repo.json));
+  checkinator = old-pkgs.callPackage "${repo}/default.nix" {};
+
+  name = "checkinator-web";
+  user = name;
+  group = name;
+  socket_dir = "/run/${name}/";
+
+  python = old-pkgs.python3.withPackages (ppackages: with ppackages;  [
+    checkinator
+    old-pkgs.python3Packages.gunicorn
+  ]);
+
+  prepare = pkgs.writeShellScriptBin "${name}-prepare" ''
+    rm -rf /mnt/secrets/${name}
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=500 --directory /mnt/secrets/${name}
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=400 -t /mnt/secrets/${name} \
+      /etc/nixos/secrets/${name}/secrets.yaml \
+      /etc/nixos/secrets/${name}/ca.pem \
+      /etc/nixos/secrets/${name}/cert.pem \
+      /etc/nixos/secrets/${name}/key.pem
+
+    ${pkgs.coreutils}/bin/mkdir -m 700 -p /var/checkinator-web/
+    ${pkgs.coreutils}/bin/chown ${user} /var/checkinator-web/
+
+    mkdir -p --mode=700 ${socket_dir}
+    chown ${user} ${socket_dir}
+    chmod 700 ${socket_dir}
+    ${pkgs.acl}/bin/setfacl -m "u:nginx:rx" ${socket_dir}
+  '';
+
+  config = builtins.toFile "${name}-config.yaml" (pkgs.lib.generators.toYAML {} {
+    # local sqlite db for storing user and MAC 
+    DB = "/var/checkinator-web/at.db";
+    
+    # debug option interpreted by flask app
+    DEBUG = false;
+    
+    # url to member wiki page
+    # "${login}" string is replaced by member login (uid)
+    WIKI_URL = "https://wiki.hackerspace.pl/people:\${login}:start";
+    
+    CLAIMABLE_PREFIXES = [
+      "10.8.0."
+      "2a0d:eb00:4242:0:"
+    ];
+    CLAIMABLE_EXCLUDE = [ ];
+    
+    SPACEAUTH_CONSUMER_KEY = "checkinator";
+    SECRETS_FILE = "/mnt/secrets/checkinator-web/secrets.yaml";
+    
+    SPECIAL_DEVICES = {
+      kektops = [ "90:e6:ba:84" ];
+      esps = [
+         "ec:fa:bc" "dc:4f:22" "d8:a0:1d" "b4:e6:2d" "ac:d0:74" "a4:7b:9d"
+         "a0:20:a6" "90:97:d5" "68:c6:3a" "60:01:94" "5c:cf:7f" "54:5a:a6"
+         "30:ae:a4" "2c:3a:e8" "24:b2:de" "24:0a:c4" "18:fe:34" "38:2b:78"
+         "bc:dd:c2" "cc:50:e3" "84:0d:8e"
+      ];
+      vms = [
+        "52:54:00" # craptrap VMs
+      ];
+    };
+    
+    PROXY_FIX = true;
+    
+    GRPC_TLS_CERT_DIR = "/mnt/secrets/checkinator-web";
+    GRPC_TLS_CA_CERT = "/mnt/secrets/checkinator-web/ca.pem";
+    GRPC_TLS_ADDRESS = "[::1]:2847";
+  });
+in {
+  users.users."${user}" = {
+    group           = "${group}";
+    useDefaultShell = true;
+  };
+  users.groups."${group}" = {};
+
+  systemd.services."${name}" = {
+    description = "Hackerspace Checkinator web interface";
+    wantedBy    = [ "multi-user.target" ];
+
+    serviceConfig.User = "${user}";
+    serviceConfig.Type = "simple";
+      
+    environment = {
+      CHECKINATOR_WEB_CONFIG=config;
+    };
+
+    serviceConfig.ExecStartPre = [
+      ''!${prepare}/bin/${name}-prepare''
+      "${pkgs.writeShellScript "checkinator-dbsetup"  ''
+        if [ ! -e "/var/checkinator-web/at.db" ]
+        then
+          ${pkgs.sqlite}/bin/sqlite3 /var/checkinator-web/at.db < ${repo}/dbsetup.sql
+        fi
+      ''}"
+    ];
+    serviceConfig.workingDirectory = checkinator;
+    serviceConfig.ExecStart = "${python}/bin/gunicorn -b unix:${socket_dir}/web.sock at.webapp:app";
+    serviceConfig.ExecStopPost = [
+      ''!${pkgs.coreutils}/bin/rm -rf /mnt/secrets/${name}''
+    ];
+
+  };
+
+  services.nginx.virtualHosts."at.hackerspace.pl" = {
+    forceSSL = true;
+    enableACME = true;
+
+    locations."/static/" = {
+      alias = "${repo}/static/";
+    };
+    locations."/" = {
+      proxyPass = "http://unix://${socket_dir}/web.sock";
+      extraConfig = ''
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+	proxy_set_header X-Forwarded-Host $host:$server_port;
+	proxy_set_header X-Forwarded-Server $host;
+	proxy_set_header X-Forwarded-Proto $scheme;
+      '';
+    };
+  };
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/configuration.nix b/hswaw/machines/customs.hackerspace.pl/configuration.nix
new file mode 100644
index 0000000..c00debb
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/configuration.nix
@@ -0,0 +1,578 @@
+{ config, pkgs, ... }:
+
+let
+  # hscloud checkout, hscloud.routing used to set up dynamic routing (OSPFv6 via bird)
+  hscloud = fetchGit {
+    url = "https://gerrit.hackerspace.pl/hscloud.git";
+    name = "hscloud";
+    rev = "e401735fdd241b25dac4cb82d828dcfa6f84b198";
+  };
+
+  hw = builtins.fromJSON (builtins.readFile ./hw.json);
+  fw = import ./fw-7535.nix;
+  vuko-pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFhaCaC/CVYv6hphqmEdKaPrIn+Q946+myvL9SSnzFZk vuko@eagle";
+  q3k-pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG599UildOrAq+LIOQjKqtGMwjgjIxozI1jtQQRKHtCP q3k@mimeomia";
+  networks = {
+    uplink = {
+      description = "Hackerspace Internet Uplink";
+      hw_addr = builtins.elemAt fw.hw_addresses 0;
+      ipv4 = "185.236.240.5";
+      ipv6 = "2a0d:eb00:2137:1::3";
+    };
+    lan = {
+      description = "Hackerspace LAN";
+      hw_addr = builtins.elemAt fw.hw_addresses 1;
+      ipv4 = "10.8.1.2";
+      ipv6 = "2a0d:eb00:4242::1";
+    };
+    managment = {
+      description = "Management network (temporary routing)";
+      hw_addr = builtins.elemAt fw.hw_addresses 2;
+    };
+    lte = {
+      description = "temp LTE uplink";
+      hw_addr = builtins.elemAt fw.hw_addresses 3;
+    };
+    vpn = {
+      description = "Hackerspace members vpn";
+      ipv4 = "10.9.1.1";
+    };
+  };
+  hostname = "customs";
+  openvpn-auth = import ./openvpn-auth { inherit pkgs; };
+  secrets-path = "/etc/nixos/secrets/";
+  update_authorized_keys = pkgs.writeShellScriptBin "update_authorized_keys" ''
+    ${pkgs.python3.withPackages (pp: [ pp.ldap3 ])}/bin/python ${./update_authorized_keys.py} ${hostname} ${secrets-path}/ldap-password.txt
+  '';
+
+in {
+  imports =
+    [
+      ./ulogd2/service.nix
+      #./hardware-configuration.nix
+      <nixpkgs/nixos/modules/profiles/minimal.nix>
+      <nixpkgs/nixos/modules/profiles/all-hardware.nix>
+      "${hscloud}/bgpwtf/machines/modules/routing.nix"
+      ./checkinator-tracker.nix
+      ./checkinator-web.nix
+      ./mikrotik-exporter.nix
+      ./netboot.nix
+      ./laserproxy/service.nix
+    ];
+
+
+  boot.loader.grub.enable = true;
+  boot.loader.grub.version = 2;
+  boot.loader.grub.device = "nodev";
+  boot.loader.grub.extraConfig = ''
+    serial --unit=0 --speed=115200
+    terminal_input serial
+    terminal_output serial
+  '';
+  boot.kernelParams = ["console=tty0" "console=ttyS0,115200"];
+
+  time.timeZone = "Europe/Warsaw";
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-partuuid/${hw.rootUUID}";
+    fsType = "ext4";
+  };
+
+  services.postfix = let acme_dir = "/var/lib/acme"; in {
+    enable = true;
+    domain = "customs.hackerspace.pl";
+    hostname = "customs.hackerspace.pl";
+    destination = [ "localhost" ];
+    sslCert = "${acme_dir}/customs.hackerspace.pl/full.pem";
+    sslKey = "${acme_dir}/customs.hackerspace.pl/key.pem";
+    enableSmtp = true;
+    enableSubmission = false;
+    #relayHost = "hackerspace.pl";
+    extraConfig = ''
+      inet_interfaces = loopback-only
+    '';
+  };
+
+  fileSystems."/mnt/secrets" = {
+    fsType = "tmpfs";
+    options = [ "rw" "mode=755" "size=200M" "nosuid" "nodev" "relatime" "noexec" ];
+  };
+
+  networking.hostName = hostname;
+  networking.domain = "hackerspace.pl";
+
+  networking.useDHCP = false;
+  networking.vlans = {
+    laser = {
+      id = 4001;
+      interface = "lan";
+    };
+    bms = {
+      id = 4002;
+      interface = "lan";
+    };
+  };
+
+  systemd.services.secrets = {
+    enable = true;
+    description = "Copy secrets and fix permissions";
+    script = ''
+      ${pkgs.coreutils}/bin/install --owner=root --mode=700 --directory /mnt/secrets/nginx/
+      ${pkgs.coreutils}/bin/install --owner=root --mode=400 -t /mnt/secrets/nginx/ \
+        ${secrets-path}/nginx/at.hackerspace.pl.key \
+        ${secrets-path}/nginx/at.hackerspace.pl.crt
+      ${pkgs.acl}/bin/setfacl -m "u:nginx:rx" /mnt/secrets/nginx
+      ${pkgs.acl}/bin/setfacl -m "u:nginx:r" /mnt/secrets/nginx/*
+    '';
+    wantedBy = [ "nginx.service" ];
+    partOf = [ "nginx.service" ];
+    serviceConfig.Type = "oneshot";
+    serviceConfig.RemainAfterExit = "true";
+    serviceConfig.User = "root";
+  };
+
+  services.prometheus.exporters.node = {
+    enable = true;
+    listenAddress = "[::1]";
+    port = 9100;
+    enabledCollectors = [ "systemd" ];
+  };
+
+  systemd.network.links = builtins.listToAttrs (map (
+    name: { name = "10-link-${name}"; value = {
+      enable = true;
+      matchConfig = {
+        MACAddress = networks."${name}".hw_addr;
+      };
+      linkConfig = {
+        Name = "${name}";
+      };
+    }; }
+  ) (builtins.filter (name: builtins.hasAttr "hw_addr" networks."${name}") (builtins.attrNames networks)));
+
+  #networking.interfaces.vpn = {
+  #  virtual = true;
+  #  name = "vpn";
+  #  #ipv4.addresses = [ { address = 10.9.1.1; prefixlen = 16; } ];
+  #};
+
+  boot.kernel.sysctl = {
+    "net.ipv4.ip_forward" = true;
+    "net.ipv6.conf.all.forwarding" = true;
+  };
+
+  # using nftables so firewall has to be disabled
+  networking.firewall.enable = false;
+  networking.nftables.enable = true;
+  networking.nftables.ruleset = ''
+    table inet filter {
+      chain input {
+        type filter hook input priority 0;
+    
+        # accept any localhost traffic
+        iifname lo accept
+    
+        # accept traffic originated from us
+        ct state {established, related} accept
+    
+        # ICMP
+        ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-query, nd-router-solicit } accept
+        ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
+    
+        # allow "ping"
+        ip6 nexthdr icmpv6 icmpv6 type echo-request accept
+        ip protocol icmp icmp type echo-request accept
+
+        # allow OSPFv3
+        ip6 nexthdr 89 accept
+
+        tcp dport 22 accept
+        tcp dport 53 accept
+        udp dport 53 accept
+        tcp dport 80 accept
+        tcp dport 443 accept
+        udp dport tftp accept
+
+        iifname managment udp dport tftp accept
+        iifname lan tcp dport 8080 accept
+
+        # openvpn-members
+        udp dport 20001 accept
+        tcp dport 20001 accept
+
+        # laserproxy
+        udp dport 40200 accept
+        udp dport 50200 accept
+    
+        counter drop
+      }
+    
+      # Allow all outgoing connections.
+      chain output {
+        type filter hook output priority 0; policy accept;
+      }
+    
+      chain forward {
+        type filter hook forward priority 0; policy drop;
+        ct state {established, related} jump accepted
+        oifname "loop" jump accepted
+        ip saddr 10.8.0.0/16 iifname "lan" jump accepted
+        ip saddr 10.9.0.0/16 iifname "vpn" jump accepted
+        ip6 saddr 2a0d:eb00:4242::0/64 iifname "lan" jump accepted
+        ip6 saddr 2a0d:eb00:4242:1::0/64 iifname "vpn" jump accepted
+        ip6 saddr 2a0d:eb00:4242:1::1/128 iifname "loop" jump accepted
+      }
+
+      chain accepted {
+        # IMPORTANT
+        # Log all connections to the outside world from LAN interface, as we are
+        # required to do so
+        oifname != "uplink" accept
+        iifname "uplink" accept
+        ip daddr { 10.0.0.0/8, 225.225.225.225/32 } accept
+        ip6 daddr { 2a0d:eb00::/29, fe80::/8 } accept
+        log group 2 accept
+      }
+    }
+
+    table inet net {
+      chain postrouting {
+        type nat hook postrouting priority 100;
+        ip saddr 10.8.0.0/16 oifname uplink snat ${networks.uplink.ipv4}
+        ip saddr 10.9.0.0/16 oifname uplink snat ${networks.uplink.ipv4}
+      }
+      
+      chain prerouting {
+        type nat hook prerouting priority -100;
+
+        # Access to staszkecoin from Internet
+        ip version 4 iifname "uplink" tcp dport 8333 dnat 10.8.1.49
+      }
+    }
+  '';
+
+  systemd.services."loop-netdev" = let n = "loop"; in {
+    description = "Dummy interface: loop";
+    wantedBy = [ "network-setup.service" "sys-subsystem-net-devices-${n}.device" ];
+    partOf = [ "network-setup.service" ];
+    after = [ "network-pre.target" ];
+    before = [ "network-setup.service" ];
+    serviceConfig.Type = "oneshot";
+    serviceConfig.RemainAfterExit = true;
+    path = [ pkgs.iproute ];
+    script = ''
+      # Remove Dead Interfaces
+      ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+      ip link add "${n}" type dummy
+      ip link set "${n}" up
+    '';
+    postStop = ''
+      ip link delete "${n}"
+    '';
+  };
+
+
+  networking.interfaces = {
+    uplink = {
+      ipv4.addresses = [ { address = networks.uplink.ipv4; prefixLength = 31; } ];
+      ipv6.addresses = [
+        { address = networks.uplink.ipv6; prefixLength = 112; }
+      ];
+    };
+    lan = {
+      ipv4.addresses = [ { address = networks.lan.ipv4; prefixLength = 16; } ];
+      ipv6.addresses = [ { address = networks.lan.ipv6; prefixLength = 64; } ];
+    };
+    loop = {
+      ipv6.addresses = [ { address = "2a0d:eb00:4242:1::1"; prefixLength = 128; } ];
+    };
+    laser = {
+      ipv4.addresses = [ { address = "10.11.0.1"; prefixLength = 24; } ];
+    };
+    bms = {
+      ipv4.addresses = [ { address = "10.11.1.1"; prefixLength = 24; } ];
+    };
+    managment = {
+      ipv4.addresses = [ { address = "10.10.1.1"; prefixLength = 24; } ];
+    };
+    lte = {
+      ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } ];
+    };
+  };
+
+  networking.defaultGateway = {
+    address = "185.236.240.4";
+    interface = "uplink";
+  };
+  networking.defaultGateway6 = {
+    address = "2a0d:eb00:2137:1::1";
+    interface = "uplink";
+  };
+
+
+  networking.nameservers = [ "1.0.0.1" "8.8.8.8" ];
+
+  services.openssh = {
+    enable = true;
+    passwordAuthentication = false;
+    logLevel = "INFO";
+  };
+
+  users.users.root.openssh.authorizedKeys.keys = [ vuko-pubkey q3k-pubkey ];
+
+  services.dhcpd4 = {
+    enable = true;
+    configFile = ./dhcpd.conf;
+    interfaces = ["lan"];
+  };
+
+  hscloud.routing = {
+    enable = true;
+    # TODO(q3k): make this optional in upstream
+    extra = "";
+    routerID = "185.236.240.5";
+    tables.master.program = true;
+    pipe.v6.aggregate_to_kernel = {
+      table = "master";
+      peerTable = "aggregate";
+      filterIn = ''
+        if source = RTS_OSPF then accept;
+        if source = RTS_OSPF_EXT2 then accept;
+        reject;
+      '';
+    };
+    ospf.v6.upstream = {
+      table = "aggregate";
+      area."0.0.0.0" = {
+        interfaces.uplink = { type = "bcast"; };
+        interfaces.lan = { type = "bcast"; stub = true; };
+        interfaces.loop = { type = "ptp"; stub = true; };
+      };
+    };
+  };
+
+  services.radvd = {
+    enable = true;
+    config = ''
+      interface lan {
+        AdvSendAdvert on;
+        prefix 2a0d:eb00:4242::/64 {
+        };
+        route 0::/0 { };
+      };
+      interface vpn {
+        AdvSendAdvert on;
+        prefix 2a0d:eb00:4242:1::/64 {
+          AdvRouterAddr on;
+        };
+        route 0::/0 { };
+      };
+    '';
+  };
+
+  services.logrotate = {
+    enable = true;
+    paths = {
+      ulogd = {
+        enable = true;
+        frequency = "weekly";
+        path = "/var/log/ulogd.pcap";
+        extraConfig = ''
+          postrotate
+            ${pkgs.killall}/bin/killall -HUP ulogd
+          endscript
+        '';
+        keep = 55;
+      };
+    };
+  };
+
+  services.cron = let
+    log-neigh = pkgs.writeShellScript "log-neigh"  ''
+      mkdir -p /var/log/arptables
+      chmod 700 /var/log/arptables
+
+      # Larger than 10MB? rotate.
+      if [[ $(find /var/log/arptables/arptables.log -type f -size +10485760c 2>/dev/null) ]]; then
+          f=/var/log/arptables/$(date "+%s").log
+          cp /var/log/arptables/arptables.log $f
+          gzip -9 $f
+          rm /var/log/arptables/arptables.log
+      fi
+
+      ip neigh >> /var/log/arptables/arptables.log
+      date --iso-8601=seconds >> /var/log/arptables/arptables.log
+    '';
+  in {
+    mailto = "vuko@hackerspace.pl";
+    enable = true;
+    systemCronJobs = [
+      "*/5 * * * *      root    ${log-neigh}"
+      "0   3 * * *      root    ${update_authorized_keys}/bin/update_authorized_keys"
+    ];
+  };
+
+  services.knot = {
+    enable = true;
+    extraConfig = ''
+      server:
+          listen: ${networks.uplink.ipv4}@53
+          listen: ${networks.uplink.ipv6}@53
+      
+      zone:
+        - domain: waw.hackerspace.pl
+          storage: ${./zones}
+          file: waw.hackerspace.pl
+        - domain: i
+          storage: ${./zones}
+          file: i
+        - domain: api.ustream.tv
+          storage: ${./zones}
+          file: api.ustream.tv
+        - domain: api.eye.fi
+          storage: ${./zones}
+          file: api.eye.fi
+      log:
+        - target: syslog
+          any: info
+    '';
+  };
+
+  services.nginx.enable = true;
+  services.nginx.mapHashBucketSize = 64;
+  services.nginx.appendHttpConfig = ''
+    server_names_hash_bucket_size 64;
+  '';
+
+  security.acme = {
+    email = "bofh@hackerspace.pl";
+    acceptTerms = true;
+  };
+
+  services.nginx.virtualHosts."customs.hackerspace.pl" = {
+    default = true;
+    enableACME = true;
+
+    locations."/" = {
+      extraConfig = ''
+        return 302 https://isztar.mf.gov.pl;
+      '';
+    };
+    locations."/metrics/luftdaten" = {
+      proxyPass = "http://10.8.0.146";
+    };
+    locations."/metrics/spejsiot" = {
+      proxyPass = "http://spejsiot.waw.hackerspace.pl/metrics";
+    };
+    locations."/metrics/apm" = {
+      proxyPass = "http://10.8.1.40:5000/metrics";
+    };
+    locations."/metrics/vending" = {
+      proxyPass = "http://10.8.1.32:8000/";
+    };
+    locations."/metrics/sztancarka" = {
+      proxyPass = "http://10.8.0.96:8888/";
+    };
+    locations."/metrics/mikrotik" = {
+      proxyPass = "http://127.0.0.1:9436/metrics";
+      extraConfig = ''
+        allow 209.250.231.127;
+        deny all;
+      '';
+    };
+    locations."/metrics/node" = {
+      proxyPass = "http://[::1]:9100/metrics";
+      extraConfig = ''
+        allow 209.250.231.127;
+        deny all;
+      '';
+    };
+    locations."/stats/sztancarka-ppm" = {
+      proxyPass = "http://10.8.0.96:9090/api/v1/query?query=rate%28cut_count_total%5B15m%5D%29+*+60";
+    };
+    locations."/stats/sztancarka-last-24h" = {
+      proxyPass = "http://10.8.0.96:9090/api/v1/query?query=round(increase(cut_count_total[24h]))";
+    };
+  };
+
+  services.unbound = {
+    enable = true;
+    interfaces = [
+      networks.lan.ipv4
+      "127.0.0.1"
+      "::1"
+      # networks.lan.ipv6 TODO
+    ];
+    allowedAccess = [
+      "127.0.0.1/8"
+      "10.0.0.0/8"
+    ];
+    extraConfig = builtins.concatStringsSep "\n" ((map (
+      name: ''
+        stub-zone:
+            name: ${name}
+            stub-addr: ${networks.uplink.ipv4}
+      ''
+    ) [ "waw.hackerspace.pl" "api.eye.fi" "api.ustream.tv" "i"]) ++ [''
+      forward-zone:
+          name: "."
+          forward-addr: 185.236.240.1
+    '']);
+  };
+
+  
+
+  # Public VPN access for Hackerspace members
+  services.openvpn.servers.members.config = ''
+    script-security 3
+    auth-user-pass-verify ${openvpn-auth}/bin/openvpn-auth-member via-env
+    verify-client-cert none
+    username-as-common-name
+    
+    #user _openvpn
+    #group _openvpn
+    multihome
+    
+    port 20001
+    proto udp
+    proto udp6
+    dev vpn
+    dev-type tun
+    ca ${secrets-path}/openvpn-public/ca.crt
+    cert ${secrets-path}/openvpn-public/server.crt
+    key ${secrets-path}/openvpn-public/server.key
+    dh ${secrets-path}/openvpn-public/dh.pem
+    server 10.9.1.0 255.255.255.0
+    
+    push "route 10.8.0.0 255.255.0.0"
+    push "route 10.9.0.0 255.255.0.0"
+    push "route 10.10.0.0 255.255.0.0"
+    push "route 10.11.0.0 255.255.0.0"
+    push "dhcp-option DNS ${networks.lan.ipv4}"
+    push "dhcp-option DOMAIN waw.hackerspace.pl"
+    
+    ifconfig-pool-persist /var/lib/openvpn-public/ipp.txt
+    #client-config-dir /var/lib/openvpn-public/ccd
+    client-to-client
+    keepalive 10 120
+    comp-lzo
+    persist-key
+    persist-tun
+  '';
+
+  environment.systemPackages = with pkgs; [
+    vim tcpdump htop nmon tmux git file procps parted dmidecode ack utillinux nmap mosh ncdu tree lz4 bind
+    rxvt_unicode.terminfo update_authorized_keys
+  ];
+  programs.mtr.enable = true;
+
+  environment.variables = {
+    EDITOR = "vim";
+  };
+
+  system.stateVersion = "20.03";
+
+  boot.vesa = false;
+  boot.loader.grub.splashImage = null;
+}
+
diff --git a/hswaw/machines/customs.hackerspace.pl/dhcpd.conf b/hswaw/machines/customs.hackerspace.pl/dhcpd.conf
new file mode 100644
index 0000000..0ce071e
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/dhcpd.conf
@@ -0,0 +1,175 @@
+option domain-search "waw.hackerspace.pl";
+option domain-name-servers 10.8.1.2;
+default-lease-time 600;
+max-lease-time 600;
+one-lease-per-client true;
+
+option cisco-ip-phone-tftp code 150 = ip-address;
+
+subnet 10.8.0.0 netmask 255.255.0.0 {
+
+    option routers 10.8.1.2;
+    range 10.8.0.20 10.8.0.199;
+    authoritative;
+    allow bootp;
+
+    if substring (option vendor-class-identifier, 15, 5) = "00000" {
+        filename "netboot.xyz.kpxe";
+    } else {
+        filename "netboot.xyz.efi";
+    }
+
+    next-server 10.8.1.2;
+
+    host laser {
+        hardware ethernet 00:0e:35:1d:a1:a4;
+        fixed-address 10.8.1.18;
+    }
+    host oki {
+        hardware ethernet 00:25:36:de:27:56;
+        fixed-address 10.8.1.20;
+    }
+    host brother {
+        hardware ethernet 00:1b:a9:24:96:e2;
+        fixed-address 10.8.1.21;
+    }
+    host zebra {
+        hardware ethernet 00:07:4d:4d:71:e4;
+        fixed-address 10.8.1.22;
+    }
+    host lj2100 {
+        hardware ethernet 00:30:C1:62:61:23;
+        fixed-address 10.8.1.23;
+    }
+    host dht21 {
+        hardware ethernet 5c:cf:7f:06:9a:3e;
+        fixed-address 10.8.1.25;
+    }
+    host ledpanel {
+        hardware ethernet 00:0A:35:00:01:22;
+        fixed-address 10.8.1.26;
+    }
+    host bridgeport {
+        hardware ethernet 90:1b:0e:1d:23:09;
+        fixed-address 10.8.1.29;
+    }
+    host 3printers1cups {
+        hardware ethernet 02:20:f5:20:6a:2d;
+        fixed-address 10.8.1.30;
+    }
+
+    host telelele {
+        hardware ethernet fe:77:d6:83:26:b1;
+        fixed-address 10.8.1.31;
+    }
+
+    # vending
+    host vending {
+        #hardware ethernet b8:27:eb:71:e4:0e;
+	#hardware ethernet b8:27:eb:3d:ba:fe;
+        hardware ethernet b8:27:eb:03:69:01;
+        fixed-address 10.8.1.32;
+    }
+
+    host transcend {
+        hardware ethernet b0:38:29:2e:5d:c9;
+        fixed-address 10.8.1.33;
+    }
+
+    host welcomer {
+        hardware ethernet b8:27:eb:37:9e:6e;
+        fixed-address 10.8.1.34;
+    }
+    
+    host arcade {
+        hardware ethernet 20:e5:17:0c:31:23;
+        fixed-address 10.8.1.35;
+    }
+
+    host inventory {
+        hardware ethernet 90:e6:ba:84:b6:e0;
+        fixed-address 10.8.1.38;
+    }
+
+    host camera {
+        hardware ethernet 52:54:00:1f:63:1b;
+        fixed-address 10.8.1.39;
+    }
+
+    # Cisco IP Phone
+    host SEPA40CC394DB0C {
+        hardware ethernet a4:0c:c3:94:db:0c;
+        next-server 10.8.1.2;
+	# managed by dfgg/drozdziak
+        #option cisco-ip-phone-tftp 10.8.0.190;
+        option cisco-ip-phone-tftp 10.8.1.2;
+        fixed-address 10.8.1.42;
+    }
+
+    # RIPE Atlas Probe
+    host ripeatlas {
+        hardware ethernet c0:25:e9:99:fb:e8;
+        fixed-address 10.8.1.43;
+    }
+    
+    host chromecast {
+        hardware ethernet 6c:ad:f8:52:4c:a7;
+        fixed-address 10.8.1.47;
+    }
+
+    # craptrap VM
+    host winbox {
+        hardware ethernet 52:54:00:D9:DB:42;
+        fixed-address 10.8.1.48;
+    }
+    
+    host staszkecoin {
+        hardware ethernet 02:42:24:75:eb:19;
+        fixed-address 10.8.1.49;
+    }
+
+
+    host blitzloop {
+        hardware ethernet 00:23:14:b0:ec:c8;
+        fixed-address 10.8.1.51;
+    }
+
+    host tronxy {
+        hardware ethernet 00:1f:16:1c:47:df;
+        fixed-address 10.8.1.52;
+    }
+
+}
+
+# Printer subnet (10.10.7.0/24) has ip-helper 10.8.1.2 set on hs-core01.
+# Make DHCP happen.
+subnet 10.10.7.0 netmask 255.255.255.0 {
+    option routers 10.10.7.1;
+    range 10.10.7.100 10.10.7.200;
+    authoritative;
+    allow bootp;
+}
+
+# Listen for relayed requests on the interface from core01
+# (even though we're not serving anything there directly)
+subnet 172.16.1.0 netmask 255.255.255.250 {
+}
+
+subnet 10.10.5.0 netmask 255.255.255.0 {
+    option routers 10.10.5.1;
+    range 10.10.5.100 10.10.5.200;
+    authoritative;
+    allow bootp;
+    filename "elilo.efi";
+    next-server 10.8.1.16;
+#    option pxelinux.configfile "elilo.conf"
+}
+# itanic ilo gnuj
+subnet 10.10.1.0 netmask 255.255.255.0 {
+    option routers 10.10.1.1;
+    range 10.10.1.150 10.10.1.200;
+    authoritative;
+    next-server 10.8.1.16;
+}
+
+
diff --git a/hswaw/machines/customs.hackerspace.pl/fw-7535.nix b/hswaw/machines/customs.hackerspace.pl/fw-7535.nix
new file mode 100644
index 0000000..f628a72
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/fw-7535.nix
@@ -0,0 +1,13 @@
+
+{
+  model = "FW-7535";
+  hw_addresses = [
+    "00:90:0b:25:bd:e0"
+    "00:90:0b:25:bd:e1"
+    "00:90:0b:25:bd:e2"
+    "00:90:0b:25:bd:e3"
+    "00:90:0b:25:bd:e4"
+    "00:90:0b:25:bd:e5"
+  ];
+}
+
diff --git a/hswaw/machines/customs.hackerspace.pl/hardware-configuration.nix b/hswaw/machines/customs.hackerspace.pl/hardware-configuration.nix
new file mode 100644
index 0000000..1ba16f7
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/hardware-configuration.nix
@@ -0,0 +1,17 @@
+# 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, ... }:
+
+{
+  imports =
+    [ <nixpkgs/nixos/modules/installer/scan/not-detected.nix>
+    ];
+
+  boot.initrd.availableKernelModules = [ "uhci_hcd" "ehci_pci" "ata_piix" "ahci" "usb_storage" "sd_mod" ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ ];
+  boot.extraModulePackages = [ ];
+
+  nix.maxJobs = lib.mkDefault 4;
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/hw.json b/hswaw/machines/customs.hackerspace.pl/hw.json
new file mode 100644
index 0000000..dc648c4
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/hw.json
@@ -0,0 +1 @@
+{"rootUUID": "2c7be07a-aebf-bb41-9a21-9a8dda8fe8a9"}
\ No newline at end of file
diff --git a/hswaw/machines/customs.hackerspace.pl/laserproxy/service.nix b/hswaw/machines/customs.hackerspace.pl/laserproxy/service.nix
new file mode 100644
index 0000000..c82ef5b
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/laserproxy/service.nix
@@ -0,0 +1,45 @@
+{ pkgs, workspace, ... }:
+
+let
+  name = "laserproxy";
+  user = name;
+  group = name;
+in {
+  users.users."${user}" = {
+    group           = "${group}";
+    useDefaultShell = true;
+  };
+  users.groups."${group}" = {};
+
+  systemd.services."${name}" = {
+    description = "Logging packet log from nftables";
+    wantedBy    = [ "multi-user.target" ];
+
+    serviceConfig.User = "${user}";
+    serviceConfig.Type = "simple";
+      
+    serviceConfig.ExecStart = "${workspace.hswaw.laserproxy}/bin/laserproxy -logtostderr -hspki_disable -web_address 127.0.0.1:2137";
+  };
+
+  services.nginx.virtualHosts."laser.waw.hackerspace.pl" = {
+    listen = [
+      { addr = "10.8.1.2"; port=80; ssl=false; }
+      #{ addr = "10.8.1.2"; port=433; ssl=true; }
+    ];
+    locations."/" = {
+      proxyPass = "http://127.0.0.1:2137/";
+      extraConfig = ''
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+	proxy_set_header X-Forwarded-Host $host:$server_port;
+	proxy_set_header X-Forwarded-Server $host;
+	proxy_set_header X-Forwarded-Proto $scheme;
+
+        allow 10.0.0.0/8;
+        deny all;
+      '';
+    };
+  };
+  
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/mikrotik-exporter.nix b/hswaw/machines/customs.hackerspace.pl/mikrotik-exporter.nix
new file mode 100644
index 0000000..26399f3
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/mikrotik-exporter.nix
@@ -0,0 +1,32 @@
+{ pkgs, ... }:
+
+let
+  unstable = import (fetchTarball {
+    sha256 = "0ww70kl08rpcsxb9xdx8m48vz41dpss4hh3vvsmswll35l158x0v";
+    url = "https://api.github.com/repos/NixOS/nixpkgs-channels/tarball/84d74ae9c9cbed73274b8e4e00be14688ffc93fe";
+  }) {config = pkgs.config; };
+
+  name = "mikrotik-exporter";
+  user = name;
+  group = name;
+
+  prepare-secrets = pkgs.writeShellScript "${name}-secrets"  ''
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=500 --directory /mnt/secrets/${name}
+    ${pkgs.coreutils}/bin/install --owner=${user} --mode=400 -t /mnt/secrets/${name} \
+      /etc/nixos/secrets/${name}/ap.yml
+  '';
+in {
+  users.users."${user}" = {
+    group           = "${group}";
+    useDefaultShell = true;
+  };
+  users.groups."${group}" = {};
+
+  systemd.services."${name}" = {
+    description = "Mikrotik prometheus exporter";
+    wantedBy    = [ "multi-user.target" ];
+    serviceConfig.Type = "simple";
+    serviceConfig.ExecStartPre = [ "!${prepare-secrets}" ];
+    serviceConfig.ExecStart = "${unstable.prometheus-mikrotik-exporter}/bin/mikrotik-exporter -config-file /mnt/secrets/${name}/ap.yml -port 127.0.0.1:9436";
+  };
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/netboot.nix b/hswaw/machines/customs.hackerspace.pl/netboot.nix
new file mode 100644
index 0000000..f232ce8
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/netboot.nix
@@ -0,0 +1,22 @@
+{ config, pkgs, lib, ... }:
+
+{
+  services.tftpd.enable = true;
+  services.tftpd.path = pkgs.linkFarm "netboot" [
+    {
+      name = "netboot.xyz.efi";
+      path = pkgs.fetchurl {
+        url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.48/netboot.xyz.efi";
+        sha256 = "1ch8ngryyr2abbrzpf0xb888a5d3rmnvfj8v0frfykhgs607666f";
+        #3b2de54224963ee17857a9737b65d49edc423e06ad7e9c9b85d9f69ca923676a";
+      };
+    }
+    {
+      name = "netboot.xyz.kpxe";
+      path = pkgs.fetchurl {
+        url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.48/netboot.xyz.kpxe";
+        sha256 = "0p3qcdiialzbqjmiss6qay7qdz6b8mdsx5lk4hf75rlkwjh3yhax";
+      };
+    }
+  ];
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/openvpn-auth/default.nix b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/default.nix
new file mode 100644
index 0000000..1a8d825
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/default.nix
@@ -0,0 +1,11 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.python3Packages.buildPythonPackage {
+  pname = "openvpn-auth";
+  version = "1.0";
+
+  src = ./.;
+
+  propagatedBuildInputs = with pkgs; [ python3Packages.ldap3 ];
+}
+
diff --git a/hswaw/machines/customs.hackerspace.pl/openvpn-auth/openvpn_auth/__init__.py b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/openvpn_auth/__init__.py
new file mode 100755
index 0000000..927b94f
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/openvpn_auth/__init__.py
@@ -0,0 +1,61 @@
+import ldap3
+import os
+import sys
+import ssl
+from ldap3.utils.conv import escape_filter_chars
+
+class NotActiveMember(Exception):
+    "Person is not an active hackerspace member"
+
+def check_member(uid: str, password: str):
+    escaped_uid = escape_filter_chars(uid)
+    user_dn = f"uid={escaped_uid},ou=People,dc=hackerspace,dc=pl"
+
+    tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1)
+    server = ldap3.Server("ldap.hackerspace.pl", use_ssl=True, tls=tls_configuration)
+    with ldap3.Connection(server, user=user_dn, password=password, raise_exceptions=True) as conn:
+        filterstr = (
+            "(&"
+                f"(uid={escaped_uid})"
+                "(objectClass=hsMember)"
+                "(|"
+                    "(memberOf=cn=starving,ou=Group,dc=hackerspace,dc=pl)"
+                    "(memberOf=cn=fatty,ou=Group,dc=hackerspace,dc=pl)"
+                    "(memberOf=cn=potato,ou=Group,dc=hackerspace,dc=pl)"
+                ")"
+            ")")
+        conn.search('ou=People,dc=hackerspace,dc=pl',
+            filterstr,
+            search_scope = ldap3.LEVEL,
+            attributes = ['uid'])
+        for e in conn.entries:
+            if e['uid'] == uid:
+                break
+        else:
+            NotActiveMember(f'Member {uid} not found in active members groups')
+
+def member_auth():
+    import argparse
+    import getpass
+
+    uid = os.environ.get('username', None)
+    password = os.environ.get('password', None)
+    
+    if uid is None and password is None:
+        print('"username" and "password" not found in environment')
+        parser = argparse.ArgumentParser()
+        parser.add_argument("uid", nargs='?', default=getpass.getuser(), help="user id")
+        args = parser.parse_args()
+
+        uid = args.uid
+        password = getpass.getpass()
+    
+    try:
+        check_member(uid, password)
+        sys.exit(0)
+    except Exception:
+        sys.exit(1)
+
+if __name__ == "__main__":
+    member_auth()
+
diff --git a/hswaw/machines/customs.hackerspace.pl/openvpn-auth/setup.py b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/setup.py
new file mode 100644
index 0000000..ec50fe7
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/openvpn-auth/setup.py
@@ -0,0 +1,17 @@
+from setuptools import setup
+
+setup(
+    name="openvpn_auth",
+    classifiers=[
+        "License :: OSI Approved :: zlib/libpng License",
+        "Programming Language :: Python :: 3.7",
+    ],
+    packages=["openvpn_auth"],
+    install_requires=["ldap3"],
+    python_requires=">=3.7,",
+    entry_points={
+        "console_scripts": [
+            "openvpn-auth-member=openvpn_auth:member_auth",
+        ]
+    },
+)
diff --git a/hswaw/machines/customs.hackerspace.pl/scripts/wipe-install.py b/hswaw/machines/customs.hackerspace.pl/scripts/wipe-install.py
new file mode 100755
index 0000000..05c04b0
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/scripts/wipe-install.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p grub2 rsync utillinux shadow utillinux e2fsprogs
+from subprocess import run
+from pathlib import Path
+from tempfile import TemporaryDirectory
+import argparse
+import json
+import os
+import sys
+import time
+
+root_device = Path('/dev/disk/by-id/ata-Crucial_CT250MX200SSD1_1537108FC44F')
+bios_boot_part_type = '21686148-6449-6E6F-744E-656564454649'
+config_dir = Path(__file__).parent.parent.absolute()
+
+if os.getlogin() != 'root':
+    print("ERROR: must be run as root", file=sys.stderr)
+    sys.exit(1)
+
+if not root_device.exists():
+    print(f"ERROR: {root_device} not found", file=sys.stderr)
+    sys.exit(1)
+
+print(f"WARNING: this script will WIPE all data on {root_device}")
+if input('Write "Yes" to continue:') != 'Yes':
+    sys.exit(1)
+
+with TemporaryDirectory() as tmp_path:
+    tmp = Path(tmp_path)
+    print(f"Created temporary directory {tmp}")
+
+    parts = (
+        'label: gpt\n'
+        f'name=grub start=2MiB size=10MiB type={bios_boot_part_type}\n'
+        'name=root size=100GiB\n'
+    )
+    run(['sfdisk', root_device], input=parts.encode())
+
+    parts_info = json.loads(run(['sfdisk', '--json', root_device], capture_output=True, check=True).stdout.decode())
+    root_part = Path(parts_info["partitiontable"]["partitions"][1]['node']).resolve()
+
+    for i in range(40):
+        if root_part.exists():
+            break
+        time.sleep(0.2)
+    else:
+        print(f"ERROR: create partition not exists: {root_part}", file=sys.stderr)
+        sys.exit(1)
+
+    run(['mkfs.ext4', root_part])
+
+    root = tmp.joinpath('root')
+    root.mkdir()
+
+    try:
+        run(['mount', root_part, root], check=True)
+
+        run(['mkdir', '-p', root.joinpath('etc', 'nixos')], check=True)
+        run(['rsync', '-r', '--progress', f'{config_dir!s}/', root.joinpath('etc', 'nixos')], check=True)
+
+        root_uuid = parts_info["partitiontable"]["partitions"][1]['uuid'].lower()
+        root.joinpath('etc', 'nixos', 'hw.json').write_text(json.dumps({
+            "rootUUID": f'{root_uuid}',
+        }))
+
+        run(['nixos-install', '--no-root-passwd', '--root', root], check=True)
+        run(['grub-install', f'--root-directory={root!s}', f'--boot-directory={root.joinpath("boot")!s}', root_device], check=False)
+        run(['chpasswd', '--root', root], input=b'root:toor')
+
+    finally:
+        run(['umount', root])
diff --git a/hswaw/machines/customs.hackerspace.pl/ulogd2/default.nix b/hswaw/machines/customs.hackerspace.pl/ulogd2/default.nix
new file mode 100644
index 0000000..01157fd
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/ulogd2/default.nix
@@ -0,0 +1,27 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.stdenv.mkDerivation {
+  name = "ulogd2";
+
+  buildInputs = with pkgs; [
+    gnumake libnetfilter_acct libnetfilter_conntrack libnetfilter_log libmnl
+    libnfnetlink automake autoconf autogen libtool pkg-config libpcap
+  ];
+
+  # hack to capture TCP and UDP port numbers (first 8 bytes) but omit rest of payload
+  patches = [ ./onlyports.patch ];
+
+  preConfigure = ''
+    echo running autogen
+    ./autogen.sh
+    autoheader
+    automake --force-missing --add-missing
+    ./configure --help
+  '';
+
+  src = pkgs.fetchgit {
+    url = "https://git.netfilter.org/ulogd2/";
+    rev = "63135e73fd878cb71b1eebf8e877c4d4c34feba7";
+    sha256 = "1ccfb8l7q9k4fy9s0sgab49ma9xphr4x4ap0v52xfrnwx57h87s2";
+  };
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/ulogd2/onlyports.patch b/hswaw/machines/customs.hackerspace.pl/ulogd2/onlyports.patch
new file mode 100644
index 0000000..c89b80c
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/ulogd2/onlyports.patch
@@ -0,0 +1,23 @@
+diff --git a/output/pcap/ulogd_output_PCAP.c b/output/pcap/ulogd_output_PCAP.c
+index e7798f2..51c4ceb 100644
+--- a/output/pcap/ulogd_output_PCAP.c
++++ b/output/pcap/ulogd_output_PCAP.c
+@@ -154,9 +154,15 @@ static int interp_pcap(struct ulogd_pluginstance *upi)
+ 	switch (ikey_get_u8(&res[5])) {
+ 	case 2: /* INET */
+ 		pchdr.len = ikey_get_u16(&res[2]);
++		if (pchdr.caplen > 20 + 8) {
++			pchdr.caplen = 20 + 8;
++		}
+ 		break;
+ 	case 10: /* INET6 -- payload length + header length */
+ 		pchdr.len = ikey_get_u16(&res[6]) + 40;
++		if (pchdr.caplen > 40 + 8) {
++			pchdr.caplen = 40 + 8;
++		}
+ 		break;
+ 	default:
+ 		pchdr.len = pchdr.caplen;
+-- 
+2.25.4
+
diff --git a/hswaw/machines/customs.hackerspace.pl/ulogd2/service.nix b/hswaw/machines/customs.hackerspace.pl/ulogd2/service.nix
new file mode 100644
index 0000000..e50d92b
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/ulogd2/service.nix
@@ -0,0 +1,32 @@
+{ pkgs, ... }:
+
+let
+  ulogd2 = import ./default.nix { pkgs = pkgs; };
+  name = "ulogd2";
+  config = pkgs.writeText "ulogd.conf" ''
+    [global]
+    logfile="/var/log/ulogd.log"
+    
+    stack=log1:NFLOG,base1:BASE,pcap1:PCAP
+    
+    [log1]
+    group=2
+    
+    [pcap1]
+    file="/var/log/ulogd.pcap"
+    sync=1
+  '';
+in {
+
+  systemd.services."${name}" = {
+    description = "Logging packet log from nftables";
+    wantedBy    = [ "multi-user.target" ];
+
+    serviceConfig.User = "root";
+    serviceConfig.Type = "simple";
+      
+    path = [ ulogd2 ];
+
+    serviceConfig.ExecStart = "${ulogd2}/bin/ulogd -c ${config}";
+  };
+}
diff --git a/hswaw/machines/customs.hackerspace.pl/update_authorized_keys.py b/hswaw/machines/customs.hackerspace.pl/update_authorized_keys.py
new file mode 100755
index 0000000..2b336f2
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/update_authorized_keys.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p python3Packages.ldap3
+
+from ldap3 import Server, Connection, LEVEL
+from ldap3.utils.dn import escape_rdn
+import getpass
+import logging
+from pathlib import Path
+import filecmp
+import os
+
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("hostname", help="hostname")
+parser.add_argument("ldap_pass_file", type=Path, help="file containing lap password")
+
+header_warning = """
+################################### WARNING ####################################
+# This file was created automatically from LDAP database and *WILL* be
+# overwritten. If you need to add / remove keys make changes to
+# {}-admin group / members sshPublicKey attributes in LDAP and rerun
+# update_authorized_keys script   
+################################################################################
+""".lstrip()
+
+def get_keys(connection: Connection, group: str):
+    c = connection
+
+    c.search(
+        search_base="ou=People,dc=hackerspace,dc=pl",
+        search_filter=(
+            "(&"
+            "(objectClass=hsMember)"
+            f"(memberOf=cn={escape_rdn(group)},ou=Group,dc=hackerspace,dc=pl)"
+            ")"
+        ),
+        search_scope=LEVEL,
+        attributes=["sshPublicKey"],
+    )
+
+    admin_keys = []
+    for entry in c.response:
+        attributes = entry["attributes"]
+        for key in entry["attributes"]["sshPublicKey"]:
+            yield key.strip()
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    args = parser.parse_args()
+
+    user = f"cn={escape_rdn(args.hostname)},ou=Boxen,dc=hackerspace,dc=pl"
+    password = args.ldap_pass_file.read_text().strip()
+
+    s = Server("ldap.hackerspace.pl", use_ssl=True)
+    with Connection(s, user=user, password=password, raise_exceptions=True) as c:
+        keys = list(get_keys(c, f"{args.hostname}-admin"))
+        if len(keys) < 2:
+            raise Exception("Less then two keys found - aborting")
+
+        ssh_dir = Path("/", "root", ".ssh")
+        ssh_dir.mkdir(mode=700, exist_ok=True)
+        ssh_dir.chmod(0o700)
+        new_file = ssh_dir.joinpath("authorized_keys_new")
+        old_file = ssh_dir.joinpath("authorized_keys")
+        try:
+            new_file.unlink()
+        except FileNotFoundError:
+            pass
+        new_file.write_bytes(
+            header_warning.format(args.hostname).encode() + b"\n".join(keys)
+        )
+        if not old_file.exists():
+            logging.info('Creating new "authorized_keys" file')
+            os.rename(new_file, old_file)
+        elif filecmp.cmp(new_file, old_file, shallow=False):
+            logging.info('Nothing changed - "authorized_keys" file is up to date')
+            new_file.unlink()
+        else:
+            logging.info('Keys changed - overwriting "authorized_keys" file')
+            os.rename(new_file, old_file)
diff --git a/hswaw/machines/customs.hackerspace.pl/zones/api.eye.fi b/hswaw/machines/customs.hackerspace.pl/zones/api.eye.fi
new file mode 100644
index 0000000..e4fae9a
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/zones/api.eye.fi
@@ -0,0 +1,15 @@
+$ORIGIN .
+$TTL 180
+api.eye.fi  IN SOA  customs.waw.hackerspace.pl. bofh.hackerspace.pl. (
+                            1474143278 ; serial
+                            1800 ; refresh (30 minutes)
+                            1800 ; retry (30 minutes)
+                            604800 ; expire (1 week)
+                            10800 ; minimum (3 hours)
+                            )
+                    NS      customs.waw.hackerspace.pl.
+
+$ORIGIN api.eye.fi.
+$TTL 300
+
+@   IN  A   10.10.3.10
diff --git a/hswaw/machines/customs.hackerspace.pl/zones/api.ustream.tv b/hswaw/machines/customs.hackerspace.pl/zones/api.ustream.tv
new file mode 100644
index 0000000..33e88f3
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/zones/api.ustream.tv
@@ -0,0 +1,15 @@
+$ORIGIN .
+$TTL 180
+api.ustream.tv  IN SOA  customs.waw.hackerspace.pl. bofh.hackerspace.pl. (
+                            1474143278 ; serial
+                            1800 ; refresh (30 minutes)
+                            1800 ; retry (30 minutes)
+                            604800 ; expire (1 week)
+                            10800 ; minimum (3 hours)
+                            )
+                    NS      customs.waw.hackerspace.pl.
+
+$ORIGIN api.ustream.tv.
+$TTL 300
+
+@   IN  A   10.8.0.151
diff --git a/hswaw/machines/customs.hackerspace.pl/zones/i b/hswaw/machines/customs.hackerspace.pl/zones/i
new file mode 100644
index 0000000..f77bcd2
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/zones/i
@@ -0,0 +1,15 @@
+$ORIGIN .
+$TTL 180
+i  IN SOA  customs.waw.hackerspace.pl. bofh.hackerspace.pl. (
+                            1474143278 ; serial
+                            1800 ; refresh (30 minutes)
+                            1800 ; retry (30 minutes)
+                            604800 ; expire (1 week)
+                            10800 ; minimum (3 hours)
+                            )
+                    NS      customs.waw.hackerspace.pl.
+
+$ORIGIN i.
+$TTL 300
+
+@   IN  A   10.8.1.38
diff --git a/hswaw/machines/customs.hackerspace.pl/zones/waw.hackerspace.pl b/hswaw/machines/customs.hackerspace.pl/zones/waw.hackerspace.pl
new file mode 100644
index 0000000..430ab75
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/zones/waw.hackerspace.pl
@@ -0,0 +1,110 @@
+$ORIGIN .
+$TTL 180
+waw.hackerspace.pl  IN SOA  customs.hackerspace.pl. bofh.hackerspace.pl. (
+                            1621022198; serial
+                            1800 ; refresh (30 minutes)
+                            1800 ; retry (30 minutes)
+                            604800 ; expire (1 week)
+                            10800 ; minimum (3 hours)
+                            )
+                    NS      customs.hackerspace.pl.
+
+$ORIGIN waw.hackerspace.pl.
+$TTL 300
+
+zbigniew    A       10.8.1.1
+graphite    CNAME   zbigniew
+
+customs     A       10.8.1.2
+customs     AAAA    2a0d:eb00:4242::1
+at          CNAME   customs
+nat         CNAME   customs
+laser       A       10.8.1.2
+
+craptrap    A       10.8.1.15
+klangsmaschine A     10.8.1.16
+music       CNAME   klangsmaschine
+sound       CNAME   klangsmaschine
+mqtt        CNAME   klangsmaschine
+iot         CNAME   klangsmaschine
+spejsiot    CNAME   klangsmaschine
+wojtylexx   CNAME   klangsmaschine
+ac          A       10.8.1.40 ; vuko's access control
+lights      A       10.8.1.40 ; vuko's lights web interface
+parts       A       10.8.1.40 ; vuko's electronics inventory
+printmaster A       10.8.1.17
+octoprint   CNAME   printmaster
+label       CNAME   printmaster
+ploter      A       10.8.1.19
+papiez      A       10.8.1.20
+dank        A       10.8.1.21
+assbox      A       10.8.1.22
+pap         A       10.8.1.23
+dht01       A       10.8.1.25
+led         A       10.8.1.26
+bridgeport  A       10.8.1.29
+cnc         CNAME   bridgeport
+printers    A       10.8.1.30
+telelele    A       10.8.1.31
+vending     A       10.8.1.32
+mate        CNAME   vending
+transcend   A       10.8.1.33
+welcomer    A       10.8.1.34
+inventory   A       10.8.1.38
+i           CNAME   inventory
+camera      A       10.8.1.39
+franciszek  A       10.8.0.205
+
+freebsd     A       10.8.1.41 ; mpts freebsd workshops
+ciscovoip   A       10.8.1.42
+ripeatlas   A       10.8.1.43
+ap01        A       10.8.1.44
+ap02        A       10.8.1.45
+ap03        A       10.8.1.46
+
+winbox      A       10.8.1.48
+staszkecoin A       10.8.1.49
+sokul       A       10.8.1.50
+blitzloop   A       10.8.1.51
+tronxy      A       10.8.1.52
+
+
+dht01api    A       94.240.35.98
+
+ap1         A       10.8.3.11
+ap2         A       10.8.3.12
+ap3         A       10.8.3.13
+ap4         A       10.8.3.14
+
+voldemort   A       10.8.0.204
+printer     CNAME   voldemort
+
+core01      A       10.10.1.10
+bladerunner A       10.10.1.12
+core02      A       10.10.1.13
+core03      A       10.10.1.14
+
+; wieloryb    A       10.10.3.10
+wieloryb    A       185.236.240.9
+ipxe        CNAME   wieloryb
+cluster     CNAME   wieloryb
+eyefi       CNAME   wieloryb
+home        CNAME   wieloryb
+kubernetes  CNAME   wieloryb
+
+; led         A       94.240.35.98
+
+wh01.prod   A       10.10.3.20
+wh02.prod   A       10.10.3.21
+wh03.prod   A       10.10.3.22
+
+ganeti A 10.10.4.10
+node1.ganeti A 10.10.4.11
+node2.ganeti A 10.10.4.12
+node3.ganeti A 10.10.4.13
+node4.ganeti A 10.10.4.14
+node5.ganeti A 10.10.4.15
+
+monitoring.vm             A 10.10.3.11
+
+obsd.viq            A 10.10.4.101
diff --git a/ops/machines.nix b/ops/machines.nix
index d0e6ae3..0a97bcd 100644
--- a/ops/machines.nix
+++ b/ops/machines.nix
@@ -52,6 +52,14 @@
     sha256 = "1ak7jqx94fjhc68xh1lh35kh3w3ndbadprrb762qgvcfb8351x8v";
   }) {};
 
+  # customs.hackerspace.pl migration temporary checkout
+  nixpkgsCustoms = import (pkgs.fetchFromGitHub {
+    owner = "nixos";
+    repo = "nixpkgs";
+    rev = "d12178b1c4a6ef1232c8c677573ba9db204e66ff";
+    sha256 = "0p7df7yzi35kblxr5ks0rxxp9cfh269g88xpj60sdhdjvfnn6cp7";
+  }) {};
+
   # Stopgap measure to import //cluster/nix machine definitions into new
   # //ops/ infrastructure.
   #
@@ -143,4 +151,8 @@
     ../bgpwtf/machines/edge01.waw.bgp.wtf.nix
     ../bgpwtf/machines/edge01.waw.bgp.wtf-hardware.nix
   ];
+
+  "customs.hackerspace.pl" = mkMachine nixpkgsCustoms [
+    ../hswaw/machines/customs.hackerspace.pl/configuration.nix
+  ];
 }