vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / continuous-integration / github-runner / service.nix
blob4b1fc230c2d3ae68ffd0af7860934446949c7e9e
1 { config
2 , lib
3 , pkgs
4 , ...
5 }:
7 with lib;
9   config.assertions = flatten (
10     flip mapAttrsToList config.services.github-runners (name: cfg: map (mkIf cfg.enable) [
11       {
12         assertion = !cfg.noDefaultLabels || (cfg.extraLabels != [ ]);
13         message = "`services.github-runners.${name}`: The `extraLabels` option is mandatory if `noDefaultLabels` is set";
14       }
15       {
16         assertion = cfg.group == null || cfg.user != null;
17         message = ''`services.github-runners.${name}`: Setting `group` while leaving `user` unset runs the service as `root`. If this is really what you want, set `user = "root"` explicitly'';
18       }
19     ])
20   );
22   config.systemd.services = flip mapAttrs' config.services.github-runners (name: cfg:
23     let
24       svcName = "github-runner-${name}";
25       systemdDir = "github-runner/${name}";
27       # %t: Runtime directory root (usually /run); see systemd.unit(5)
28       runtimeDir = "%t/${systemdDir}";
29       # %S: State directory root (usually /var/lib); see systemd.unit(5)
30       stateDir = "%S/${systemdDir}";
31       # %L: Log directory root (usually /var/log); see systemd.unit(5)
32       logsDir = "%L/${systemdDir}";
33       # Name of file stored in service state directory
34       currentConfigTokenFilename = ".current-token";
36       workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
37       # Support old github-runner versions which don't have the `nodeRuntimes` arg yet.
38       package = cfg.package.override (old: optionalAttrs (hasAttr "nodeRuntimes" old) { inherit (cfg) nodeRuntimes; });
39     in
40     nameValuePair svcName {
41       description = "GitHub Actions runner";
43       wantedBy = [ "multi-user.target" ];
44       wants = [ "network-online.target" ];
45       after = [ "network.target" "network-online.target" ];
47       environment = {
48         HOME = workDir;
49         RUNNER_ROOT = stateDir;
50       } // cfg.extraEnvironment;
52       path = (with pkgs; [
53         bash
54         coreutils
55         git
56         gnutar
57         gzip
58       ]) ++ [
59         config.nix.package
60       ] ++ cfg.extraPackages;
62       serviceConfig = mkMerge [
63         {
64           ExecStart = "${package}/bin/Runner.Listener run --startuptype service";
66           # Does the following, sequentially:
67           # - If the module configuration or the token has changed, purge the state directory,
68           #   and create the current and the new token file with the contents of the configured
69           #   token. While both files have the same content, only the later is accessible by
70           #   the service user.
71           # - Configure the runner using the new token file. When finished, delete it.
72           # - Set up the directory structure by creating the necessary symlinks.
73           ExecStartPre =
74             let
75               # Wrapper script which expects the full path of the state, working and logs
76               # directory as arguments. Overrides the respective systemd variables to provide
77               # unambiguous directory names. This becomes relevant, for example, if the
78               # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
79               # to contain more than one directory. This causes systemd to set the respective
80               # environment variables with the path of all of the given directories, separated
81               # by a colon.
82               writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
83                 set -euo pipefail
85                 STATE_DIRECTORY="$1"
86                 WORK_DIRECTORY="$2"
87                 LOGS_DIRECTORY="$3"
89                 ${lines}
90               '';
91               runnerRegistrationConfig = getAttrs [
92                 "ephemeral"
93                 "extraLabels"
94                 "name"
95                 "noDefaultLabels"
96                 "runnerGroup"
97                 "tokenFile"
98                 "url"
99                 "workDir"
100               ]
101                 cfg;
102               newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
103               currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
104               newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
105               currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
107               runnerCredFiles = [
108                 ".credentials"
109                 ".credentials_rsaparams"
110                 ".runner"
111               ];
112               unconfigureRunner = writeScript "unconfigure" ''
113                 copy_tokens() {
114                   # Copy the configured token file to the state dir and allow the service user to read the file
115                   install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
116                   # Also copy current file to allow for a diff on the next start
117                   install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
118                 }
119                 clean_state() {
120                   find "$STATE_DIRECTORY/" -mindepth 1 -delete
121                   copy_tokens
122                 }
123                 diff_config() {
124                   changed=0
125                   # Check for module config changes
126                   [[ -f "${currentConfigPath}" ]] \
127                     && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
128                     || changed=1
129                   # Also check the content of the token file
130                   [[ -f "${currentConfigTokenPath}" ]] \
131                     && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
132                     || changed=1
133                   # If the config has changed, remove old state and copy tokens
134                   if [[ "$changed" -eq 1 ]]; then
135                     echo "Config has changed, removing old runner state."
136                     echo "The old runner will still appear in the GitHub Actions UI." \
137                          "You have to remove it manually."
138                     clean_state
139                   fi
140                 }
141                 if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
142                   # In ephemeral mode, we always want to start with a clean state
143                   clean_state
144                 elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
145                   # There are state files from a previous run; diff them to decide if we need a new registration
146                   diff_config
147                 else
148                   # The state directory is entirely empty which indicates a first start
149                   copy_tokens
150                 fi
151                 # Always clean workDir
152                 find -H "$WORK_DIRECTORY" -mindepth 1 -delete
153               '';
154               configureRunner = writeScript "configure" /*bash*/''
155                 if [[ -e "${newConfigTokenPath}" ]]; then
156                   echo "Configuring GitHub Actions Runner"
157                   # shellcheck disable=SC2054  # don't complain about commas in --labels
158                   args=(
159                     --unattended
160                     --disableupdate
161                     --work "$WORK_DIRECTORY"
162                     --url ${escapeShellArg cfg.url}
163                     --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
164                     ${optionalString (cfg.name != null ) "--name ${escapeShellArg cfg.name}"}
165                     ${optionalString cfg.replace "--replace"}
166                     ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
167                     ${optionalString cfg.ephemeral "--ephemeral"}
168                     ${optionalString cfg.noDefaultLabels "--no-default-labels"}
169                   )
170                   # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
171                   # if it is not a PAT, we assume it contains a registration token and use the --token option
172                   token=$(<"${newConfigTokenPath}")
173                   if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
174                     args+=(--pat "$token")
175                   else
176                     args+=(--token "$token")
177                   fi
178                   ${package}/bin/Runner.Listener configure "''${args[@]}"
179                   # Move the automatically created _diag dir to the logs dir
180                   mkdir -p  "$STATE_DIRECTORY/_diag"
181                   cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
182                   rm    -rf "$STATE_DIRECTORY/_diag/"
183                   # Cleanup token from config
184                   rm "${newConfigTokenPath}"
185                   # Symlink to new config
186                   ln -s '${newConfigPath}' "${currentConfigPath}"
187                 fi
188               '';
189               setupWorkDir = writeScript "setup-work-dirs" ''
190                 # Link _diag dir
191                 ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
193                 # Link the runner credentials to the work dir
194                 ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
195               '';
196             in
197             map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [
198               "+${unconfigureRunner}" # runs as root
199               configureRunner
200               setupWorkDir
201             ];
203           # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
204           # to trigger a fresh registration.
205           Restart = if cfg.ephemeral then "on-success" else "no";
206           # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
207           # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
208           RestartForceExitStatus = [ 2 ];
210           # Contains _diag
211           LogsDirectory = [ systemdDir ];
212           # Default RUNNER_ROOT which contains ephemeral Runner data
213           RuntimeDirectory = [ systemdDir ];
214           # Home of persistent runner data, e.g., credentials
215           StateDirectory = [ systemdDir ];
216           StateDirectoryMode = "0700";
217           WorkingDirectory = workDir;
219           InaccessiblePaths = [
220             # Token file path given in the configuration, if visible to the service
221             "-${cfg.tokenFile}"
222             # Token file in the state directory
223             "${stateDir}/${currentConfigTokenFilename}"
224           ];
226           KillSignal = "SIGINT";
228           # Hardening (may overlap with DynamicUser=)
229           # The following options are only for optimizing:
230           # systemd-analyze security github-runner
231           AmbientCapabilities = mkBefore [ "" ];
232           CapabilityBoundingSet = mkBefore [ "" ];
233           # ProtectClock= adds DeviceAllow=char-rtc r
234           DeviceAllow = mkBefore [ "" ];
235           NoNewPrivileges = mkDefault true;
236           PrivateDevices = mkDefault true;
237           PrivateMounts = mkDefault true;
238           PrivateTmp = mkDefault true;
239           PrivateUsers = mkDefault true;
240           ProtectClock = mkDefault true;
241           ProtectControlGroups = mkDefault true;
242           ProtectHome = mkDefault true;
243           ProtectHostname = mkDefault true;
244           ProtectKernelLogs = mkDefault true;
245           ProtectKernelModules = mkDefault true;
246           ProtectKernelTunables = mkDefault true;
247           ProtectSystem = mkDefault "strict";
248           RemoveIPC = mkDefault true;
249           RestrictNamespaces = mkDefault true;
250           RestrictRealtime = mkDefault true;
251           RestrictSUIDSGID = mkDefault true;
252           UMask = mkDefault "0066";
253           ProtectProc = mkDefault "invisible";
254           SystemCallFilter = mkBefore [
255             "~@clock"
256             "~@cpu-emulation"
257             "~@module"
258             "~@mount"
259             "~@obsolete"
260             "~@raw-io"
261             "~@reboot"
262             "~capset"
263             "~setdomainname"
264             "~sethostname"
265           ];
266           RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
268           BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
270           # Needs network access
271           PrivateNetwork = mkDefault false;
272           # Cannot be true due to Node
273           MemoryDenyWriteExecute = mkDefault false;
275           # The more restrictive "pid" option makes `nix` commands in CI emit
276           # "GC Warning: Couldn't read /proc/stat"
277           # You may want to set this to "pid" if not using `nix` commands
278           ProcSubset = mkDefault "all";
279           # Coverage programs for compiled code such as `cargo-tarpaulin` disable
280           # ASLR (address space layout randomization) which requires the
281           # `personality` syscall
282           # You may want to set this to `true` if not using coverage tooling on
283           # compiled code
284           LockPersonality = mkDefault false;
286           DynamicUser = mkDefault true;
287         }
288         (mkIf (cfg.user != null) {
289           DynamicUser = false;
290           User = cfg.user;
291         })
292         (mkIf (cfg.group != null) {
293           DynamicUser = false;
294           Group = cfg.group;
295         })
296         cfg.serviceOverrides
297       ];
298     }
299   );