Ctadel

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

  1. Go to github.com → Settings → Developer settings → GitHub Apps → New GitHub App.

  2. 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 the installation_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 as CTADEL_GH_WEBHOOK_SECRET on the router (not on customer stacks).
  3. Repository permissions (only what we need — minimum surface):

    PermissionAccessWhy
    ContentsRead-onlyClone repos at the scanned commit.
    MetadataRead-onlyMandatory; lets us list installed repos.
    Pull requestsRead & writePost the PR comment (IaC, Secrets, SCA results).
    Commit statusesRead & writePost ctadel/iac, ctadel/secrets, ctadel/sca status checks.
  4. Subscribe to events: Push, Pull request.

  5. Where can this GitHub App be installed: Any account (so any customer org can install it).

  6. Click Create GitHub App. GitHub gives you an App ID at the top of the page.

  7. Scroll to Private keys → Generate a private key. A .pem file downloads. Keep it — you set its contents (or path) as CTADEL_GH_APP_PRIVATE_KEY below.

  8. 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

  1. As an org admin on a test GitHub org, open Ctadel Settings → Integrations.
  2. Click Install Ctadel on GitHub. You land on GitHub's installation page for your App.
  3. Choose All repositories (or a subset) and confirm.
  4. GitHub redirects back to Ctadel; you should see "GitHub App installed" and an integration row.
  5. 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-scanner logs).
    • PR comments and status checks on the next PR.

Permissions surface — what we asked for and why

PermissionWhy we need itWhy we don't ask for more
contents:readClone the repo at the scanned commit.We never push.
metadata:readRequired by GitHub on every App; gives us the repo list.
pull_requests:writePost a single combined PR comment (and update on rescans).We don't change PR titles, labels, reviewers.
statuses:writePost 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 401CTADEL_GH_WEBHOOK_SECRET doesn'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 GitHubCTADEL_GH_APP_SLUG is wrong; double-check it matches the apps/<slug> URL.

What's next