Kai Ole Hartwig
pinup · documentation

Security

A dependency bot holds a token that can write to every repository it scans. And it reads configuration from all of them. These are the lines pinup holds, and what a repository can and cannot make the bot do.

  • Reports: security reports go to the address in the repository's SECURITY.md, not as an issue

The invariants

Every run produces a plan before any write

What changes is known in bytes before the first byte changes.

Apply consumes byte-range edits, never dependencies

Nothing downstream of the plan can invent a change; YAML and JSON are never re-serialised.

The core decides, plugins apply

A task runs in a scope it may not leave; a path outside discards its whole result.

The stricter of two labels wins

An analyzer loosens an automerge only with trustEffective: true.

Plugins get no credentials

A task sees PATH, LANG, TZ and whatever PINUP_PLUGIN_ENV names; never the platform token, never the signing key.

A plan explains why nothing happens

A held update carries its reason; a refused command is named, not skipped.

The platform token

The token is bound to the instance's host and, on GitLab, to the API paths the bot itself calls: releases, tags, raw files, packages, the group Composer registry, its own user. A URL a repository configuration names on the instance (a customDatasources template, a registryUrls entry) is requested without it. Otherwise any developer in any scanned repository could point the bot's api token at /api/v4/groups/<id>/variables and read the result as a “release list”.

Registry access exists only for the estate's own registry (PINUP_REGISTRY_HOST), exchanged at the instance's token realm over TLS, scoped to the one repository a lookup needs. Every other registry is asked anonymously.

The GitHub token is bound to api.github.com (or the Enterprise host). A redirect away from a platform's host is refused, not followed: net/http drops Authorization across hosts, a PRIVATE-TOKEN header it does not.

No token ever lands in a file or a URL. git asks for credentials through GIT_ASKPASS, which is the pinup binary itself, and it answers only a prompt for the platform's host.

What a repository can decide

A repository's own configuration file can disable itself, change versionings and registries, add rules, request tasks. It cannot:

Commits

Every commit is signed as the runner is configured (openpgp or ssh with PINUP_SIGNING_KEY), by an identity the key vouches for (CheckIdentity rejects a key that does not name the author). The run reads the platform's verdict on its own commits. Unsigned works only with the explicit word none.

A branch somebody else committed to is theirs: the run neither rebuilds it nor closes its request, and it says whose it is.

Withdrawn versions

A package source may withdraw versions: an apk repository through withdrawn.json at its root, container images through the list PINUP_WITHDRAWN_IMAGES names. pinup never offers a withdrawn version. A dependency that sits on one is raised to the replacement like a security fix, with no release age, schedule, dashboard approval or request limit. The withdrawal and its advisory ids stand in the plan, the dashboard and the request. A list that cannot be read is a warning; the run goes on without it.

Reports

Security reports go to the address in the repository's SECURITY.md, not as an issue.

Next

Back to pinup

The overview: the problem, the fit check, the quickstart, every chapter.

pinup →
Back to pinup

Getting started

The binary, a token, the first plan, the first run, a CI job.

Read →
Getting started