Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ configures it: `hub.yml`, `targets.yml`, `.touchmark/operations.yml`, the CI fil
* **Accounts.** A **reader** (read-only) runs `plan` on hub pull requests. A **writer**,
a different account, runs `distribute` and `doctor` from the hub's default branch only.
The **hub channel**, the CI's own token (`GITHUB_TOKEN`, `CI_JOB_TOKEN`), reaches the hub
repository alone. The writer has no access to the hub.
repository alone. The writer has no access to the hub, unless the hub says
`security.writer_on_hub: guard` (GitLab: the writer reaches the hub as a Developer, and
touchmark verifies it cannot get content onto the default branch).
* **Fingerprint.** The hub is known by its host and immutable repository id
(`github.com/712345678`), which CI provides. `id` in `hub.yml` names the sync branch
`touchmark/<id>` and is for people.
Expand Down Expand Up @@ -262,6 +264,7 @@ ten `::error` and ten `::warning` annotations.
| `memory.auto_close_cooldown` | `30d` | how long content a bot closed waits before it is proposed again |
| `security.write_isolation` | `platform` | `platform` (CI keeps the key to the default branch; probed), `external` (an OIDC secret store), `none` (needs `security.reason`) |
| `security.private_targets_in_public_hub` | `skip` | `deliver` puts their names into public CI logs |
| `security.writer_on_hub` | `refuse` | `guard` (GitLab): the writer may reach the hub as a Developer; `doctor` verifies the guard (`hub-guard`), `plan` refuses a hub merge request the writer opened or pushed to |
| `sensitive_paths` | none | patterns added to the built-in list highlighted under ⚠ in pull requests |
| `packs.<pack>.description`, `.requires`, `.formerly` | none | metadata; `requires` adds and orders dependencies; `formerly` keeps a renamed pack's history |

