Configuration Reference¶
Complete listing of configuration sources, YAML keys, environment variables, CLI flags, and the systemd unit's configuration-related facts for incus-gh-runner.
Sources and precedence¶
Controller-mode configuration is assembled from four sources. Precedence, highest first:
- CLI flags
- Environment variables (
INCUS_GH_RUNNER_prefix) - YAML configuration file
- Built-in defaults
Each key is resolved independently, so different keys in the same run may come from different sources.
The default configuration file path is /etc/incus-gh-runner/config.yaml. This path is optional: if no file exists there, startup continues using environment variables, flags, and defaults. An explicit --config path is not optional — if the file does not exist or cannot be read, the process exits with an error before startup completes.
Configuration files are decoded exactly before source precedence is applied. Unknown or duplicate keys, misspellings, wrong YAML scalar or container types, aliases, merge keys, and multiple YAML documents are rejected. Field-level errors identify the field; validation errors do not include field values.
Migration: the controller no longer selects an implicit socket¶
Exactly one of incus.socket and incus.url is required in controller mode.
Configurations that previously omitted incus.socket must add the intended
path explicitly. This is an intentional breaking change: only the standalone
validate subcommand retains an implicit socket default.
YAML configuration keys¶
Duration values use Go duration syntax (for example 30s, 5m).
github¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
github.config_url |
string | — | Required. Absolute HTTPS GitHub or GHES organization or repository URL. GitHub-hosted domains may not specify a port; GHES ports must be valid. Enterprise URLs, trailing DNS dots, userinfo, query strings, fragments, escaped paths, and other path shapes are rejected. |
github.scale_set |
string | — | Required. Non-empty and exactly representable by the GitHub client without query decoding. Also used as the sole runner label. A pre-existing scale set must have that exact label and runner self-update disabled. |
github.runner_group |
string | default |
Controller validation requires a non-empty, exactly representable value, default at repository scope, and an explicit non-default name at organization scope. The controller does not enforce the group's repository or workflow policy; restricting the organization group to selected repositories and commit-pinned workflows is a separate GitHub-side control (see Deploy). |
github.message_poll_timeout |
duration | 0s |
Optional workaround for actions/scaleset#122. 0s preserves the upstream long poll. A nonzero value must be at least 5s; when it expires, the controller treats that poll as an empty response and immediately opens the next poll. |
github.token_file |
string | — | PAT file read once at startup. Mutually exclusive with github.app and INCUS_GH_RUNNER_GITHUB_TOKEN. |
github.app.client_id |
string | — | Required if GitHub App credentials are configured. |
github.app.installation_id |
int64 | — | Required if GitHub App credentials are configured. Must be greater than 0. |
github.app.private_key_file |
string | — | Required if GitHub App credentials are configured. Path to a PEM file, read once at startup. |
Temporary workaround increases message API traffic
At 5s, an idle controller can make up to about 12 message requests per
minute instead of about 1.2 with the upstream 50-second long poll. Enable
this only while the upstream issue remains unresolved, and remove the
override once the client or service wakes active polls reliably.
incus¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
incus.socket |
string | "" |
Required for local mode. Unix socket path. Mutually exclusive with incus.url and all three HTTPS file settings. |
incus.url |
string | "" |
Required for HTTPS mode. Absolute HTTPS Incus API root endpoint. Mutually exclusive with incus.socket; plaintext HTTP and URL user information are rejected. |
incus.client_cert_file |
string | "" |
Required with incus.url; invalid with incus.socket. PEM TLS client certificate path. |
incus.client_key_file |
string | "" |
Required with incus.url; invalid with incus.socket. PEM TLS client private-key path. |
incus.server_cert_file |
string | "" |
Required with incus.url; invalid with incus.socket. PEM certificate whose exact leaf certificate must be presented by the Incus server. |
incus.project |
string | — | Required. Non-empty. Must already exist and be directly accessible to the configured Incus identity. |
incus.image |
string | — | Required. Non-empty. Existing local image alias or fingerprint, resolved to a full fingerprint during preflight. |
incus.profiles |
list of strings | [] |
Optional. No empty entries. Profiles must already exist. Their effective configuration and devices are pinned during preflight and materialized directly onto each VM. When omitted, Incus image/default profile selection is reproduced before pinning. |
incus.owner |
string | — | Required. Non-empty. Exact cleanup selector written to every instance this process manages; not an authorization boundary. |
incus.bootstrap_timeout |
duration | 5m |
Must be greater than 0. |
incus.diagnostics_dir |
string | "" |
Optional. Directory for terminal-runner serial console diagnostics. Persistence is disabled when empty. |
For HTTPS, the client performs normal certificate-chain, hostname, validity,
and key-usage verification and also requires the presented leaf certificate to
equal incus.server_cert_file. The same pin applies to HTTP and WebSocket
connections, and a redirect cannot downgrade the connection to plaintext
HTTP. A wrong, expired, untrusted, or hostname-invalid certificate fails the
connection; there is no insecure or trust-on-first-use fallback.
During startup, the controller reads and parses the client certificate, client
key, and server certificate inside the bounded initial Incus connection. It
first confirms direct access to incus.project, then selects that project. The
resulting SDK client is rebound to the retained process context before image
and profile preflight. Certificate files are not watched or reloaded; restart
the service after rotating any of them. File-read and parse errors identify
the setting without including certificate or private-key contents.
Sensitive console diagnostics
Content written to incus.diagnostics_dir can include workload console output. Restrict access to this directory accordingly.
Each capture is limited to 1 MiB and the directory sink retains at most 256
capture files. The packaged tmpfiles policy expires files older than 30 days
from /var/log/incus-gh-runner/diagnostics; deployments using another
directory must update that policy path.
job_proof¶
Job machine proofs are disabled when this section is absent. The two settings must be configured together; setting only one fails controller startup.
When enabled, the controller attempts one signed proof for every valid GitHub
JobStarted event on its resolved scale set; the
job proofs reference documents the proof format. Events enter
a non-blocking queue whose capacity is max(1, capacity.max_runners);
malformed events or a full queue produce no proof and do not interrupt runner
demand tracking. The guest helper times out when no committed proof arrives.
| Key | Type | Default | Required / validation |
|---|---|---|---|
job_proof.host_id |
string | "" |
Stable host identity recorded in every proof and enrolled beside the public key. Required when job_proof.signing_key_file is set. |
job_proof.signing_key_file |
string | "" |
Regular file no larger than 16 KiB containing exactly one PKCS#8 PEM Ed25519 private key. Read once during startup. Required when job_proof.host_id is set. |
The upstream listener acknowledges a queue message before invoking the job callback. A controller crash in that interval or before proof delivery can leave a running job without a proof; version 1 does not persist or reconstruct events after restart, and the guest helper's timeout is the only signal a job receives that no proof was committed.
capacity¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
capacity.min_runners |
int | 0 |
Must be >= 0. |
capacity.max_runners |
int | 1 |
Must be >= capacity.min_runners. |
concurrency¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
concurrency.incus_operations |
int | 2 |
Must be >= 1. |
timeouts¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
timeouts.incus_operation |
duration | 5m |
Must be greater than 0. |
timeouts.shutdown |
duration | 30s |
Must be greater than 0. |
retry¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
retry.initial |
duration | 1s |
Must be greater than 0. |
retry.maximum |
duration | 30s |
Must be >= retry.initial. |
Top-level¶
| Key | Type | Default | Required / validation |
|---|---|---|---|
reconcile_interval |
duration | 1s |
Must be greater than 0. |
Environment variables¶
Every scalar YAML key binds to an environment variable named
INCUS_GH_RUNNER_ followed by the key path, uppercased, with . and _ both
rendered as _. The list-valued incus.profiles key is YAML-only.
Examples:
| YAML key | Environment variable |
|---|---|
github.config_url |
INCUS_GH_RUNNER_GITHUB_CONFIG_URL |
github.message_poll_timeout |
INCUS_GH_RUNNER_GITHUB_MESSAGE_POLL_TIMEOUT |
incus.socket |
INCUS_GH_RUNNER_INCUS_SOCKET |
incus.url |
INCUS_GH_RUNNER_INCUS_URL |
incus.client_cert_file |
INCUS_GH_RUNNER_INCUS_CLIENT_CERT_FILE |
incus.client_key_file |
INCUS_GH_RUNNER_INCUS_CLIENT_KEY_FILE |
incus.server_cert_file |
INCUS_GH_RUNNER_INCUS_SERVER_CERT_FILE |
incus.project |
INCUS_GH_RUNNER_INCUS_PROJECT |
capacity.min_runners |
INCUS_GH_RUNNER_CAPACITY_MIN_RUNNERS |
timeouts.shutdown |
INCUS_GH_RUNNER_TIMEOUTS_SHUTDOWN |
github.token_file |
INCUS_GH_RUNNER_GITHUB_TOKEN_FILE |
github.app.private_key_file |
INCUS_GH_RUNNER_GITHUB_APP_PRIVATE_KEY_FILE |
job_proof.host_id |
INCUS_GH_RUNNER_JOB_PROOF_HOST_ID |
job_proof.signing_key_file |
INCUS_GH_RUNNER_JOB_PROOF_SIGNING_KEY_FILE |
INCUS_GH_RUNNER_GITHUB_TOKEN¶
Environment-only GitHub personal access token. This variable:
- Is never read from the YAML file (there is no
github.tokenYAML key). - Has no corresponding CLI flag.
- Is trimmed of surrounding whitespace before use.
The packaged systemd deployment supplies the PAT through github.token_file via the PAT credential drop-in. A raw environment value serves local or externally supervised execution where the environment is already the credential boundary.
github.token_file¶
Path to a file containing a GitHub personal access token. The controller reads and trims the file once during startup; a missing, unreadable, or empty file fails startup. The packaged PAT drop-in sets INCUS_GH_RUNNER_GITHUB_TOKEN_FILE to systemd's protected runtime credential copy, so the path should be absent from config.yaml in that deployment.
Incus HTTPS files¶
The packaged HTTPS deployment keeps client_cert_file and
server_cert_file in readable public-certificate files under
/etc/incus-gh-runner. It keeps the client private-key source root-owned and
mode 0600. The credentials-incus-https.conf drop-in loads that source into
systemd's protected runtime credential directory and sets
INCUS_GH_RUNNER_INCUS_CLIENT_KEY_FILE=%d/incus-client-key; omit
client_key_file from config.yaml when using this drop-in.
Obtain the pinned server certificate through an authenticated out-of-band channel and compare its fingerprint with a trusted operator record. Do not bootstrap the pin with an insecure request or blind trust on first use. Ciphers and certificate trust are not relaxed when the exact pin is present. Restart the service after replacing the client certificate, client key, or server pin.
GitHub credential rule¶
Exactly one credential source must be configured:
- GitHub App — all three of
github.app.client_id,github.app.installation_id, andgithub.app.private_key_filemust be set. - Personal access token file —
github.token_fileorINCUS_GH_RUNNER_GITHUB_TOKEN_FILEmust identify the protected token file. - Personal access token value —
INCUS_GH_RUNNER_GITHUB_TOKENmust be set.
Configuring more than one method is an error, including setting both PAT sources. Configuring no credential is also an error.
Incus authority remains security-critical
Local incus-admin socket access is root-equivalent on the compute host.
An unrestricted Incus TLS client certificate also has administrative
authority; HTTPS alone does not restrict it. Use a TLS certificate
restricted to the runner project for the controller, retain the restricted
project and workload-isolation controls, and use a dedicated,
single-purpose compute host. The incus.owner marker limits intended
cleanup but is forgeable by another project writer and is not
authorization.
The packaged systemd deployment supplies either
github.app.private_key_file or github.token_file through one selected
GitHub credential drop-in. HTTPS mode additionally supplies
incus.client_key_file through its independent Incus credential drop-in.
Secret values do not belong in config.yaml. See
systemd unit facts.
CLI¶
Running incus-gh-runner without a subcommand starts the controller. The
controller configuration sources and flags documented above apply to that
mode. The validate and proof verify subcommands are independent of controller
configuration and GitHub credentials.
--config¶
| Flag | Type | Default |
|---|---|---|
--config |
string | "" |
Configuration file path. Empty selects the default path (/etc/incus-gh-runner/config.yaml, optional). A non-empty value must point to an existing, readable file.
Configuration flags¶
Each flag overrides its corresponding YAML key and environment variable.
| Flag | Type | Default | Configuration key |
|---|---|---|---|
--min-runners |
int | 0 |
capacity.min_runners |
--max-runners |
int | 1 |
capacity.max_runners |
--incus-operations |
int | 2 |
concurrency.incus_operations |
--reconcile-interval |
duration | 1s |
reconcile_interval |
--operation-timeout |
duration | 5m |
timeouts.incus_operation |
--shutdown-timeout |
duration | 30s |
timeouts.shutdown |
--version¶
Prints build metadata and exits:
In a release build, <version>, <commit>, and <date> are populated at build time. In a development build, these render as dev, none, and unknown respectively.
validate <baseline>¶
Validates exactly one rendered JSON baseline against the embedded CUE policy,
then compares it with effective state read through one flag-selected Incus
connection. The command performs GET operations only; it never creates,
changes, or deletes Incus resources and does not invoke external cue,
incus, or jq executables.
| Flag | Type | Default |
|---|---|---|
--socket |
string | /var/lib/incus/unix.socket |
--url |
string | "" |
--client-cert |
string | "" |
--client-key |
string | "" |
--server-cert |
string | "" |
With no connection flags, validate uses
/var/lib/incus/unix.socket. An explicit --socket selects another local
socket. An explicit --url clears the implicit socket and requires all three
certificate flags. Supplying both --socket and --url is an error.
These are flag-only validator inputs. The command does not load controller YAML, controller environment bindings, controller flags, or GitHub credentials. The HTTPS certificate behavior is the same strict normal TLS verification plus exact server-leaf pin used by controller mode. A successful validation prints one human-readable line to stdout; compatibility notices are written to stderr.
The validator reads the Incus server configuration, including sensitive
listener values, plus the named runner project, the network and ACL in the
default project, the runner-project profile, and the global storage pool. Its
operator credential must have an administrative view that exposes every one
of those resources and values; hidden sensitive fields fail validation. Keep
that credential separate from the project-restricted controller certificate.
Do not broaden the controller certificate to make validation pass.
The live comparison confirms effective resource ceilings but cannot re-measure or re-prove the physical-host capacity and reserved headroom used to generate them. Re-render and review the baseline after those generation-time inputs change.
proof verify <proof>¶
Authenticates one DSSE job machine proof against an enrolled Ed25519 public key and expected host identity:
incus-gh-runner proof verify \
--public-key machine-provenance-key.pub.pem \
--expected-host-id builder-host-01 \
job-proof.dsse.json > verified-payload.json
| Flag | Type | Default |
|---|---|---|
--public-key |
string | required |
--expected-host-id |
string | required |
The command reads no controller configuration and never loads the signing key. It limits the envelope and decoded payload to 64 KiB, requires the exact version 1 payload type and one signature, rejects unknown payload fields, and prints only verified payload JSON to stdout. Repository, workflow, image, and freshness policy are deliberately left to the caller. Any parse, signature, key, schema, or host mismatch exits non-zero without printing unverified payload bytes. The job proofs reference documents the envelope, the version 1 payload fields, and the key-ID rule the command enforces.
Exit behavior¶
Controller mode exits 0 on clean shutdown. It exits 1 on configuration
load failure, configuration validation failure, or runtime failure, printing
the error to stderr. There is no flag to control controller log level or log
format; controller logs are always structured JSON on stdout. validate exits
0 only when policy and live-state checks pass and exits non-zero with an error
on stderr otherwise.
systemd unit facts¶
The packaged base unit is deploy/systemd/incus-gh-runner.service. It
deliberately selects no GitHub credential method. Install exactly one packaged
GitHub credential drop-in as
/etc/systemd/system/incus-gh-runner.service.d/credentials.conf:
credentials-github-app.confloads/etc/incus-gh-runner/github-app-private-key.pemand setsINCUS_GH_RUNNER_GITHUB_APP_PRIVATE_KEY_FILE.credentials-personal-access-token.confloads/etc/incus-gh-runner/github-tokenand setsINCUS_GH_RUNNER_GITHUB_TOKEN_FILE.
For HTTPS mode, also install credentials-incus-https.conf as
incus-https.conf. It clears the base unit's SupplementaryGroups value,
loads the root-only /etc/incus-gh-runner/client.key, and sets
INCUS_GH_RUNNER_INCUS_CLIENT_KEY_FILE to the protected runtime copy. The
DynamicUser needs read access to the public client and server certificate
files. It does not need incus-admin membership in HTTPS mode.
When job proofs are enabled, install exactly one independent proof-key drop-in
as job-proof.conf:
credentials-job-proof-file.confloads the root-only plaintext source withLoadCredential=.credentials-job-proof-tpm.confdecrypts the TPM-bound source withLoadCredentialEncrypted=.
Both set INCUS_GH_RUNNER_JOB_PROOF_SIGNING_KEY_FILE to the same protected
runtime file (%d/machine-provenance-key), so they compose with either GitHub
credential method and either Incus connection mode.
| Directive | Value |
|---|---|
ExecStart |
/usr/bin/incus-gh-runner --config /etc/incus-gh-runner/config.yaml |
| GitHub credential | Selected by one systemd drop-in; absent from the base unit |
| Incus client key | Selected by credentials-incus-https.conf in HTTPS mode; absent in socket mode |
ConfigurationDirectory |
incus-gh-runner (mode 0755), resolves to /etc/incus-gh-runner |
LogsDirectory |
incus-gh-runner (mode 0700), resolves to /var/log/incus-gh-runner |
DynamicUser |
yes |
SupplementaryGroups |
incus-admin in the base socket-mode unit; cleared by the HTTPS drop-in |
UMask |
0077 |
Restart |
on-failure |
RestartSec |
5s |
StartLimitIntervalSec / StartLimitBurst |
60s / 5 |
KillSignal |
SIGTERM |
TimeoutStopSec |
70s |
TimeoutStopSec must exceed the application's internal shutdown budget of
2 × timeouts.shutdown. At the default timeouts.shutdown of 30s, that
budget is 60s, under the unit's 70s stop timeout. Raising
timeouts.shutdown requires raising TimeoutStopSec to keep
TimeoutStopSec > 2 × timeouts.shutdown. See
How incus-gh-runner works for the shutdown
model these settings drive.
Hardening directives¶
The unit sets the following sandboxing directives: NoNewPrivileges,
ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices,
ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs,
ProtectControlGroups, ProtectClock, ProtectHostname,
ProtectProc=invisible, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, MemoryDenyWriteExecute,
SystemCallArchitectures=native, empty CapabilityBoundingSet, empty
AmbientCapabilities, and
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6.
See also¶
- How incus-gh-runner works — capacity formula, lifecycle states, and the shutdown model these settings drive.
- Deploy — end-to-end production deployment using this configuration surface.
- Operate — day-2 operational use of these settings.
- Guest contract — schemas and metadata keys not covered by this page.
- Job proofs — the proof envelope, payload schema, key-ID rule, and enrollment facts.