diff --git a/README.md b/README.md index e058793..109b74f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -# Chinmina Bridge: Buildkite/Github OIDC token bridge +# Chinmina Bridge -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjamestelfer%2Fchinmina-bridge.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjamestelfer%2Fchinmina-bridge?ref=badge_shield) +**Connect Buildkite to GitHub with secure, short-lived tokens.** +Chinmina Bridge allows Buildkite agents to securely generate GitHub API tokens +that can be used to perform Git or other GitHub API actions. It is intended to +be an alternative to the use of SSH deploy keys or long-lived Personal Access +Tokens. -Allows Buildkite agents to get valid GitHub tokens that can be used to perform -Git or other GitHub API actions. It is intended to be an alternative to the use -of SSH deploy keys or long-lived Personal Access Tokens. +![High level Chinmina diagram](docs/chinmina-high-level.png) The bridge itself is an HTTP endpoint that uses a [GitHub application][github-app] to create [ephemeral GitHub access @@ -13,227 +15,29 @@ tokens][github-app-tokens]. Requests are authorized with a [Buildkite OIDC][buildkite-oidc] token, allowing a token to be created just for the repository associated with an executing pipeline. -The token is created with `contents:read` permissions, and only has access to -the repository associated with the executing pipeline. - -Two endpoints are exposed: `/token`, which returns a token and its expiry, and -`/git-credentials`, which returns the token and repository metadata in the [Git -Credentials format][git-credential-helper]. +> [!NOTE] +> Find out more about Chinmina Bridge is available in the [documentation][docs]. +> +> This has and expanded [introduction][docs-intro], a [getting +> started][docs-started] guide and a detailed [configuration +> reference][docs-config]. This has a more detailed description of the +> implementation, and clear guidance on how to configuration and installation. [github-app]: https://docs.github.com/en/apps [github-app-tokens]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app [buildkite-oidc]: https://buildkite.com/docs/agent/v3/cli-oidc [git-credential-helper]: https://git-scm.com/docs/gitcredentials#_custom_helpers -## Overview - -`chinmina-bridge` is used by jobs running on a Buildkite agent to request tokens -from Github. These can be used to communicate with the GitHub API or (via Git) -to enable authenticated Git actions. - -Git authentication is facilitated by a [Git credential -helper](https://github.com/jamestelfer/github-app-auth-buildkite-plugin), which -communicates with the bridge and supplies the result to Git in the appropriate -format. - -The following sequence illustrates a Git authentication flow facilitated by -`chinmina-bridge`. - -```mermaid -sequenceDiagram - box Buildkite Agent - participant Buildkite Job - participant Git - participant Credential Helper - end - box Self hosted - participant Chinmina Bridge - end - Buildkite Job->>+Git: clone - Git ->>+ Credential Helper: get credentials - Credential Helper->>+Buildkite API: Request Buildkite OIDC token - Buildkite API->>-Credential Helper: bk-oidc - Credential Helper->>+Chinmina Bridge: Request GH token (auth bk-oidc) - Chinmina Bridge->>+Buildkite API: Get Pipeline Details - Buildkite API-->>-Chinmina Bridge: pipeline-repository - Chinmina Bridge->>+GitHub: Create Token (auth app JWT) - GitHub-->>-Chinmina Bridge: app-token - Chinmina Bridge->>-Credential Helper: bk-oidc - Credential Helper->>-Git: "x-access-token"/app-token - Git-->>-Buildkite Job: complete -``` - -## Why? - -There are two options generally used to authenticate Buildkite agents to GitHub: - -1. Via a PAT (owned by a GitHub user) that is saved in the agent S3 secrets bucket -2. Via a deploy key (registered to a single repository) that is likewise saved to - S3. - -As the organization scales however, the overhead of managing them becomes -unwieldy, and it can be quite difficult for an organisation to successfully -manage a rotation scheme. - -Unless centralized issuance is practiced as well, both of these schemes can -produce tokens that are tied to a user, leading to unexpected problems when a -user leaves the organization. There is also the potential for key material to be -stored or shared incorrectly, leading to increased possibility of accidental -leakage. - -Lastly, all key material is typically stored in an S3 bucket. This is -straightforward to configure and maintain, but creates a significant issue in -the event of an account/bucket breach. - -Using a GitHub application to authenticate GitHub actions allows: - -1. Access keys for repositories are generated on demand and expire after one - hour. -1. The generated tokens are only kept by a build agent for the duration of the - step, and do not require any other persistence. -1. The private key for the GitHub application is specific to the - `chinmina-bridge` service. It can (and should) be rotated, an operation that - is easy to perform. -1. Supplied tokens are scoped to just the repositories and actions necessary for - the requesting pipeline. -1. Additional Buildkite configuration per repository is not required. If the - application has access, the agent can request a token for it. No need to - create PATs or generate keypairs, and no need to upload them in multiple - places. This allows the an organization to have tighter access control on - pipeline setup without creating additional support overhead. -1. Tokens can enable a wider set of actions than simple Git operations (e.g. PR - comments). This is not yet implemented in `chinmina-bridge`, but is a high - priority for future enhancement. - -Also, since `chinmina-bridge` uses Buildkite's OIDC tokens to authorize requests, -the claims associated with the token can be used to further refine access to a token. - -Github has some [good documentation][gh-deploy-keys] about the pros and cons of -the application token approach. There are two primary downsides documented: - -> - Additional setup is needed to create the GitHub App. -> - Installation access tokens expire after 1 hour, and so need to be -> re-generated, typically on-demand using code. - -`chinmina-bridge` solves the second problem, by making token generation for a -pipeline at build time trivial. - - -[gh-deploy-keys]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#github-app-installation-access-tokens - -### What's right for your organization? - -To understand what's right for your organization, consider: - -- how many pipelines do you have? (That is, how many keys are managed?) -- how easily are tokens rotated? -- (related) if the secrets bucket is somehow compromised, how difficult would it be for the organization to respond? -- if tokens are issued to a user, does a person leaving cause an outage in a build pipeline? -- what processes/restrictions does your organization have around repository access in GitHub and pipeline creation in Buildkite? - -## Limitations - -- can only grant `contents:read` access -- will only grant access to the repository associated with a pipeline -- if the buildkite user has permissions to modify the pipeline repository, they - may configure a repository that they don't have access to in GitHub (but is - accessible in the app). This would allow them to potentially extract code via - use of the pipeline step configuration. **BUT**: - - it's OK if your organization members have read access to the same set of - repositories covered by the `chinmina-bridge` GitHub application.
- **OR** - - it's OK if your organization controls the creation/configuration of - pipelines: this restricts the opportunity to misconfigure a pipeline. - -## Operations - -See the [observability documentation](./docs/observability.md) for more details -on the information provided by the system when running. - -## Configuration - -Requirements: - -1. A Buildkite organization, and a user with sufficient access to create an API - token that can be used to get the details of any pipeline that is expected to - be built. -1. A Github organization, and a user with sufficient permissions to create a - Github App and install it into the organization. -1. Ability to deploy a server that can be accessed by the build agents (for example, an ECS service) -1. Ability to allow Buildkite agents to download and use a custom plugin _or_ - ability to add a plugin to the default settings of the Buildkite agents. - -### Buildkite setup - -Create an API key with access to the REST API **only** with access to the `read_pipelines` scope. - -Save the key securely: it will be provided to the server in a later step. Use a -"bot" user to create the token if you can. - -### Github setup - -1. Create an application in your Github organization - - The application must request `contents:read` - - Note the application ID - - Create and save a private key for the application -2. Install the application into the Github organization - - choose the repositories the application will have access to. This is the - limit of the resources that the application can vend tokens for. - -### Configure and deploy the bridge server - -The server is a Go application expecting to read configuration from environment -variables, and can be deployed to a server or as a container. - -#### Variables - -**Server** - -- `SERVER_PORT` (optional, default `8080`): the TCP port the server will listen on. -- `SERVER_SHUTDOWN_TIMEOUT_SECS` (optional, default `25`): the number of seconds - the server will wait when asked to terminate with `SIGINT` - -**Authorization** - -- `JWT_BUILDKITE_ORGANIZATION_SLUG` (**required**): the slug of your Buildkite - organization. This is the identifier of your organization that appears in your - Buildkite URLs. -- `JWT_AUDIENCE` (optional, default=`app-token-issuer`): The expected value of the - `aud` claim in the JWT. Describes the intended audience of the issued JWT - token, guards against token reuse. Using a non-default value will require configuration of the credentials helper plugin. -- `JWT_ISSUER_URL` (optional, default `https://agent.buildkite.com`): the - expected value of the `iss` claim in the agent JWT. Also used to discover the - JWKS configuration from the `.well-known` address. -- `JWT_JWKS_STATIC` (optional): a local JWKS JSON file that can be used instead - of Buildkite. Used to verify the JWT sent by the Buildkite agents to the - server. This should only be required for server testing, as agents will only - create a token using the Buildkite key. - -**Buildkite API** - -- `BUILDKITE_API_TOKEN` (**required**): The API token created for pipeline - metadata lookups. **Store securely and provide to the container securely.** - -**GitHub API connectivity** - -- `GITHUB_APP_PRIVATE_KEY` (**required**): The PEM formatted private key of the - created Github app. **Store securely and provide to the container securely.** - This is a highly sensitive credential. -- `GITHUB_APP_ID` (**required**): The application ID of the Github application - created above. -- `GITHUB_APP_INSTALLATION_ID` (**required**): The installation ID of the - created Github application into your organization. +[docs]: https://chinmina.github.io +[docs-intro]: https://chinmina.github.io/introduction/ +[docs-started]: https://chinmina.github.io/guides/getting-started/ +[docs-config]: https://chinmina.github.io/reference/configuration/ ## Contributing -Contributions are welcome. - -- `direnv` is the tool for setting up the test environment -- some variant of docker compose makes it easier to run locally -- Run `make keygen` to create test keys -- Execute `git` commands in the `.development/keys` directory. This has git - configuration set up so it uses a local credential helper that will use the - keys in the `.development/keys` directory. +This project welcomes contributions! Take a look at the outstanding issues for +something to dip your toes into, open an issue to get some input, or raise a PR +if you're confident. ## License diff --git a/docs/chinmina-high-level.png b/docs/chinmina-high-level.png new file mode 100644 index 0000000..d62a341 Binary files /dev/null and b/docs/chinmina-high-level.png differ diff --git a/docs/kms.md b/docs/kms.md deleted file mode 100644 index d536e04..0000000 --- a/docs/kms.md +++ /dev/null @@ -1,84 +0,0 @@ -# Using KMS to sign GitHub JWTs - -It is more secure (though more complicated) to provide Chinmina with an AWS KMS key to sign JWTs for GitHub requests. - -## Uploading the KMS key - -1. [Generate the private key][github-key-generate] for the GitHub application. - -2. Check the private key and convert it ready for upload - - the key spec for your GitHub key _should_ be RSA 2048. To verify that this is - the case, run `openssl rsa -text -noout -in yourkey.pem` and examine the - output. - - convert the GitHub key from PEM to DER format for AWS: - - ```shell - openssl rsa -inform PEM -outform DER -in ./private-key.pem -out private-key.cer - ``` - -3. Follow the [AWS instructions][aws-import-key-material] for importing the - application private key into GitHub. This includes creating an RSA 2048 key - of type "EXTERNAL", encrypting the key material according to the instructions - and uploading it. - -4. Create an alias for the KMS key to allow for easy [manual key - rotation][aws-manual-key-rotation]. - -> [!IMPORTANT] -> A key alias is essential to allow for key rotation. Unless you're stopped -> by environmental policy, use the alias. The key will be able to be rotated -> without any service downtime. - -5. Ensure that the key policy has a statement allowing Chinmina to access the key. The specified role should be the role that the Chinmina process has access to at runtime. - - ```json - { - "Sid": "Allow Chinmina to sign using the key", - "Effect": "Allow", - "Principal": { - "AWS": [ - "arn:aws:iam::226140413739:role/full-task-role-name" - ] - }, - "Action": [ - "kms:Sign" - ], - "Resource": "*" - } - ``` - -> [!TIP] -> Chinmina does not assume a role to access the key. It assumes valid -> credentials are present for the AWS SDK to use. - -## Configuring the Chinmina service - -1. Set the environment variable `GITHUB_APP_PRIVATE_KEY_ARN` to the ARN of the **alias** that has just been created. - -2. Update IAM for your key - 1. The KMS key resource policy needs to allow the service to use the key - _for signing only_. - 2. The IAM policy for the Chinmina process (i.e. the AWS role available to - Chinmina when it runs) needs to be able to use the _alias_ created for - the private key. This is done with a condition in the policy element: - - ```json - { - "Action": "kms:Sign", - "Effect": "Allow", - "Resource": "*", - "Condition": { - "StringEquals": { - "kms:RequestAlias": "alias/chinmina-signing", - }, - }, - } - ``` - - Using the `kms:RequestAlias` condition instead of the fully qualified - key ARN in the `resource` attribute allows for transparent key rotation - without service interruption. - -[github-key-generate]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#generating-private-keys -[aws-import-key-material]: https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys.html -[aws-manual-key-rotation]: https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html#rotate-keys-manually diff --git a/docs/releases.md b/docs/releases.md deleted file mode 100644 index 023606a..0000000 --- a/docs/releases.md +++ /dev/null @@ -1,72 +0,0 @@ -# Release process - -In short: - -1. Releases are triggered by creating a release tag from `main`. This is currently manual. -2. Release tags conform to semantic versioning -3. Commits use conventional commit messages to aid in the changelog creation process -4. A GoReleaser pipeline is used to create the artifacts -5. All artifacts (binaries and images) are signed by the build process using `cosign` - -## When is a release ready? - -Releases are created on an as-needed basis. We prefer multiple, smaller releases over releases that have a greater number of changes. - -A release is ready when: - -- there are committed changes on `main`, and -- there is confidence in its stability. - -Stability is a pre-requisite for merging, so there should not be significant questions about the appropriateness of a `main` release. - -## Triggering a release - -Releases are triggered via the creation of a semantic-versioned tag, in the format `vX.Y.Z`. Creation of a tag in this format triggers the automated release process. - -Only repository administrators may create a tag in this format. - -## Release signing - -The [Sigstore][sigstore] ecosystem is leveraged for signing executable release outputs. ([Docs][sigstore-docs].) - -- [`cosign`][cosign] is used as the signing CLI tool -- The [`fulcio`][fulcio] public-good instance is used for ephemeral signing certificates -- The [`rekor`][rekor] [public-good instance][rekor-search] is used for Certificate Transparency record publishing. - -The signing process allows some useful attributes of the binaries to be verified: - -- the provider of the identity for the build process (i.e. GitHub Actions) -- the build process that was used to generate them (both scripts and compute) -- the Git reference of the code that was used to build the binary - -Releases are signed with `cosign`, with transparency records published to the [public-good Rekor instance]. - -[sigstore]: https://www.sigstore.dev/ -[sigstore-docs]: https://docs.sigstore.dev/ -[cosign]: https://github.com/sigstore/cosign?tab=readme-ov-file -[fulcio]: https://github.com/sigstore/fulcio?tab=readme-ov-file -[rekor]: https://github.com/sigstore/rekor?tab=readme-ov-file -[rekor-search]: https://search.sigstore.dev/ - -## Testing the release process - -It is possible to run GoReleaser locally to test some of the release proceses. -(`goreleaser` must be available.) - -```shell -# from the root of the local working copy -goreleaser release --clean --verbose --skip "announce,validate" -``` - -This will run the binary and image builds, and publish a temporary image to -[`ttl.sh`](https://ttl.sh/). Temporary images can be used in local testing with -`docker compose`. - -Some processes are skipped when doing this: - -- binary signing -- image signing -- changelog generation -- GitHub release creation - -Thus release testing verifies a proportion of the GoReleaser configuration, and allows the image/binary builds to be integration tested. diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md deleted file mode 100644 index 93960a7..0000000 --- a/docs/verifying-releases.md +++ /dev/null @@ -1,92 +0,0 @@ -# Verifying releases with `cosign` - -Releases [are signed](./releases.md) with `cosign` as part of the release -process. The build produces additional attestation bundles during this process, -which can be used to verify both binaries and Docker images. - -For binaries, bundles are present in the `tar.gz` archive created by the -release. For images, bundles are stored in the OCI registry alongside the image -itself. - -## Obtaining `cosign` - -Download from the [`sigstore/cosign` project on GitHub][cosign-download], and -[verify the release][cosign-verify] as you prefer. - -[cosign-download]: https://github.com/sigstore/cosign?tab=readme-ov-file#installation -[cosign-verify]: https://docs.sigstore.dev/cosign/system_config/installation/#verifying-cosign-releases - -## Release identity - -The certificates issued by the release are issued for the GitHub Actions OIDC -provider, and the identity is the executed workflow, referenced by the Git tag -being built. - -| Field | Format | -|-|-| -| Issuer | `https://token.actions.githubusercontent.com` | -| Identity | `https://github.com/jamestelfer/chinmina-bridge/.github/workflows/release.yaml@refs/tags/` | - -> [!IMPORTANT] -> **Git tags are not static:** they can be updated to point to a different -> commit SHA. Examine the recorded claims for the exact commit. -> -> There are claims recorded for the exact commit of both the workflow that -> produced the artifact and the commit that the artifact source was built from. - -## Verifying an image release - -Images are published to Docker Hub in the `chinmina` repository. The images are -named `chinmina-bridge` and are labelled with their release tag (`vX.Y.Z`). - -An image can be verified with the following `cosign` command: - -```shell -TAG=vX.Y.Z \ -cosign verify "chinmina/chinmina-bridge:$TAG" \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-identity "https://github.com/jamestelfer/chinmina-bridge/.github/workflows/release.yaml@refs/tags/$TAG" \ - --output text - -# more details are available if you use JSON output: -TAG=vX.Y.Z \ -cosign verify "chinmina/chinmina-bridge:$TAG" \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-identity "https://github.com/jamestelfer/chinmina-bridge/.github/workflows/release.yaml@refs/tags/$TAG" \ - --output json | jq -``` - -The path `.[].optional.Bundle.Payload.logIndex` is the index entry in the public -transparency log, recording the details of the signing event. The details of the -event can be found at: https://search.sigstore.dev/. - -## Verifying the binary releases - -Download and extract the `tar.gz` of the binary you're interested in. The -artifacts present include both the binary itself (named `chinmina-bridge`) and -the signing bundle (`chinmina-bridge.cosign.bundle`). - -```shell -# declare the release details for download -TAG=vX.Y.Z -ARCH=arm64 - -# download the binary -curl -L -o chinmina-bridge_linux_${ARCH}.tar.gz \ - https://github.com/jamestelfer/chinmina-bridge/releases/download/${TAG}/chinmina-bridge_linux_${ARCH}.tar.gz - -# extract to the current directory -tar xvf chinmina-bridge_linux_${ARCH}.tar.gz - -# verify -cosign verify-blob \ - chinmina-bridge \ - --bundle chinmina-bridge.cosign.bundle \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-identity "https://github.com/jamestelfer/chinmina-bridge/.github/workflows/release.yaml@refs/tags/$TAG" - -# peek the details -jq -r '.rekorBundle.Payload.logIndex | "https://search.sigstore.dev/?logIndex=\(.)"' < chinmina-bridge.cosign.bundle - -# open the URL that is shown -```