Expand Down Expand Up @@ -324,7 +327,12 @@ from its environment once read:
probe does not test.
5. **The writer has no access to the hub.** Install the Apps on targets only; keep the
hub outside the GitLab groups the accounts belong to. `doctor` fails when the writer
can push to the hub, and warns when it can see a private one.
can push to the hub, and warns when it can see a private one. On GitLab a hub that
must live in the group of its targets sets `security.writer_on_hub: guard`: the writer
is then a Developer of the hub, `doctor`'s `hub-guard` verifies that it can neither
push nor merge to the default branch and that merge request pipelines run the default
branch's CI file and must succeed, and `plan` exits 2 in a hub merge request the
writer opened or pushed to.
6. **Nothing reaches a repository without its consent.** `.engineering-assets.yml` at
the root, possibly empty, or an entry with `opt_in: assumed` when the hub owns the
decision; `enabled: false` in the file overrides the hub. Never ship that file in a
Expand Down
28 changes: 27 additions & 1 deletion docs/concepts/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,36 @@ closed where it is missing.
separate account has its own rate limits.
- **The writer has no access to the hub**, so a leaked write key cannot change the packs.
`doctor` fails when the writer can push to the hub, and warns when it can see a
private one.
private one. See [A writer on the hub](#a-writer-on-the-hub-gitlab) for a GitLab hub
that has to live in the group of its targets.
- **`plan` holds no write credential**, and a test of the code's call graph keeps it from
ever reaching one.

## A writer on the hub (GitLab)

On GitLab a member of a group inherits its projects, and GitLab cannot lower an inherited
role on one project. A hub kept in the group that holds its targets is therefore reached
by the writer, a group access token or a service account of that group. What rule 5
protects is that a leaked write key cannot change what `distribute` ships;
`security.writer_on_hub: guard` gets that by verifying the hub instead of hiding it:

- the writer is below Maintainer on the hub (a Maintainer changes its protection,
settings and variables);
- the default branch is protected, and no rule that matches it lets the writer push or
merge (GitLab applies the most permissive matching rule);
- merge request pipelines run the CI file of the default branch
(*CI/CD configuration file* `.gitlab-ci.yml@<hub path>:<default branch>`): otherwise the
writer, pushing to a merge request's branch, could rewrite the CI file to drop the plan;
- a merge waits for a pipeline that succeeded, and a skipped pipeline does not count;
- `plan`, in every hub merge request pipeline, exits 2 when the writer or one of its
`known_authors` opened the merge request or pushed to its source branch since the
branch was created (read from GitLab's push events, which name who pushed), and when it
cannot tell; the setting comes from the default branch's `hub.yml`.

`doctor` grades these as `hub-guard` instead of `hub-hidden`, `doctor --hub-token` grades
the settings with a maintainer's token, and `touchmark setup gitlab` sets them up. Other
platforms report `hub-guard` as unknown: keep the writer off the hub there.

## The write key stays on the default branch

The write key must be visible only to jobs of the hub's default branch, which only
Expand Down
6 changes: 5 additions & 1 deletion docs/getting-started/gitlab.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ See [Set up a hub's platform](../guide/setup.md#gitlab).
to, and add the accounts, or create the group access tokens, only on groups that hold
targets and not the hub. A member of a group inherits its projects, and a group access
token is a member of its group, so a writer on a group that holds the hub could push
to it; `touchmark setup gitlab` refuses that layout, and `doctor` fails on it.
to it; `touchmark setup gitlab` refuses that layout, and `doctor` fails on it. If the
hub must stay in that group, set `security.writer_on_hub: guard` in `hub.yml` first:
setup then accepts the layout and sets up what keeps the writer, a Developer of the
hub, from getting content onto its default branch (see
[A writer on the hub](../concepts/security.md#a-writer-on-the-hub-gitlab)).
- The *reader* has the role Reporter and a token with `read_api` and
`read_repository`. Store it as the variable `TOUCHMARK_READ_TOKEN`, masked but not
protected: merge request pipelines need it.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each check has a name, a status and a detail. The names you will see:
| `scopes`, `2fa` | the write identity (Gitea, Forgejo) | the token lacks a scope the writer needs, or has more than it needs |
| `signing-key` | the write identity | `TOUCHMARK_<ID>_SIGNING_KEY` is set but cannot sign for the writer |
| `hub-hidden` | the write identity | the writer can see the hub, so a leaked write key could change the packs |
| `hub-guard` | the write identity, under `security.writer_on_hub: guard` | the writer reaches the hub and could get content onto its default branch: a Maintainer role, a rule that lets it push or merge, merge request pipelines that run the source branch's CI file, merges that do not wait for a pipeline that succeeded; GitLab only, unknown elsewhere |
| `markers` | the write identity | a pull request carries a marker with this hub's `id` but another fingerprint, or touchmark's marker from an unknown author |
| `access` | each target | the writer cannot write there; on GitHub, the App's installation has more permissions than touchmark asks for |
| `rules` | each target | a branch rule would block the sync branch or require what touchmark cannot do |
Expand Down
9 changes: 7 additions & 2 deletions docs/guide/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ with the checks of `doctor --hub-token`.

Everything goes through the API. The token needs the `api` scope and must belong to a
Maintainer of the hub and an Owner of the group whose projects are the targets (or to an
administrator). The hub must live outside that group. The final check also reads the
administrator). The hub must live outside that group, unless `hub.yml` says
`security.writer_on_hub: guard`: then setup accepts a hub inside it, checks that the writer
is below Maintainer on the hub (`writer-guard`), and also turns on *Pipelines must succeed*,
turns off *Skipped pipelines are considered successful* (`merge-checks`), and sets the *CI/CD
configuration file* to `.gitlab-ci.yml@<hub path>:<default branch>` (`ci-config`), so that
merge request pipelines run the default branch's CI file. The final check also reads the
variables of the groups above the hub, which GitLab shows to their Owners only: for a
Maintainer that check stays `unknown`.

Expand All @@ -32,7 +37,7 @@ setup:
create them only when an administrator allows it), group access tokens of the group
otherwise. `--accounts` chooses, `--reader-name` and `--writer-name` name them. The
reader is a Reporter of the group, the writer a Developer, and the writer must not
reach the hub;
reach the hub (under `writer_on_hub: guard`, not as a Maintainer);
- **mints their tokens straight into the hub's variables**: `TOUCHMARK_READ_TOKEN` masked
and not protected, since merge request pipelines read it, and `TOUCHMARK_WRITE_TOKEN`
protected, masked and hidden, with the environment scope `touchmark-distribute`. A
Expand Down
1 change: 1 addition & 0 deletions docs/project/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Secrets never appear on a command line: curl reads its `PRIVATE-TOKEN` header fr
That group access token is revoked before `migrate` runs: the bot of a revoked token must still be found and its merge request adopted.
- **`TestDoctor`**: `doctor` through `cli.Main` with the writer's token over a fresh subgroup: the token's expiry and scopes (`/personal_access_tokens/self`), a bot or service account (2fa ok); a Developer's access; protected branches over the sync branch, for Maintainers (rules fail) and for Developers without force pushes (warn); another hub's marker with the same id and a person's copy of this hub's (markers warn); a hub the writer does not see (`hub-hidden` ok), then one in its subgroup (fail). `--hub-token` with root's token reads the hub's variables and protected tags: a protected write key scoped to `touchmark-distribute` is ok, an unprotected one fails, a protected tag Developers may create fails.
- **`TestSetup`**: `touchmark setup gitlab` through `cli.Main` as a maintainer runs it, with the person's token: an Owner of the seeded group, whose fresh subgroup holds the targets (one project opted in), and an Owner of a separate top-level group where the hub lives (GitLab shows a group's variables, which the final check reads, to its Owners only; a Maintainer gets that check as `unknown` and exit code 3). The hub starts as GitLab makes projects (the default branch protected for Maintainers) and keeps a stale write key, a personal access token of the person that root mints, in an unprotected variable of every environment. The first run must end with no failed, manual or unknown step, and root then reads: the reader and the writer (service accounts where the instance lets the person create them, else group access tokens; the `FINDING` line says which) with Reporter and Developer in the group and no access to the hub; `TOUCHMARK_READ_TOKEN` masked and not protected with scope `*`; `TOUCHMARK_WRITE_TOKEN` protected, masked and hidden with scope `touchmark-distribute`, and the stale copy gone, its token revoked (`GET /personal_access_tokens/self` with it answers 401); the environment; `main` with push No one, merge Maintainers and no force push; no pipeline variables; `protect_merge_request_pipelines` false; the two schedules. A second run and a dry run have only `ok` steps and rotate no token (the active tokens are the same before and after). `doctor --hub-token` with the person's token grades the hub with no `fail` or `warn`. With the runner, a merge request (merged by the person: push to `main` is No one now) names the writer in `hub.yml` and adds a pipeline whose jobs on `main` must succeed: one without an environment runs `probe` (the write key is not visible there) and `plan` (the reader's token reads the group), one with `environment: {name: touchmark-distribute, action: prepare}` runs `doctor` with the writer's token. No token appears in any output or job log.
- **`TestWriterOnHub`**: `security.writer_on_hub: guard` on a hub in a fresh subgroup of the seeded group, which the writer reaches as a Developer: `main` protected (push No one, merge Maintainers), *Pipelines must succeed* without skipped pipelines, and the CI file read from `main` (`ci_config_path` `.gitlab-ci.yml@<hub>:main`). `doctor` with the writer's token grades `hub-guard` ok (and no `hub-hidden`). The person's merge request runs the default branch's `plan` on the runner, which passes. The writer then pushes to that merge request's branch a CI file of its own without `plan`: the pipeline still runs `main`'s, and `plan` exits 2 naming the writer's push. A merge request the writer opens fails the same way. With the CI file read from the source branch again, `hub-guard` fails (exit 1).
- **`TestProtectedBranch`**: wildcard protected branches, which the writer, a Developer, reads before it pushes. One target protects `touchmark/*` for Maintainers alone: a dry run and the first run block it `blocked:rules:protected-branch` before any write, with a warning naming the rule. Another lets Developers push but not force-push: the first run opens a merge request, and after a pack change the rebuilt branch needs a force push, which is refused, so the target is `blocked:rules:protected-branch` and its merge request stays as it was. A third has two rules on the sync branch, `touchmark/*` for Maintainers alone and the branch's own name for Developers with force pushes: GitLab applies the most permissive rule that matches a branch, so it is delivered and updated as if unprotected.
- **`TestFacts`**: GitLab behavior touchmark relies on that the documentation leaves open, through the API and git alone: 409 on a duplicate merge request; draft title prefixes and a `Draft:` commit; labels created by name and `add_labels`; quick actions in a person's description on create and update, against touchmark-shaped text; `closed_by` for a person and for a bot, and `merged_by`; a deleted source branch closes the merge request, who GitLab names as the closer (`closed_by` and `resource_state_events`), and what a reopen without the branch does; a force push keeps it open; how soon after a git push the merge request API takes the new branch (`push-then-create`, see below); `repository_object_format` and sha256 projects; git over HTTP with Basic `oauth2:<token>` and other user names; git with a group access token that has the scope `api` alone; descriptions of 200 000 bytes and around 1 MiB; the reader's `members/all` of the writer; rate-limit headers of a default instance; the bot of a revoked group access token, watched for 90 s (the user, and the author of its merge request); which users API view carries the `bot` field; what a fork of a project the reader cannot read shows; `GET /personal_access_tokens/self`; `push_rule` on CE; the pagination headers.

Expand Down
Loading
Loading