Coverage and the GitHub App
Connect GitHub so a push that bypasses a recorder becomes a dated, attributed coverage gap instead of silence.
A recorder only sees what passes through it. A developer who never installs the hook, a
CI job that calls git push directly, an agent running on a laptop nobody enrolled —
none of them produce records. Coverage is a deployment property, not a cryptographic one.
The GitHub App closes that loop from the other side. It observes every push that reaches your organization, whatever ran on whichever machine, and the Console reconciles those observations against recorder records. An observed push with no matching record becomes a coverage gap: dated, attributed to a repository, ref and actor, and countable.
This is the honest answer to "what if someone works around the recorder". Sealr cannot prevent it. It can make it visible.
What the app is allowed to do
- Repository → Contents: Read-only
- Repository → Metadata: Read-only
- Subscribed events: Push (add Delete if you also want branch and tag deletions observed)
Nothing else. And no private key is generated or stored: the app never calls the GitHub API, it only receives signed webhooks. That is what keeps "Sealr holds no credential that can read your code" true rather than aspirational.
Two connection models
The Console supports both at once. Choose per customer; there is no global switch.
| Model A — shared Sealr app | Model B — your own app | |
|---|---|---|
| Who registers the app | Sealr, once | You, on your org |
| Effort to connect | one click: Install | register the app, copy three values |
| Who holds the webhook secret | Sealr (server config, never shown) | you |
| Blast radius if the secret leaks | every tenant on that Console | that one installation |
| Best for | the default | sovereign or regulated estates that will not route events through a vendor-controlled app |
| Console requirement | GITHUB_APP_SLUG and GITHUB_APP_WEBHOOK_SECRET set | none |
In both models the tenant is taken from the stored installation→tenant binding, never from the webhook payload, and no event is recorded until an HMAC-SHA256 over the raw body verifies in constant time.
Once, by whoever runs the Console. Register a GitHub App under Settings → Developer settings → GitHub Apps → New GitHub App:
- Webhook → Active: on, URL
https://<console-host>/v1/ingest/github - Webhook secret: a long random string — this is
GITHUB_APP_WEBHOOK_SECRET, and customers never see it - Setup URL (Post-installation):
https://<console-host>/v1/integrations/github/callback, with Redirect on update ticked — this is what binds an installation to a tenant - Permissions: Contents read-only, Metadata read-only
- Subscribe to events: Push
Then set two environment variables on the Console and restart:
GITHUB_APP_SLUG=sealr # github.com/apps/<slug>
GITHUB_APP_WEBHOOK_SECRET=<the secret you generated>GET /v1/integrations/github/capabilities now reports sharedApp: true and the Console
shows the one-click button.
Per customer. In the Console: Coverage → Connect GitHub. That calls
POST /v1/integrations/github/connect, which returns a GitHub install URL carrying a
signed, 15-minute state naming the tenant. GitHub shows its own Install & choose
repositories screen; on confirm it redirects to the Setup URL with installation_id and
state. The Console verifies the state's signature and expiry and binds that installation
to that tenant.
A forged or expired state creates no binding, and an installation already bound to another tenant is never silently moved.
The test that proves it works
Do not test the plumbing. Test the guarantee. With a repository connected:
# 1. A push THROUGH a recorder — should reconcile cleanly.
sealr run --resource repo:acme/api -- git push origin feature-a
# 2. A push that BYPASSES the recorder entirely — plain git.
git push origin feature-bWait for the reconciliation grace window, then open Coverage in the Console. Push #2
appears as a coverage gap naming the repository, ref, actor and time; a coverage.gap
webhook fires; and the Exposure Report counts it under unrecorded_pushes. Push #1 does
not appear.
That is the product's promise made visible: not "nobody can bypass the recorder", but "bypassing it is seen".
How reconciliation decides
An observed push becomes eligible for reconciliation once it is older than the grace
window — 5 minutes by default, set with RECONCILE_GRACE_SECONDS — so a push whose
record is still syncing is not flagged.
A recorder record accounts for an observation when all of these hold:
- the record's integration is
git; - its target resource equals
repo:<full_name>of the observed repository; - the record's wall time is within 15 minutes either side of the observation;
- when the observation names a ref, the record's projected operation contains the short
ref name (
refs/heads/main→main).
The match is on repository, ref name and a generous time window rather than an exact identifier, because the two sides have independent clocks and the recorder only ever projects the redacted command line. Being generous is deliberate: false gaps train operators to ignore the report, which costs more than an occasional missed match.
For a push to reconcile, the recorder must record it against the matching resource.
sealr run --resource repo:acme/api -- git push … does that. A recorded push with no
--resource gets a cmd:git target resource and will not match an observation.
Everything reconciled is written back: the observation is marked with its matching record
id, or flagged as a gap. Gaps are durable rows — the coverage.gap webhook is
best-effort alerting on top of them, not the evidence itself.
Reading the result
- Coverage — the gap list: repository, ref, actor, whether the push was forced or a
deletion, and when it was observed.
GET /v1/coverage/gaps?days=… - Exposure Report —
unrecorded_pushesover the reporting window. - Overview — the same figure leads the page, because it describes what you do not have.
The Console runs a reconciliation pass every 60 seconds across all tenants. You can force
one with POST /v1/coverage/reconcile rather than waiting.
What this does not give you
- It observes pushes to GitHub. An agent that writes to a database, applies Terraform, or pushes to a different forge is outside its view.
- A gap says an observed push had no matching record. It does not say who intended to
bypass anything — a mis-set
--resource, a recorder that was offline past the match window, or a genuinely unrecorded machine all look the same from here. - It is detection, not prevention. Read what Sealr proves before you rely on it.
Approvals
How a REQUIRE_APPROVAL verdict holds an operation, who may decide, what is recorded, and what happens when the Console is unreachable.
Verifying evidence
For auditors — get the open-source verifier, run it against a .seal bundle, read the report, and check the conformance vectors. Nothing from the vendor is required.