Repository navigation
feat: Route TLS #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: Route TLS #11
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
23e7636
feat: add tls-route-provisioner action
DerekRoberts 2602635
docs: add ADR to prevent JS conversion
DerekRoberts f99789c
fix(ops): inject bcgov/action-oc-runner step
DerekRoberts bc95a78
docs: update ADR to reference oc binary
DerekRoberts 0dd7bfd
docs: add tls-route-provisioner to READMEs
DerekRoberts 38dc316
feat(ops): add dry_run mode and PR validation workflow
DerekRoberts caa2f6c
fix(ops): address automated code review feedback
DerekRoberts 6f4fa4f
fix(ops): default insecure_skip_tls_verify to true
DerekRoberts bbe5932
feat(ops): add lifecycle tracking labels to backup secrets
DerekRoberts a0482ba
docs(ops): document backup labels and termination policy
DerekRoberts 20dcf03
feat(ops): run tls-route-provisioner locally from one script
DerekRoberts 0aab8eb
fix(ci): set least-privilege GITHUB_TOKEN on PR workflow
DerekRoberts a44bb8a
fix(ci): default the PR workflow token to no permissions
DerekRoberts b0db283
fix(ops): stop using action-oc-runner to install oc
DerekRoberts 82d1ad2
refactor: rename tls-route-provisioner to route-tls
DerekRoberts e4338ce
docs: lead with the Route TLS Action and prod GitHub secrets
DerekRoberts 012a575
fix: drop app label; default route_name to <repo>-vanity-url
DerekRoberts e3e74c2
docs: map Entrust/Sectigo PEMs to TLS GitHub secrets
DerekRoberts 6ec2df2
docs: use CAPS for GitHub secret and variable names
DerekRoberts 8c7731c
docs: TLS_CA_CERTIFICATE is the Entrust issuing CA only
DerekRoberts b21afef
docs: drop consumer-specific examples from Route TLS
DerekRoberts 76b64d8
docs: drop redundant Route app-label sentence
DerekRoberts 2fa167a
fix: require the issuing CA on every Route TLS apply
DerekRoberts 5a0615a
docs: stop calling the issuing CA optional
DerekRoberts f84ad72
fix: drop insecure_skip_tls_verify input
DerekRoberts 0911ff4
fix: pin oc, verify the issuing CA, keep TLS backups complete
DerekRoberts abf1088
fix: install oc via action-oc-runner instead of a pinned tarball
DerekRoberts 00c10ff
fix: reuse the oc-runner session and tighten PEM checks
DerekRoberts File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| name: PR | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| test-route-tls: | ||
| name: Test Route TLS | ||
| permissions: {} | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - name: Checkout Code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Generate dummy certificates | ||
| run: | | ||
| set -euo pipefail | ||
| openssl req -x509 -newkey rsa:2048 -keyout /tmp/key.pem -out /tmp/cert.pem \ | ||
| -days 30 -nodes -subj "/CN=myapp.bcgov.ca" \ | ||
| -addext "subjectAltName=DNS:myapp.bcgov.ca" | ||
| openssl req -x509 -newkey rsa:2048 -keyout /tmp/other-key.pem -out /tmp/other.pem \ | ||
| -days 30 -nodes -subj "/CN=other.example" | ||
| cp /tmp/cert.pem /tmp/ca.pem | ||
| echo "DUMMY_CERT<<EOF" >> "$GITHUB_ENV" | ||
| cat /tmp/cert.pem >> "$GITHUB_ENV" | ||
| echo "EOF" >> "$GITHUB_ENV" | ||
| echo "DUMMY_KEY<<EOF" >> "$GITHUB_ENV" | ||
| cat /tmp/key.pem >> "$GITHUB_ENV" | ||
| echo "EOF" >> "$GITHUB_ENV" | ||
| echo "DUMMY_CA<<EOF" >> "$GITHUB_ENV" | ||
| cat /tmp/ca.pem >> "$GITHUB_ENV" | ||
| echo "EOF" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Script dry-run | ||
| env: | ||
| ROUTE_HOST: myapp.bcgov.ca | ||
| ROUTE_NAME: my-test-route | ||
| TARGET_SERVICE: my-test-service | ||
| TLS_CERTIFICATE_FILE: /tmp/cert.pem | ||
| TLS_PRIVATE_KEY_FILE: /tmp/key.pem | ||
| TLS_CA_CERTIFICATE_FILE: /tmp/ca.pem | ||
| DRY_RUN: "true" | ||
| ROUTE_OUT: /tmp/route.yml | ||
| run: | | ||
| set -euo pipefail | ||
| output="$(./route-tls/provision.sh 2>&1)" || { echo "$output"; exit 1; } | ||
| printf '%s\n' "$output" | ||
| if printf '%s\n' "$output" | grep -qE 'BEGIN (PRIVATE KEY|RSA PRIVATE KEY)'; then | ||
| echo "provision.sh leaked a private key to stdout/stderr" | ||
| exit 1 | ||
| fi | ||
| grep -q '^ caCertificate: |' /tmp/route.yml | ||
| grep -q '^ certificate: |' /tmp/route.yml | ||
| grep -q '^ key: |' /tmp/route.yml | ||
| if grep -q '^ caCertificate:' /tmp/route.yml; then | ||
| echo "caCertificate was written as a spec sibling" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Script rejects mismatched key | ||
| env: | ||
| ROUTE_HOST: myapp.bcgov.ca | ||
| ROUTE_NAME: my-test-route | ||
| TARGET_SERVICE: my-test-service | ||
| TLS_CERTIFICATE_FILE: /tmp/cert.pem | ||
| TLS_PRIVATE_KEY_FILE: /tmp/other-key.pem | ||
| TLS_CA_CERTIFICATE_FILE: /tmp/ca.pem | ||
| DRY_RUN: "true" | ||
| ROUTE_OUT: /tmp/route-mismatch.yml | ||
| run: | | ||
| if ./route-tls/provision.sh; then | ||
| echo "expected cert/key mismatch to fail" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Script rejects wrong CA | ||
| env: | ||
| ROUTE_HOST: myapp.bcgov.ca | ||
| ROUTE_NAME: my-test-route | ||
| TARGET_SERVICE: my-test-service | ||
| TLS_CERTIFICATE_FILE: /tmp/cert.pem | ||
| TLS_PRIVATE_KEY_FILE: /tmp/key.pem | ||
| TLS_CA_CERTIFICATE_FILE: /tmp/other.pem | ||
| DRY_RUN: "true" | ||
| ROUTE_OUT: /tmp/route-ca.yml | ||
| run: | | ||
| if ./route-tls/provision.sh; then | ||
| echo "expected CA mismatch to fail" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Script rejects host mismatch | ||
| env: | ||
| ROUTE_HOST: wrong.example | ||
| ROUTE_NAME: my-test-route | ||
| TARGET_SERVICE: my-test-service | ||
| TLS_CERTIFICATE_FILE: /tmp/cert.pem | ||
| TLS_PRIVATE_KEY_FILE: /tmp/key.pem | ||
| TLS_CA_CERTIFICATE_FILE: /tmp/ca.pem | ||
| DRY_RUN: "true" | ||
| ROUTE_OUT: /tmp/route-host.yml | ||
| run: | | ||
| if ./route-tls/provision.sh; then | ||
| echo "expected host mismatch to fail" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Composite action dry-run | ||
| uses: ./route-tls | ||
| with: | ||
| hostname: myapp.bcgov.ca | ||
| target_service: my-test-service | ||
| tls_certificate: ${{ env.DUMMY_CERT }} | ||
| tls_private_key: ${{ env.DUMMY_KEY }} | ||
| tls_ca_certificate: ${{ env.DUMMY_CA }} | ||
| dry_run: "true" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # Route TLS | ||
|
|
||
| GitHub Action that applies an OpenShift Route with your TLS certificate, key, and issuing CA. openssl checks that the PEMs match, cover `hostname`, and are not expired. If the Route already has TLS, that material is snapshotted to a Secret before overwrite. | ||
|
|
||
| The Route uses `termination: edge` and `insecureEdgeTerminationPolicy: Redirect`. | ||
|
|
||
| Pin a tag or commit SHA, not `@main`. This action does not use `GITHUB_TOKEN` itself. Non-dry-run jobs use `bcgov/action-oc-runner`, which checkouts the caller repo (that step needs `contents: read` if the repository is private). | ||
|
|
||
| ## GitHub secrets | ||
|
|
||
| Put the PEMs on a **prod** GitHub Environment (not repository secrets), so pull-request jobs cannot read them. The job that calls this action must use that environment. OpenShift login uses environment secrets `OC_NAMESPACE` and `OC_TOKEN`, and variable `OC_SERVER`. | ||
|
|
||
| NR cert packages from Entrust look like `app.example.gov.bc.ca/`. Map them like this: | ||
|
|
||
| | Secret | File in the package | Notes | | ||
| | --- | --- | --- | | ||
| | `TLS_CERTIFICATE` | `<host>.pem` | The leaf only (CN/SAN is `hostname`). | | ||
| | `TLS_PRIVATE_KEY` | `<host>.key` | Unencrypted PKCS#8 (`BEGIN PRIVATE KEY`). Never commit this. | | ||
| | `TLS_CA_CERTIFICATE` | `Entrust OV TLS Issuing RSA CA 2.pem` | The issuing CA only. One PEM. | | ||
|
|
||
| Leave out **Sectigo Public Server Authentication Root R46.pem** and **USERTrust RSA Certification Authority.pem**. Those are roots; clients already have them. Leave out `<host>.csr`; the request is finished. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```yaml | ||
| - uses: bcgov/actions-openshift/route-tls@v1 | ||
| with: | ||
| hostname: app.example.gov.bc.ca | ||
| target_service: myapp-prod | ||
| tls_certificate: ${{ secrets.TLS_CERTIFICATE }} | ||
| tls_private_key: ${{ secrets.TLS_PRIVATE_KEY }} | ||
| tls_ca_certificate: ${{ secrets.TLS_CA_CERTIFICATE }} | ||
| oc_namespace: ${{ secrets.OC_NAMESPACE }} | ||
| oc_server: ${{ vars.OC_SERVER }} | ||
| oc_token: ${{ secrets.OC_TOKEN }} | ||
| ``` | ||
|
|
||
| `route_name` defaults to `<repository>-vanity-url` (e.g. `myapp-vanity-url`) so PR-close `app=` sweeps do not delete it. Override `route_name` if you already have a name. | ||
|
|
||
| First prod run: add `dry_run: "true"` until the job is green, then drop it. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Description | Required | Default | | ||
| | --- | --- | --- | --- | | ||
| | `hostname` | Route `spec.host` (no `https://`) | Yes | | | ||
| | `target_service` | Service to send traffic to | Yes | | | ||
| | `route_name` | OpenShift Route name | No | `<repo>-vanity-url` | | ||
| | `tls_certificate` | Leaf PEM | Yes | | | ||
| | `tls_private_key` | Private key PEM | Yes | | | ||
| | `tls_ca_certificate` | Issuing CA PEM | Yes | | | ||
| | `oc_namespace` | Namespace | unless `dry_run` | | | ||
| | `oc_server` | API URL | unless `dry_run` | | | ||
| | `oc_token` | Token | unless `dry_run` | | | ||
| | `dry_run` | Validate and write YAML only | No | `false` | | ||
|
|
||
| ## What it does | ||
|
|
||
| 1. Fail if the cert and key do not match, the issuing CA did not sign the leaf, the cert is expired, or the cert does not cover `hostname` (CN or SAN, including wildcards). | ||
| 2. Unless `dry_run`, snapshot the live Route's TLS (cert, key, CA) into a Secret named `<route>-backup-<sha256-prefix>`, labeled `backup-type=route-tls` (no `app` label). Re-applying the same cert is a no-op on that Secret. Restore from that Secret if an apply goes wrong. | ||
| 3. `oc apply` the Route (GitHub installs `oc` via `bcgov/action-oc-runner`). Private keys are never printed. | ||
|
|
||
| ## Local CLI (optional) | ||
|
|
||
| `provision.sh` is the same code the Action runs. Use it to dry-run PEMs on disk without GitHub or a cluster. | ||
|
|
||
| ```bash | ||
| cd route-tls | ||
| export ROUTE_HOST=app.example.gov.bc.ca | ||
| export ROUTE_NAME=myapp-prod-vanity-url | ||
| export TARGET_SERVICE=myapp-prod | ||
| export TLS_CERTIFICATE_FILE=/path/to/cert.pem | ||
| export TLS_PRIVATE_KEY_FILE=/path/to/key.pem | ||
| export TLS_CA_CERTIFICATE_FILE=/path/to/ca.pem | ||
| export DRY_RUN=true | ||
| ./provision.sh | ||
| ``` | ||
|
|
||
| Writes `route.yml` (contains the private key; gitignored; not printed). To apply from a laptop, unset `DRY_RUN` and set `OC_NAMESPACE`, `OC_SERVER`, and `OC_TOKEN`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Stay bash: openssl + oc. Do not convert to JavaScript. | ||
| name: "Route TLS" | ||
| description: "Applies an OpenShift Route with custom TLS (cert/key/CA), openssl checks, and archival backups." | ||
| inputs: | ||
| hostname: | ||
| description: "Route spec.host (e.g. myapp.gov.bc.ca). Hostname only, no scheme." | ||
| required: true | ||
| route_name: | ||
| description: "OpenShift Route name. Defaults to <repository>-vanity-url so it is not swept up with app= label cleanup." | ||
| required: false | ||
| default: ${{ github.event.repository.name }}-vanity-url | ||
| target_service: | ||
| description: "The OpenShift Service to send traffic to" | ||
| required: true | ||
| tls_certificate: | ||
| description: "Leaf certificate PEM" | ||
| required: true | ||
| tls_private_key: | ||
| description: "The private TLS key PEM" | ||
| required: true | ||
| tls_ca_certificate: | ||
| description: "Issuing CA PEM (Entrust OV TLS Issuing RSA CA 2)" | ||
| required: true | ||
| oc_namespace: | ||
| description: "OpenShift namespace. Required unless dry_run is true." | ||
| required: false | ||
| default: "" | ||
| oc_server: | ||
| description: "OpenShift API URL. Required unless dry_run is true." | ||
| required: false | ||
| default: "" | ||
| oc_token: | ||
| description: "OpenShift token. Required unless dry_run is true." | ||
| required: false | ||
| default: "" | ||
| dry_run: | ||
| description: "If true, skip the cluster: validate cert/key/host and write YAML only" | ||
| required: false | ||
| default: "false" | ||
|
|
||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: OpenShift CLI | ||
| if: ${{ inputs.dry_run != 'true' }} | ||
| uses: bcgov/action-oc-runner@111868d1fc50db0a40417ba321d865ef5c931bbd # v1.7.0 | ||
| with: | ||
| oc_namespace: ${{ inputs.oc_namespace }} | ||
| oc_server: ${{ inputs.oc_server }} | ||
| oc_token: ${{ inputs.oc_token }} | ||
|
|
||
| - name: Apply route TLS | ||
| shell: bash | ||
| env: | ||
| ROUTE_HOST: ${{ inputs.hostname }} | ||
| ROUTE_NAME: ${{ inputs.route_name }} | ||
| TARGET_SERVICE: ${{ inputs.target_service }} | ||
| TLS_CERTIFICATE: ${{ inputs.tls_certificate }} | ||
| TLS_PRIVATE_KEY: ${{ inputs.tls_private_key }} | ||
| TLS_CA_CERTIFICATE: ${{ inputs.tls_ca_certificate }} | ||
| OC_NAMESPACE: ${{ inputs.oc_namespace }} | ||
| OC_SERVER: ${{ inputs.oc_server }} | ||
| OC_TOKEN: ${{ inputs.oc_token }} | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| run: bash "${{ github.action_path }}/provision.sh" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.