at master 12 kB view raw
1import ../make-test-python.nix { 2 name = "systemd-confinement"; 3 4 nodes.machine = 5 { pkgs, lib, ... }: 6 let 7 testLib = pkgs.python3Packages.buildPythonPackage { 8 name = "confinement-testlib"; 9 format = "setuptools"; 10 unpackPhase = '' 11 cat > setup.py <<EOF 12 from setuptools import setup 13 setup(name='confinement-testlib', py_modules=["checkperms"]) 14 EOF 15 cp ${./checkperms.py} checkperms.py 16 ''; 17 }; 18 19 mkTest = 20 name: testScript: 21 pkgs.writers.writePython3 "${name}.py" 22 { 23 libraries = [ 24 pkgs.python3Packages.pytest 25 testLib 26 ]; 27 } 28 '' 29 # This runs our test script by using pytest's assertion rewriting, so 30 # that whenever we use "assert <something>", the actual values are 31 # printed rather than getting a generic AssertionError or the need to 32 # pass an explicit assertion error message. 33 import ast 34 from pathlib import Path 35 from _pytest.assertion.rewrite import rewrite_asserts 36 37 script = Path('${pkgs.writeText "${name}-main.py" '' 38 import errno, os, pytest, signal 39 from subprocess import run 40 from checkperms import Accessibility, assert_permissions 41 42 ${testScript} 43 ''}') # noqa 44 filename = str(script) 45 source = script.read_bytes() 46 47 tree = ast.parse(source, filename=filename) 48 rewrite_asserts(tree, source, filename) 49 exec(compile(tree, filename, 'exec', dont_inherit=True)) 50 ''; 51 52 mkTestStep = 53 num: 54 { 55 description, 56 testScript, 57 config ? { }, 58 serviceName ? "test${toString num}", 59 rawUnit ? null, 60 }: 61 { 62 systemd.packages = lib.optional (rawUnit != null) ( 63 pkgs.writeTextFile { 64 name = serviceName; 65 destination = "/etc/systemd/system/${serviceName}.service"; 66 text = rawUnit; 67 } 68 ); 69 70 systemd.services.${serviceName} = { 71 inherit description; 72 requiredBy = [ "multi-user.target" ]; 73 confinement = (config.confinement or { }) // { 74 enable = true; 75 }; 76 serviceConfig = (config.serviceConfig or { }) // { 77 ExecStart = mkTest serviceName testScript; 78 Type = "oneshot"; 79 }; 80 } 81 // removeAttrs config [ 82 "confinement" 83 "serviceConfig" 84 ]; 85 }; 86 87 parametrisedTests = 88 lib.concatMap 89 ( 90 { user, privateTmp }: 91 let 92 withTmp = if privateTmp then "with PrivateTmp" else "without PrivateTmp"; 93 94 serviceConfig = 95 if user == "static-user" then 96 { 97 User = "chroot-testuser"; 98 Group = "chroot-testgroup"; 99 } 100 else if user == "dynamic-user" then 101 { 102 DynamicUser = true; 103 } 104 else 105 { }; 106 107 in 108 [ 109 { 110 description = "${user}, chroot-only confinement ${withTmp}"; 111 config = { 112 confinement.mode = "chroot-only"; 113 # Only set if privateTmp is true to ensure that the default is false. 114 serviceConfig = 115 serviceConfig 116 // lib.optionalAttrs privateTmp { 117 PrivateTmp = true; 118 }; 119 }; 120 testScript = 121 if user == "root" then 122 '' 123 assert os.getuid() == 0 124 assert os.getgid() == 0 125 126 assert_permissions({ 127 'bin': Accessibility.READABLE, 128 'nix': Accessibility.READABLE, 129 'run': Accessibility.READABLE, 130 ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"} 131 ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"} 132 ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"} 133 }) 134 '' 135 else 136 '' 137 assert os.getuid() != 0 138 assert os.getgid() != 0 139 140 assert_permissions({ 141 'bin': Accessibility.READABLE, 142 'nix': Accessibility.READABLE, 143 'run': Accessibility.READABLE, 144 ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"} 145 ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"} 146 ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"} 147 }) 148 ''; 149 } 150 { 151 description = "${user}, full APIVFS confinement ${withTmp}"; 152 config = { 153 # Only set if privateTmp is false to ensure that the default is true. 154 serviceConfig = 155 serviceConfig 156 // lib.optionalAttrs (!privateTmp) { 157 PrivateTmp = false; 158 }; 159 }; 160 testScript = 161 if user == "root" then 162 '' 163 assert os.getuid() == 0 164 assert os.getgid() == 0 165 166 assert_permissions({ 167 'bin': Accessibility.READABLE, 168 'nix': Accessibility.READABLE, 169 ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"} 170 'run': Accessibility.WRITABLE, 171 172 'proc': Accessibility.SPECIAL, 173 'sys': Accessibility.SPECIAL, 174 'dev': Accessibility.WRITABLE, 175 176 ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"} 177 ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"} 178 }) 179 '' 180 else 181 '' 182 assert os.getuid() != 0 183 assert os.getgid() != 0 184 185 assert_permissions({ 186 'bin': Accessibility.READABLE, 187 'nix': Accessibility.READABLE, 188 ${lib.optionalString privateTmp "'tmp': Accessibility.STICKY,"} 189 'run': Accessibility.STICKY, 190 191 'proc': Accessibility.SPECIAL, 192 'sys': Accessibility.SPECIAL, 193 'dev': Accessibility.SPECIAL, 194 'dev/shm': Accessibility.STICKY, 195 'dev/mqueue': Accessibility.STICKY, 196 197 ${lib.optionalString privateTmp "'var': Accessibility.READABLE,"} 198 ${lib.optionalString privateTmp "'var/tmp': Accessibility.STICKY,"} 199 }) 200 ''; 201 } 202 ] 203 ) 204 ( 205 lib.cartesianProduct { 206 user = [ 207 "root" 208 "dynamic-user" 209 "static-user" 210 ]; 211 privateTmp = [ 212 true 213 false 214 ]; 215 } 216 ); 217 218 in 219 { 220 imports = lib.imap1 mkTestStep ( 221 parametrisedTests 222 ++ [ 223 { 224 description = "existence of bind-mounted /etc"; 225 config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; 226 testScript = '' 227 assert Path('/etc/passwd').read_text() 228 ''; 229 } 230 ( 231 let 232 symlink = pkgs.runCommand "symlink" { 233 target = pkgs.writeText "symlink-target" "got me"; 234 } "ln -s \"$target\" \"$out\""; 235 in 236 { 237 description = "check if symlinks are properly bind-mounted"; 238 config.confinement.packages = lib.singleton symlink; 239 testScript = '' 240 assert Path('${symlink}').read_text() == 'got me' 241 ''; 242 } 243 ) 244 { 245 description = "check if StateDirectory works"; 246 config.serviceConfig.User = "chroot-testuser"; 247 config.serviceConfig.Group = "chroot-testgroup"; 248 config.serviceConfig.StateDirectory = "testme"; 249 250 # We restart on purpose here since we want to check whether the state 251 # directory actually persists. 252 config.serviceConfig.Restart = "on-failure"; 253 config.serviceConfig.RestartMode = "direct"; 254 255 testScript = '' 256 assert not Path('/tmp/canary').exists() 257 Path('/tmp/canary').touch() 258 259 if (foo := Path('/var/lib/testme/foo')).exists(): 260 assert Path('/var/lib/testme/foo').read_text() == 'works' 261 else: 262 Path('/var/lib/testme/foo').write_text('works') 263 print('<4>Exiting with failure to check persistence on restart.') 264 raise SystemExit(1) 265 ''; 266 } 267 { 268 description = "check if /bin/sh works"; 269 testScript = '' 270 assert Path('/bin/sh').exists() 271 272 result = run( 273 ['/bin/sh', '-c', 'echo -n bar'], 274 capture_output=True, 275 check=True, 276 ) 277 assert result.stdout == b'bar' 278 ''; 279 } 280 { 281 description = "check if suppressing /bin/sh works"; 282 config.confinement.binSh = null; 283 testScript = '' 284 assert not Path('/bin/sh').exists() 285 with pytest.raises(FileNotFoundError): 286 run(['/bin/sh', '-c', 'echo foo']) 287 ''; 288 } 289 { 290 description = "check if we can set /bin/sh to something different"; 291 config.confinement.binSh = "${pkgs.hello}/bin/hello"; 292 testScript = '' 293 assert Path('/bin/sh').exists() 294 result = run( 295 ['/bin/sh', '-g', 'foo'], 296 capture_output=True, 297 check=True, 298 ) 299 assert result.stdout == b'foo\n' 300 ''; 301 } 302 { 303 description = "check if only Exec* dependencies are included"; 304 config.environment.FOOBAR = pkgs.writeText "foobar" "eek"; 305 testScript = '' 306 with pytest.raises(FileNotFoundError): 307 Path(os.environ['FOOBAR']).read_text() 308 ''; 309 } 310 { 311 description = "check if fullUnit includes all dependencies"; 312 config.environment.FOOBAR = pkgs.writeText "foobar" "eek"; 313 config.confinement.fullUnit = true; 314 testScript = '' 315 assert Path(os.environ['FOOBAR']).read_text() == 'eek' 316 ''; 317 } 318 { 319 description = "check if shipped unit file still works"; 320 config.confinement.mode = "chroot-only"; 321 rawUnit = '' 322 [Service] 323 SystemCallFilter=~kill 324 SystemCallErrorNumber=ELOOP 325 ''; 326 testScript = '' 327 with pytest.raises(OSError) as excinfo: 328 os.kill(os.getpid(), signal.SIGKILL) 329 assert excinfo.value.errno == errno.ELOOP 330 ''; 331 } 332 ] 333 ); 334 335 config.users.groups.chroot-testgroup = { }; 336 config.users.users.chroot-testuser = { 337 isSystemUser = true; 338 description = "Chroot Test User"; 339 group = "chroot-testgroup"; 340 }; 341 }; 342 343 testScript = '' 344 machine.wait_for_unit("multi-user.target") 345 ''; 346}