at 21.11-pre 12 kB view raw
1{ config, pkgs, lib, ... }: 2with lib; 3let 4 cfg = config.services.github-runner; 5 svcName = "github-runner"; 6 systemdDir = "${svcName}/${cfg.name}"; 7 # %t: Runtime directory root (usually /run); see systemd.unit(5) 8 runtimeDir = "%t/${systemdDir}"; 9 # %S: State directory root (usually /var/lib); see systemd.unit(5) 10 stateDir = "%S/${systemdDir}"; 11 # %L: Log directory root (usually /var/log); see systemd.unit(5) 12 logsDir = "%L/${systemdDir}"; 13in 14{ 15 options.services.github-runner = { 16 enable = mkOption { 17 default = false; 18 example = true; 19 description = '' 20 Whether to enable GitHub Actions runner. 21 22 Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here: 23 <link xlink:href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners" 24 >About self-hosted runners</link>. 25 ''; 26 type = lib.types.bool; 27 }; 28 29 url = mkOption { 30 type = types.str; 31 description = '' 32 Repository to add the runner to. 33 34 Changing this option triggers a new runner registration. 35 ''; 36 example = "https://github.com/nixos/nixpkgs"; 37 }; 38 39 tokenFile = mkOption { 40 type = types.path; 41 description = '' 42 The full path to a file which contains the runner registration token. 43 The file should contain exactly one line with the token without any newline. 44 The token can be used to re-register a runner of the same name but is time-limited. 45 46 Changing this option or the file's content triggers a new runner registration. 47 ''; 48 example = "/run/secrets/github-runner/nixos.token"; 49 }; 50 51 name = mkOption { 52 # Same pattern as for `networking.hostName` 53 type = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$"; 54 description = '' 55 Name of the runner to configure. Defaults to the hostname. 56 57 Changing this option triggers a new runner registration. 58 ''; 59 example = "nixos"; 60 default = config.networking.hostName; 61 }; 62 63 runnerGroup = mkOption { 64 type = types.nullOr types.str; 65 description = '' 66 Name of the runner group to add this runner to (defaults to the default runner group). 67 68 Changing this option triggers a new runner registration. 69 ''; 70 default = null; 71 }; 72 73 extraLabels = mkOption { 74 type = types.listOf types.str; 75 description = '' 76 Extra labels in addition to the default (<literal>["self-hosted", "Linux", "X64"]</literal>). 77 78 Changing this option triggers a new runner registration. 79 ''; 80 example = literalExample ''[ "nixos" ]''; 81 default = [ ]; 82 }; 83 84 replace = mkOption { 85 type = types.bool; 86 description = '' 87 Replace any existing runner with the same name. 88 89 Without this flag, registering a new runner with the same name fails. 90 ''; 91 default = false; 92 }; 93 94 extraPackages = mkOption { 95 type = types.listOf types.package; 96 description = '' 97 Extra packages to add to <literal>PATH</literal> of the service to make them available to workflows. 98 ''; 99 default = [ ]; 100 }; 101 }; 102 103 config = mkIf cfg.enable { 104 warnings = optionals (isStorePath cfg.tokenFile) [ 105 '' 106 `services.github-runner.tokenFile` points to the Nix store and, therefore, is world-readable. 107 Consider using a path outside of the Nix store to keep the token private. 108 '' 109 ]; 110 111 systemd.services.${svcName} = { 112 description = "GitHub Actions runner"; 113 114 wantedBy = [ "multi-user.target" ]; 115 wants = [ "network-online.target" ]; 116 after = [ "network.target" "network-online.target" ]; 117 118 environment = { 119 HOME = runtimeDir; 120 RUNNER_ROOT = runtimeDir; 121 }; 122 123 path = (with pkgs; [ 124 bash 125 coreutils 126 git 127 gnutar 128 gzip 129 ]) ++ [ 130 config.nix.package 131 ] ++ cfg.extraPackages; 132 133 serviceConfig = rec { 134 ExecStart = "${pkgs.github-runner}/bin/runsvc.sh"; 135 136 # Does the following, sequentially: 137 # - Copy the current and the previous `tokenFile` to the $RUNTIME_DIRECTORY 138 # and make it accessible to the service user to allow for a content 139 # comparison. 140 # - If the module configuration or the token has changed, clear the state directory. 141 # - Configure the runner. 142 # - Copy the configured `tokenFile` to the $STATE_DIRECTORY and make it 143 # inaccessible to the service user. 144 # - Set up the directory structure by creating the necessary symlinks. 145 ExecStartPre = 146 let 147 # Wrapper script which expects the full path of the state, runtime and logs 148 # directory as arguments. Overrides the respective systemd variables to provide 149 # unambiguous directory names. This becomes relevant, for example, if the 150 # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory= 151 # to contain more than one directory. This causes systemd to set the respective 152 # environment variables with the path of all of the given directories, separated 153 # by a colon. 154 writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" '' 155 set -euo pipefail 156 157 STATE_DIRECTORY="$1" 158 RUNTIME_DIRECTORY="$2" 159 LOGS_DIRECTORY="$3" 160 161 ${lines} 162 ''; 163 currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json"; 164 runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg; 165 newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig); 166 currentConfigTokenFilename = ".current-token"; 167 newConfigTokenFilename = ".new-token"; 168 runnerCredFiles = [ 169 ".credentials" 170 ".credentials_rsaparams" 171 ".runner" 172 ]; 173 ownConfigTokens = writeScript "own-config-tokens" '' 174 # Copy current and new token file to runtime dir and make it accessible to the service user 175 cp ${escapeShellArg cfg.tokenFile} "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" 176 chmod 600 "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" 177 chown "$USER" "$RUNTIME_DIRECTORY/${newConfigTokenFilename}" 178 179 if [[ -e "$STATE_DIRECTORY/${currentConfigTokenFilename}" ]]; then 180 cp "$STATE_DIRECTORY/${currentConfigTokenFilename}" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" 181 chmod 600 "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" 182 chown "$USER" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}" 183 fi 184 ''; 185 disownConfigTokens = writeScript "disown-config-tokens" '' 186 # Make the token inaccessible to the runner service user 187 chmod 600 "$STATE_DIRECTORY/${currentConfigTokenFilename}" 188 chown root:root "$STATE_DIRECTORY/${currentConfigTokenFilename}" 189 ''; 190 unconfigureRunner = writeScript "unconfigure" '' 191 differs= 192 # Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist 193 ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1 194 # Also trigger a registration if the token content changed 195 ${pkgs.diffutils}/bin/diff -q \ 196 "$RUNTIME_DIRECTORY"/{${currentConfigTokenFilename},${newConfigTokenFilename}} \ 197 >/dev/null 2>&1 || differs=1 198 199 if [[ -n "$differs" ]]; then 200 echo "Config has changed, removing old runner state." 201 echo "The old runner will still appear in the GitHub Actions UI." \ 202 "You have to remove it manually." 203 find "$STATE_DIRECTORY/" -mindepth 1 -delete 204 fi 205 ''; 206 configureRunner = writeScript "configure" '' 207 empty=$(ls -A "$STATE_DIRECTORY") 208 if [[ -z "$empty" ]]; then 209 echo "Configuring GitHub Actions Runner" 210 token=$(< "$RUNTIME_DIRECTORY"/${newConfigTokenFilename}) 211 RUNNER_ROOT="$STATE_DIRECTORY" ${pkgs.github-runner}/bin/config.sh \ 212 --unattended \ 213 --work "$RUNTIME_DIRECTORY" \ 214 --url ${escapeShellArg cfg.url} \ 215 --token "$token" \ 216 --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} \ 217 --name ${escapeShellArg cfg.name} \ 218 ${optionalString cfg.replace "--replace"} \ 219 ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"} 220 221 # Move the automatically created _diag dir to the logs dir 222 mkdir -p "$STATE_DIRECTORY/_diag" 223 cp -r "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/" 224 rm -rf "$STATE_DIRECTORY/_diag/" 225 226 # Cleanup token from config 227 rm -f "$RUNTIME_DIRECTORY"/${currentConfigTokenFilename} 228 mv "$RUNTIME_DIRECTORY"/${newConfigTokenFilename} "$STATE_DIRECTORY/${currentConfigTokenFilename}" 229 230 # Symlink to new config 231 ln -s '${newConfigPath}' "${currentConfigPath}" 232 fi 233 ''; 234 setupRuntimeDir = writeScript "setup-runtime-dirs" '' 235 # Link _diag dir 236 ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag" 237 238 # Link the runner credentials to the runtime dir 239 ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/" 240 ''; 241 in 242 map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [ 243 "+${ownConfigTokens}" # runs as root 244 unconfigureRunner 245 configureRunner 246 "+${disownConfigTokens}" # runs as root 247 setupRuntimeDir 248 ]; 249 250 # Contains _diag 251 LogsDirectory = [ systemdDir ]; 252 # Default RUNNER_ROOT which contains ephemeral Runner data 253 RuntimeDirectory = [ systemdDir ]; 254 # Home of persistent runner data, e.g., credentials 255 StateDirectory = [ systemdDir ]; 256 StateDirectoryMode = "0700"; 257 WorkingDirectory = runtimeDir; 258 259 # By default, use a dynamically allocated user 260 DynamicUser = true; 261 262 KillMode = "process"; 263 KillSignal = "SIGTERM"; 264 265 # Hardening (may overlap with DynamicUser=) 266 # The following options are only for optimizing: 267 # systemd-analyze security github-runner 268 AmbientCapabilities = ""; 269 CapabilityBoundingSet = ""; 270 # ProtectClock= adds DeviceAllow=char-rtc r 271 DeviceAllow = ""; 272 LockPersonality = true; 273 NoNewPrivileges = true; 274 PrivateDevices = true; 275 PrivateMounts = true; 276 PrivateTmp = true; 277 PrivateUsers = true; 278 ProtectClock = true; 279 ProtectControlGroups = true; 280 ProtectHome = true; 281 ProtectHostname = true; 282 ProtectKernelLogs = true; 283 ProtectKernelModules = true; 284 ProtectKernelTunables = true; 285 ProtectSystem = "strict"; 286 RemoveIPC = true; 287 RestrictNamespaces = true; 288 RestrictRealtime = true; 289 RestrictSUIDSGID = true; 290 UMask = "0066"; 291 292 # Needs network access 293 PrivateNetwork = false; 294 # Cannot be true due to Node 295 MemoryDenyWriteExecute = false; 296 }; 297 }; 298 }; 299}