summaryrefslogtreecommitdiffstats
path: root/nixos/tests/shadow/system.nix
blob: ddba233e3c5a61f6a6a9b89b5a16a8ff8a146a59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
{ pkgs, ... }:
let
  # Create a Python environment for the controller with all necessary test framework dependencies
  controllerPython = pkgs.python3.withPackages (ps: [
    ps.flaky
    ps.jc
    ps.passlib
    ps.pytest
    ps.pytest-mh
    ps.pytest-ticket
  ]);

  shadowHostName = "shadowhost";
in
{
  name = "shadow-system-tests";

  meta.maintainers = with pkgs.lib.maintainers; [ joaosreis ];

  nodes = {
    # The target host: runs sshd, has shadow and other test dependencies installed, mutable users, and some specific settings to match the expectations of the test suite
    shadowhost =
      { pkgs, ... }:
      {
        networking.hostName = shadowHostName;
        services.openssh = {
          enable = true;
          settings = {
            PermitRootLogin = "yes";
            PasswordAuthentication = false;
          };
        };

        users.mutableUsers = true;

        environment.systemPackages = with pkgs; [
          shadow
          expect
          vim
        ];

        users.defaultUserShell = "/bin/sh";

        security.loginDefs.settings = {
          PASS_MAX_DAYS = 99999;
          PASS_MIN_DAYS = 0;
          PASS_WARN_AGE = 7;
          USERGROUPS_ENAB = "yes";
          CREATE_HOME = "yes";
          UID_MIN = 1001;
          GID_MIN = 1001;
        };

        security.pam.services = {
          newusers.text = ''
            auth    required    pam_permit.so
            account required    pam_permit.so
            password required   pam_permit.so
            session required    pam_permit.so
          '';
        };

        services.envfs.enable = true;
      };

    # The controller: runs pytest-mh against the shadow host
    controller =
      { pkgs, ... }:
      {
        environment.systemPackages = [
          controllerPython
          pkgs.openssh
        ];
      };

  };

  testScript = ''
    import textwrap

    start_all()

    # ------------------------------------------------------------------
    # 1. Generate an SSH keypair on the controller and authorise it
    #    on the shadow host
    # ------------------------------------------------------------------
    controller.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
    controller.succeed(
        "ssh-keygen -t ed25519 -N \'\' -f /root/.ssh/id_ed25519 2>&1"
    )
    pub_key = controller.succeed("cat /root/.ssh/id_ed25519.pub").strip()

    # Inject the generated public key into the shadow host at runtime
    shadowhost.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
    shadowhost.succeed(
        f"echo '{pub_key}' >> /root/.ssh/authorized_keys && "
        "chmod 600 /root/.ssh/authorized_keys"
    )

    # ------------------------------------------------------------------
    # 2. Make sure the shadow host has a writable /etc/login.defs,
    #    since the test framework expects to be able to write to it.
    # ------------------------------------------------------------------
    shadowhost.succeed(
        "cp --remove-destination $(readlink -f /etc/login.defs) /etc/login.defs && "
        "chmod 644 /etc/login.defs"
    )

    # ------------------------------------------------------------------
    # 3. Copy the upstream test suite onto the controller
    # ------------------------------------------------------------------
    controller.succeed(
        "cp -r ${pkgs.shadow.passthru.testFramework} /root/shadow-tests && "
        "chmod -R u+w /root/shadow-tests"
    )

    # ------------------------------------------------------------------
    # 4. Write the mhc.yaml topology config
    #    This tells pytest-mh where the shadow host is and which role
    #    it plays. The hostname must match the NixOS node name.
    # ------------------------------------------------------------------
    controller.succeed(textwrap.dedent("""
        cat > /root/shadow-tests/mhc.yaml << 'EOF'
        domains:
          - id: shadow
            hosts:
              - hostname: ${shadowHostName}
                role: shadow
                ssh:
                  user: root
                  private_key: /root/.ssh/id_ed25519
        EOF
    """))


    # ------------------------------------------------------------------
    # 5. Run the upstream pytest-mh test suite from the controller
    # ------------------------------------------------------------------
    shadowhost.wait_for_unit("sshd.service")
    # gpasswd tests are disabled, since they rely on specific behavior of the gpasswd command that is not applicable to NixOS
    controller.succeed(
        "cd /root/shadow-tests && "
        "${controllerPython}/bin/pytest "
        "--mh-config=mhc.yaml "
        "--deselect=tests/test_gpasswd.py "
        "-v tests/"
    )
  '';
}