A GitHub App that syncs Okta groups to GitHub teams, reports pull requests merged without meeting their branch's requirements, and reports stale security alerts, with Slack notifications. Run it as an HTTP server, a container, or an AWS Lambda function.
- Okta group sync - Sync Okta group members to GitHub teams, by group name or name pattern
- Orphaned user detection - Report organization members in no synced team
- Automatic reconciliation - Sync again when someone changes a team outside Okta
- PR compliance monitoring - Report PRs merged into monitored branches without the reviews or status checks that branch protection and rulesets require
- Security alerts monitoring - Report open Dependabot, code scanning, and secret scanning alerts older than a threshold
- GitHub App installer - Optionally create the GitHub App from a manifest with one click, using github-app-setup-go
- Slack notifications - Reports for each feature, in per-feature channels
- Go 1.27 or later, to build from source
- A GitHub App (setup guide), or the built-in installer
- Optional: an Okta API Services app (setup guide)
- Optional: a Slack app (setup guide)
Prebuilt binaries for the server and the Lambda function are attached to each release.
cp .env.example .env # then edit it
make run # or: make up, to run in docker composeOr build the binary or image:
make build # dist/github-ops-app
make image # github-ops-app:devOn shutdown the server lets running syncs finish, for up to 10 minutes; give
it a grace period that long, such as Kubernetes'
terminationGracePeriodSeconds: 630, or a sync may stop partway and finish on
the next run.
Schedule the Okta sync and security alert reports with any scheduler, such as a Kubernetes CronJob, by calling the scheduled endpoints with the admin token:
curl -X POST -H "Authorization: Bearer $APP_ADMIN_TOKEN" \
https://github-ops.example.com/scheduled/okta-syncmake build-lambda # dist/bootstrap, for the provided.al2023 runtimeAPI Gateway delivers webhooks and EventBridge runs the scheduled actions. See cmd/lambda/README.md, or deploy with terraform-aws-github-ops-app.
Every route is served under APP_BASE_PATH, if set.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks |
Signature | GitHub webhooks |
| GET | /healthz |
None | Liveness; always 200 |
| GET | /readyz |
None | 200 once the configuration loads |
| POST | /scheduled/okta-sync |
Admin token | Sync Okta groups to teams |
| POST | /scheduled/security-alerts |
Admin token | Report stale security alerts |
| POST | /scheduled/slack-test |
Admin token | Send a sample of every notification |
| GET | /server/status |
Admin token | Status and enabled features |
| GET | /server/config |
Admin token | Configuration, secrets redacted |
| GET | /setup |
None | GitHub App installer, when enabled |
Webhooks must carry a valid X-Hub-Signature-256; there is no unsigned mode.
The admin endpoints require Authorization: Bearer <APP_ADMIN_TOKEN> and are
disabled when no admin token is set. Webhooks from organizations other than
APP_GITHUB_ORG are ignored.
See .env.example for a complete example. The configuration
is validated at startup: an invalid value, or a partly configured feature, is
an error, and every problem is reported at once.
Any APP_ value may be an AWS SSM Parameter Store ARN, in any partition,
which is replaced by the parameter's decrypted value. The role needs
ssm:GetParameter (and kms:Decrypt for a customer managed key).
APP_GITHUB_WEBHOOK_SECRET=arn:aws:ssm:us-east-1:123456789012:parameter/github-ops-app/webhook-secret| Variable | Description |
|---|---|
APP_GITHUB_ORG |
Organization the app manages |
APP_GITHUB_APP_ID |
GitHub App ID |
APP_GITHUB_APP_PRIVATE_KEY |
Private key (PEM; \n escapes are accepted) |
APP_GITHUB_APP_PRIVATE_KEY_PATH |
Or a path to the private key file |
APP_GITHUB_WEBHOOK_SECRET |
Webhook secret |
APP_GITHUB_INSTALLATION_ID |
Installation ID; looked up from the org when unset |
APP_GITHUB_BASE_URL |
API URL for GitHub Enterprise Server |
With the installer enabled, the service starts without GitHub App credentials
and serves /setup, which creates the app from a manifest with the right
permissions and saves its credentials. The service then loads them without a
restart. See GitHub App Setup.
These settings use the names of github-app-setup-go.
| Variable | Description |
|---|---|
GITHUB_APP_INSTALLER_ENABLED |
true to serve the installer |
STORAGE_MODE |
envfile (default) or aws-ssm |
STORAGE_DIR |
Env file the envfile mode writes (./.env) |
AWS_SSM_PARAMETER_PREFIX |
Parameter path prefix for aws-ssm |
AWS_SSM_KMS_KEY_ID |
KMS key for aws-ssm (default: AWS managed key) |
APP_PUBLIC_URL |
External URL of the service, including the base path |
Setting any of these enables the sync and requires the domain, client ID, private key, and rules.
| Variable | Description |
|---|---|
APP_OKTA_DOMAIN |
Okta domain, such as acme.okta.com |
APP_OKTA_CLIENT_ID |
OAuth 2.0 client ID |
APP_OKTA_PRIVATE_KEY |
Private key (PEM, PKCS#1 or PKCS#8) |
APP_OKTA_PRIVATE_KEY_PATH |
Or a path to the private key file |
APP_OKTA_PRIVATE_KEY_ID |
Key ID (kid) registered with the OAuth app |
APP_OKTA_SCOPES |
Scopes (default: okta.groups.read,okta.users.read) |
APP_OKTA_GITHUB_USER_FIELD |
Profile attribute with the GitHub username |
APP_OKTA_SYNC_RULES |
JSON array of rules (see below) |
APP_OKTA_SYNC_SAFETY_THRESHOLD |
Max share of a team to remove (default: 0.5) |
APP_OKTA_ORPHANED_USER_NOTIFICATIONS |
Report orphaned users (default: true) |
| Variable | Description |
|---|---|
APP_PR_COMPLIANCE_ENABLED |
true to check merged PRs |
APP_PR_MONITORED_BRANCHES |
Branches or globs, such as main,release/* (main,master) |
| Variable | Description |
|---|---|
APP_SECURITY_ALERTS_ENABLED |
true to enable the report |
APP_SECURITY_ALERTS_MIN_AGE_DAYS |
Minimum alert age in days (default: 30) |
APP_SECURITY_ALERTS_MIN_SEVERITY |
critical, high (default), medium, or low |
Code scanning alerts use their security severity, or for rules without one,
error as high, warning as medium, and note as low. Secret scanning alerts
have no severity and count as high.
The token and channel are required together.
| Variable | Description |
|---|---|
APP_SLACK_TOKEN |
Bot token (xoxb-...) |
APP_SLACK_CHANNEL |
Default channel ID |
APP_SLACK_CHANNEL_PR_BYPASS |
Channel for PR bypass reports |
APP_SLACK_CHANNEL_OKTA_SYNC |
Channel for sync reports |
APP_SLACK_CHANNEL_ORPHANED_USERS |
Channel for orphaned user reports |
APP_SLACK_CHANNEL_SECURITY_ALERTS |
Channel for security alert reports |
APP_SLACK_FOOTER_NOTE_PR_BYPASS |
Footer for PR bypass reports, in mrkdwn |
| Variable | Description |
|---|---|
APP_ADMIN_TOKEN |
Token for the admin endpoints, at least 16 characters |
APP_LOG_LEVEL |
debug, info (default), warn, or error |
APP_LISTEN_ADDR |
Server address (default: :8080) |
APP_BASE_PATH |
Path every route is served under, such as an API stage |
Each rule maps one Okta group, by name, or every group matching a pattern, to a GitHub team:
[
{
"name": "engineering-teams",
"okta_group_pattern": "^github-eng-.*",
"strip_prefix": "github-eng-",
"github_team_prefix": "eng-",
"create_team_if_missing": true
},
{
"name": "platform-team",
"okta_group_name": "Platform Engineers",
"github_team_name": "platform",
"team_privacy": "secret"
}
]| Field | Description |
|---|---|
name |
Name used in reports |
enabled |
false to skip the rule (default: true) |
okta_group_name |
Exact name of one Okta group |
okta_group_pattern |
Regular expression matching Okta group names |
github_team_name |
Team name, for okta_group_name rules |
strip_prefix |
Prefix removed from the group name to derive the team name |
github_team_prefix |
Prefix added to the derived team name |
sync_members |
false to only create the team (default: true) |
create_team_if_missing |
Create missing teams; otherwise they are reported (default: false) |
team_privacy |
closed (default) or secret, for created teams |
A rule sets exactly one of okta_group_name and okta_group_pattern. Unknown
fields are rejected, so a misspelled field is an error instead of being
ignored.
Sync safety:
- Only members who can sign in to Okta are synced:
ACTIVE, andPASSWORD_EXPIRED,LOCKED_OUT, andRECOVERY, so an expired password does not remove anyone. Suspended and deprovisioned users are removed. - No one is removed from a team if more than the safety threshold of its members would be, which guards against an empty or partial group from Okta.
- Users who are not organization members are never removed.
- A group whose members cannot be read from Okta is reported and not synced.
- Two groups that map to the same team in one run are reported; only the first is synced, so they cannot remove each other's members.
- Okta usernames are validated as GitHub logins before they are used.
+------------+ +------------------------------------+ +-----------+
| GitHub | ---> | github-ops-app | ---> | Slack |
| webhooks | | | | reports |
+------------+ | +------------------------------+ | +-----------+
| | PR Compliance Check | |
+------------+ | | - required reviews | | +-----------+
| Okta | ---> | | - required status checks | | ---> | GitHub |
| groups | | +------------------------------+ | | Teams API |
+------------+ | | +-----------+
| +------------------------------+ |
+------------+ | | Okta Sync Engine | |
| Scheduler | ---> | | - map groups to teams | |
| (cron/EB) | | | - sync membership | |
+------------+ | +------------------------------+ |
| |
| +------------------------------+ |
| | Security Alerts Monitor | |
| | - dependabot alerts | |
| | - code scanning alerts | |
| | - secret scanning alerts | |
| +------------------------------+ |
+------------------------------------+
- Trigger: the schedule, or a
teamormembershipwebhook from a person, not a bot, about a team a rule manages - Fetch: read the Okta groups each rule matches, with their members
- Map: derive each group's GitHub team, creating it if allowed
- Sync: add and remove team members to match the group
- Report: post the changes, errors, and orphaned users to Slack
A sync requested while one is running does not run concurrently; the running
sync runs once more when it finishes. The server answers webhooks at once
(202) and syncs afterwards, since GitHub waits only 10 seconds; Lambda syncs
before it answers. Members of a team's child teams are never removed from the
parent team, where GitHub also lists them.
Orphaned user detection is skipped when a rule's team could not be found, since its members would otherwise be reported as orphaned.
- Receive: a
pull_requestwebhook for a PR merged into a monitored branch of the organization's repositories - Check: compare the PR's approving reviews and its head commit's status checks with what branch protection and rulesets require
- Report: post the PR to Slack when it did not meet them, with the merger's repository role when it explains the bypass
A reviewer counts once, by their latest review that approved, requested changes, or was dismissed. Check runs that are successful, neutral, or skipped pass, as on GitHub.
- Trigger: the schedule
- Fetch: read every page of open Dependabot, code scanning, and secret scanning alerts in the organization, oldest first
- Filter: keep alerts older than the minimum age at or above the minimum severity
- Report: post the alerts by repository to Slack, with any source that failed
make help # list targets
make test # all tests, with the race detector
make test-e2e # the scenarios in fixtures/scenarios.json against mock apis
make lint # golangci-lint
make vuln # govulncheck
make check # everything ci runsThe end-to-end tests run the real service against mock GitHub, Okta, and Slack APIs. Add a scenario to fixtures/scenarios.json to cover a new behavior.
401on webhooks:APP_GITHUB_WEBHOOK_SECRETdoesn't match the app's webhook secret.503 service not configured: the configuration did not load; the logs list every problem. With the installer enabled, finish the setup at/setup.403from GitHub when checking a PR: the app lacks a permission, such as Administration: read for branch protection. See the permissions.- Group not found from Okta: check the domain, scopes, and the group name.
- No Slack messages: check the token has
chat:writeand the bot is in the channel, or addchat:write.public.
Upgrading from v0? See Upgrading to v1.
MIT