Set up the Ctadel GitHub App
Ctadel authenticates to GitHub as a GitHub App (not as a user). One App is registered for
the whole Ctadel SaaS; every customer org installs that App on their side. The webhook URL of
the App points to the central webhook router (webhook.ctadel.eu),
which dispatches each event to the right per-tenant Ctadel stack.
The App's RSA private key is replicated to each customer stack (so each stack mints its own installation tokens locally without a runtime dependency on the router). The router itself only holds the webhook secret — never the private key.
Step 1, create the App
-
Go to github.com → Settings → Developer settings → GitHub Apps → New GitHub App.
-
Fill in:
- GitHub App name: e.g.
Ctadel Twin. This is the public name customers see during install. - Homepage URL:
https://ctadel.eu. - Callback URL: leave empty. We don't use user OAuth, only App installations.
- Setup URL:
https://webhook.ctadel.eu/callback(the central router's callback — not a per-tenant URL). After install GitHub redirects there with theinstallation_id; the router persists the mapping and bounces the customer back to their stack. - Webhook URL:
https://webhook.ctadel.eu/webhook/git(the central router again). Every customer's webhook flows through there. - Webhook secret: generate a long random string (e.g.
openssl rand -hex 32). Save it — you set it asCTADEL_GH_WEBHOOK_SECRETon the router (not on customer stacks).
- GitHub App name: e.g.
-
Repository permissions (only what we need — minimum surface):
Permission Access Why Contents Read-only Clone repos at the scanned commit. Metadata Read-only Mandatory; lets us list installed repos. Pull requests Read & write Post the PR comment (IaC, Secrets, SCA results). Commit statuses Read & write Post ctadel/iac,ctadel/secrets,ctadel/scastatus checks. -
Subscribe to events:
Push,Pull request. -
Where can this GitHub App be installed: Any account (so any customer org can install it).
-
Click Create GitHub App. GitHub gives you an App ID at the top of the page.
-
Scroll to Private keys → Generate a private key. A
.pemfile downloads. Keep it — you set its contents (or path) asCTADEL_GH_APP_PRIVATE_KEYbelow. -
Optional but recommended: upload a logo (the Ctadel hex logo works well).
Step 2, note the App slug
The App URL is https://github.com/apps/<slug>. The slug is derived from the App name (e.g. ctadel-scanner). You set it as CTADEL_GH_APP_SLUG below — it's what the Settings page links to when a customer clicks Install on GitHub.
Step 3, set the env vars (split across router and stacks)
On the central webhook router (gateway/.env, see Set up the webhook router):
CTADEL_GH_APP_SLUG=ctadel-twin
CTADEL_GH_WEBHOOK_SECRET=<the long random string from Step 1>
CTADEL_INSTALL_SIGNING_KEY=<openssl rand -hex 32 — also on every stack>
CTADEL_INTER_SVC_SECRET=<openssl rand -hex 32 — also on every stack>
CTADEL_TENANT_BASE_DOMAIN=ctadel.eu
On each per-customer stack (.env.prod):
# Identity of the App (public)
CTADEL_GH_APP_ID=123456
CTADEL_GH_APP_SLUG=ctadel-twin
# Stack's tenant id + how to reach the router
CTADEL_TENANT_ID=acme
CTADEL_WEBHOOK_ROUTER_URL=https://webhook.ctadel.eu
CTADEL_INSTALL_SIGNING_KEY=<same value as on the router>
CTADEL_INTER_SVC_SECRET=<same value as on the router>
# RSA private key for minting installation tokens — replicated per stack
CTADEL_GH_APP_PRIVATE_KEY_PATH=/run/secrets/ctadel-gh-app.pem
The stack does not need CTADEL_GH_WEBHOOK_SECRET — only the router does.
Step 4, restart the stack
docker compose up -d --force-recreate csm-orchestrator csm-frontend
Tail orchestrator logs and check that you see:
GitHub App initialized (app_id=123456)
If you see "GitHub App not configured" the env vars weren't picked up — verify the file path and the container env.
Step 5, smoke-test as a customer
- As an org admin on a test GitHub org, open Ctadel Settings → Integrations.
- Click Install Ctadel on GitHub. You land on GitHub's installation page for your App.
- Choose All repositories (or a subset) and confirm.
- GitHub redirects back to Ctadel; you should see "GitHub App installed" and an integration row.
- Push a trivial commit to one of the selected repos. You should see:
- The webhook delivered (GitHub → App settings → Advanced → Recent deliveries).
- A scan dispatched (
csm-git-scannerlogs). - PR comments and status checks on the next PR.
Permissions surface — what we asked for and why
| Permission | Why we need it | Why we don't ask for more |
|---|---|---|
contents:read | Clone the repo at the scanned commit. | We never push. |
metadata:read | Required by GitHub on every App; gives us the repo list. | — |
pull_requests:write | Post a single combined PR comment (and update on rescans). | We don't change PR titles, labels, reviewers. |
statuses:write | Post ctadel/iac, ctadel/secrets, ctadel/sca so branch protection can gate merges. | We don't post arbitrary check runs. |
If a customer's security team asks: this is strictly less than what a PAT user-token gave us before, and Ctadel cannot read private branches outside the installed repos.
Troubleshooting
- Webhook delivery shows 401 —
CTADEL_GH_WEBHOOK_SECRETdoesn't match the App's webhook secret. Both must be byte-identical. mint installation token … 401— the App ID and the private key don't match. Re-download the key from the App's Private keys page.mint installation token … 404— the installation was deleted by the customer admin (they removed the App from their org). Re-install or mark the integration disabled.- Customer sees blank Install page on GitHub —
CTADEL_GH_APP_SLUGis wrong; double-check it matches theapps/<slug>URL.