Skip to content
Merged
Show file tree
Hide file tree
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 Aug 6, 2026
2602635
docs: add ADR to prevent JS conversion
DerekRoberts Aug 6, 2026
f99789c
fix(ops): inject bcgov/action-oc-runner step
DerekRoberts Aug 6, 2026
bc95a78
docs: update ADR to reference oc binary
DerekRoberts Aug 6, 2026
0dd7bfd
docs: add tls-route-provisioner to READMEs
DerekRoberts Aug 6, 2026
38dc316
feat(ops): add dry_run mode and PR validation workflow
DerekRoberts Aug 11, 2026
caa2f6c
fix(ops): address automated code review feedback
DerekRoberts Aug 11, 2026
6f4fa4f
fix(ops): default insecure_skip_tls_verify to true
DerekRoberts Aug 11, 2026
bbe5932
feat(ops): add lifecycle tracking labels to backup secrets
DerekRoberts Aug 11, 2026
a0482ba
docs(ops): document backup labels and termination policy
DerekRoberts Aug 11, 2026
20dcf03
feat(ops): run tls-route-provisioner locally from one script
DerekRoberts Aug 27, 2026
0aab8eb
fix(ci): set least-privilege GITHUB_TOKEN on PR workflow
DerekRoberts Sep 2, 2026
a44bb8a
fix(ci): default the PR workflow token to no permissions
DerekRoberts Sep 2, 2026
b0db283
fix(ops): stop using action-oc-runner to install oc
DerekRoberts Sep 2, 2026
82d1ad2
refactor: rename tls-route-provisioner to route-tls
DerekRoberts Sep 2, 2026
e4338ce
docs: lead with the Route TLS Action and prod GitHub secrets
DerekRoberts Sep 2, 2026
012a575
fix: drop app label; default route_name to <repo>-vanity-url
DerekRoberts Sep 3, 2026
e3e74c2
docs: map Entrust/Sectigo PEMs to TLS GitHub secrets
DerekRoberts Sep 3, 2026
6ec2df2
docs: use CAPS for GitHub secret and variable names
DerekRoberts Sep 3, 2026
8c7731c
docs: TLS_CA_CERTIFICATE is the Entrust issuing CA only
DerekRoberts Sep 3, 2026
b21afef
docs: drop consumer-specific examples from Route TLS
DerekRoberts Sep 3, 2026
76b64d8
docs: drop redundant Route app-label sentence
DerekRoberts Sep 3, 2026
2fa167a
fix: require the issuing CA on every Route TLS apply
DerekRoberts Sep 3, 2026
5a0615a
docs: stop calling the issuing CA optional
DerekRoberts Sep 3, 2026
f84ad72
fix: drop insecure_skip_tls_verify input
DerekRoberts Sep 3, 2026
0911ff4
fix: pin oc, verify the issuing CA, keep TLS backups complete
DerekRoberts Sep 3, 2026
abf1088
fix: install oc via action-oc-runner instead of a pinned tarball
DerekRoberts Sep 3, 2026
00c10ff
fix: reuse the oc-runner session and tighten PEM checks
DerekRoberts Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/workflows/pr-open.yml
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"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ web_modules/
.env.*
!.env.example

# Local route-tls dry-run output (contains the private key)
route-tls/route.yml

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ Consolidated repo for bcgov-specific openshift actions. Please feel free to cont
Cleans up Helm releases, labeled resources, and PVCs in a target OpenShift namespace on pull request close or merge.
* See [cleanup-pr/README.md](./cleanup-pr/README.md) for detailed inputs and usage.

### 2. Deployer (`.github/workflows/.deployer.yml`)
### 2. [Route TLS](./route-tls) (Composite Action)
GitHub Action that applies an OpenShift Route with a custom TLS certificate (openssl checks and archival backups).
* See [route-tls/README.md](./route-tls/README.md) for secrets, inputs, and usage.

### 3. Deployer (`.github/workflows/.deployer.yml`)
Comment thread
DerekRoberts marked this conversation as resolved.
A reusable workflow that manages Helm and Template-based application deployments to OpenShift. It configures variables, handles release/tag naming conventions, and automates target environment promotions.

#### Example Usage:
Expand All @@ -21,7 +25,7 @@ jobs:
packages: backend frontend migrations
```

### 3. SchemaSpy (`.github/workflows/.schema-spy.yml`)
### 4. SchemaSpy (`.github/workflows/.schema-spy.yml`)
A reusable workflow that spins up a Postgres/PostGIS service, runs migrations using Flyway, generates interactive database documentation with SchemaSpy, and automatically publishes the results to GitHub Pages.

#### Example Usage:
Expand Down
79 changes: 79 additions & 0 deletions route-tls/README.md
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`.
65 changes: 65 additions & 0 deletions route-tls/action.yml
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"
Loading
Loading