Unverified Commit 4d4b662a authored by phanirithvij's avatar phanirithvij
Browse files

nixos/reaction: test core plugins

parent 94cf274c
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -12,12 +12,6 @@
      stopForFirewall = false;
      # example.jsonnet or example.yml can be copied and modified from ${pkgs.reaction}/share/examples
      settingsFiles = [ "${pkgs.reaction}/share/examples/example.jsonnet" ];
      settings = {
        # In the qemu vm `run0 ls` as root prints nothing, so we can't use it
        # see https://reaction.ppom.me/reference.html#systemd
        plugins.ipset.systemd = false;
        plugins.virtual.systemd = false;
      };
      runAsRoot = false;
    };
    services.openssh.enable = true;
+1 −0
Original line number Diff line number Diff line
@@ -2,4 +2,5 @@
lib.recurseIntoAttrs {
  basic = runTest ./basic.nix;
  firewall = runTest ./firewall.nix;
  plugins = runTest ./plugins.nix;
}
+125 −0
Original line number Diff line number Diff line
{
  lib,
  pkgs,
  ...
}:
{
  name = "reaction-core-plugins";

  nodes.server = args: {
    services.reaction = {
      enable = true;
      stopForFirewall = false;
      runAsRoot = false;
      settings = import ./settings.nix args;
      /*
        # NOTE: When runAsRoot is true, disable run0
        settings = {
          # In the qemu vm `run0 ls` as root prints nothing, so we can't use it
          # see https://reaction.ppom.me/reference.html#systemd
          plugins.ipset.systemd = false;
          plugins.virtual.systemd = false;
        };
      */
    };
    /*
      NOTE:
        - if reaction is run as non-root, the plugins need these capabilities, remove these if runAsRoot is true
        - CAP_DAC_READ_SEARCH is for journalctl for accessing ssh logs
        - useful tools: capable (from package bcc), captree, getpcaps (from libpcap)
    */
    systemd.services.reaction.serviceConfig = {
      CapabilityBoundingSet = [
        "CAP_NET_ADMIN"
        "CAP_NET_RAW"
        "CAP_DAC_READ_SEARCH"
      ];
      AmbientCapabilities = [
        "CAP_NET_ADMIN"
        "CAP_NET_RAW"
        "CAP_DAC_READ_SEARCH"
      ];
    };
    services.openssh.enable = true;

    users.users.nixos.isNormalUser = true; # neeeded to establish a ssh connection, by default root login is succeeding without any password
  };

  nodes.client = _: {
    environment.systemPackages = [
      pkgs.sshpass
      pkgs.libressl.nc
    ];
  };

  testScript =
    { nodes, ... }: # py
    ''
      start_all()

      # Wait for everything to be ready.
      server.wait_for_unit("multi-user.target")
      server.wait_for_unit("reaction")
      server.wait_for_unit("sshd")

      client_addr = "${(lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address}"
      server_addr = "${(lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address}"

      # Verify there is not ban and the port is reachable from the client.
      server.succeed(f"reaction show | grep -q {client_addr} || test $? -eq 1")
      client.succeed(f"nc -w3 -z {server_addr} 22")

      # Cause authentication failure log entries.
      for _ in range(2):
        client.fail(f"""
          sshpass -p 'wrongpassword' \
            ssh -o StrictHostKeyChecking=no \
              -o User=nixos \
              -o ServerAliveInterval=1 \
              -o ServerAliveCountMax=2 \
              {server_addr}
        """)

      # Verify there is a ban and the port is unreachable from the client.
      server.sleep(2)
      output = server.succeed("reaction show")
      print(output)
      assert client_addr in output, f"client did not get banned, {client_addr}"

      client.fail(f"nc -w3 -z {server_addr} 22")

      # Check that unbanning works
      output = server.succeed("reaction flush")
      print(output)

      client.succeed(f"nc -w3 -z {server_addr} 22")
    '';

  # Debug interactively with:
  # - nix run .#nixosTests.reaction.driverInteractive -L
  # - run_tests()
  # ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.)
  # ssh -o User=root vsock%4
  interactive.sshBackdoor.enable = true;

  interactive.nodes.server =
    { config, ... }:
    {
      # not needed, only for manual interactive debugging
      virtualisation.memorySize = 4096;
      virtualisation.graphics = false;
      environment.systemPackages = with pkgs; [
        btop
        sysz
        sshpass
        libressl.nc
      ];
    };

  meta.maintainers =
    with lib.maintainers;
    [
      ppom
    ]
    ++ lib.teams.ngi.members;
}
+74 −0
Original line number Diff line number Diff line
{ config, ... }:
let
  banFor = name: duration: {
    ban = {
      type = "ipset";
      options = {
        set = "reaction-${name}";
        action = "add";
      };
    };
    unban = {
      after = duration;
      type = "ipset";
      options = {
        set = "reaction-${name}";
        action = "del";
      };
    };
  };

  journalctl = "${config.systemd.package}/bin/journalctl";
in
{
  patterns = {
    ip = {
      type = "ip";
      ipv6mask = 64;
      ignore = [
        "127.0.0.1"
        "::1"
      ];
      ignorecidr = [
        "10.1.1.0/24"
        "2a01:e0a:b3a:1dd0::/64"
      ];
    };
  };

  streams = {
    ssh = {
      cmd = [
        journalctl
        "-fn0"
        "-o"
        "cat"
        "-u"
        "sshd.service"
      ];
      filters = {
        failedlogin = {
          regex = [
            "authentication failure;.*rhost=<ip>(?: |$)"
            "Failed password for .* from <ip> port"
            "Invalid user .* from <ip> "
            "Connection (?:reset|closed) by invalid user .* <ip> port"
          ];
          retry = 2;
          retryperiod = "6h";
          actions = banFor "ssh" "48h";
        };
        connectionreset = {
          regex = [
            "Connection (?:reset|closed) by(?: authenticating user .*)? <ip> port"
            "Received disconnect from <ip> port .*[preauth]"
            "Timeout before authentication for connection from <ip> to"
          ];
          retry = 2;
          retryperiod = "6h";
          actions = banFor "sshreset" "48h";
        };
      };
    };
  };
}