Skip to content

Merge pull request #46 from sethbergman/fix/nlb-client-ip-and-kms-dat… #153

Merge pull request #46 from sethbergman/fix/nlb-client-ip-and-kms-dat…

Merge pull request #46 from sethbergman/fix/nlb-client-ip-and-kms-dat… #153

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
jobs:
terraform:
name: Terraform fmt/validate/test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: hashicorp/setup-terraform@v4
with:
# terraform_wrapper wraps the binary in a script that mangles
# exit codes and output, which breaks `terraform test`.
terraform_wrapper: false
# Pinned for the same reason the integration job pins the Vault
# CLI: a floating version makes failures hard to attribute, and
# `fmt -check` in particular can start failing on formatting
# rules a new release introduced rather than on anything here.
#
# It also stops an upstream outage from reddening main. The
# action's default is `latest`, which resolves the version over
# the network on every run; that lookup returned a 504 on
# 2026-08-25 and failed the job before any Terraform ran.
#
# terraform/{aws,azure} declare required_version >= 1.7.
terraform_version: "1.15.9"
- name: terraform fmt
run: terraform fmt -check -recursive terraform/
- name: terraform validate (aws)
run: |
cd terraform/aws
terraform init -backend=false
terraform validate
- name: terraform validate (azure)
run: |
cd terraform/azure
terraform init -backend=false
terraform validate
- name: terraform test (aws)
run: |
# Runs against mocked providers, so no AWS credentials and
# nothing is created. These assert on configuration semantics
# that validate cannot see — quorum arithmetic, whether an
# "internal" load balancer really lands in private subnets,
# whether the health check keeps standby nodes in the pool.
cd terraform/aws
terraform test
- name: terraform test (azure)
run: |
# Same arrangement: mocked providers, no Azure credentials,
# nothing created. Catches things validate cannot — notably
# Key Vault's 24-character name limit, which constrains the
# value rather than the schema and so fails only at apply.
cd terraform/azure
terraform test
ansible-lint:
name: Ansible lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install ansible-lint
- run: ansible-lint ansible/
env:
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
# Lint and parse catch different things: ansible-lint is happy with
# a playbook that references a role variable no inventory defines,
# and --syntax-check is happy with style problems. Run both.
- name: Syntax-check the playbook
run: ansible-playbook --syntax-check -i inventory/local playbooks/site.yml
working-directory: ansible
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run shellcheck
run: |
sudo apt-get update && sudo apt-get install -y shellcheck
shellcheck scripts/*.sh
# Test harnesses and their shims are shell too, and a bug in
# them shows up as a *passing* test rather than a failing one.
#
# Discovered rather than listed: an enumerated list silently
# stops covering each new suite, which is the same failure mode
# these lint rules exist to catch.
mapfile -t harness < <(git ls-files 'tests/**/*.sh' 'tests/*/fake-bin/*')
printf 'Linting %d test files\n' "${#harness[@]}"
[ "${#harness[@]}" -gt 0 ] || { echo "No test scripts found — the glob is wrong"; exit 1; }
shellcheck -s bash "${harness[@]}"
# Windows checkouts do not track the executable bit, so a script
# committed from one lands as 100644 and CI fails with "Permission
# denied" rather than anything about the script. Cheaper to assert
# the invariant than to rediscover it each time.
- name: Every shell script must be executable
run: |
# Also covers tests/*/fake-bin/*, whose shims deliberately have
# no .sh suffix — they stand in for `vault`, `aws`, `curl` and
# must be named exactly that to be found on PATH.
non_exec="$(git ls-files -s '*.sh' 'tests/*/fake-bin/*' | grep -v '^100755' || true)"
if [[ -n "$non_exec" ]]; then
echo "These scripts are missing the executable bit:"
echo "$non_exec"
echo "Fix with: git update-index --chmod=+x <path>"
exit 1
fi
shell: bash
lint-invariants:
name: Shell invariants
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Properties shellcheck has no opinion about, each added because the
# pattern it rejects shipped here and was found by reading rather
# than by any test.
- name: Check shell invariants
run: ./tests/lint/run-tests.sh
markdownlint:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: DavidAnson/markdownlint-cli2-action@v24
with:
globs: "**/*.md"
docs-index:
name: Docs index
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# docs/README.md is generated from the H1 and H2 headings of every
# file in docs/. A hand-edit to it, a renamed section, or a new
# document nobody indexed all surface here as a diff, which is the
# only thing keeping an index true to what it indexes.
- name: Docs index is current
run: ./scripts/generate-docs-index.sh --check
# Staleness is not the only way the index can be wrong: a generator
# that misreads a heading produces a wrong description this job
# would then defend, since both halves of the diff come from it.
- name: Generator reads the documents correctly
run: ./tests/docs-index/run-tests.sh
smoke-test:
name: Deploy + smoke test (Docker Compose)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Bring up the local HA Vault cluster (Transit auto-unseal + monitoring)
run: |
ROOT_TOKEN=$(./scripts/bootstrap-dev-cluster.sh --with-monitoring)
echo "::add-mask::$ROOT_TOKEN"
echo "VAULT_ROOT_TOKEN=$ROOT_TOKEN" >> "$GITHUB_ENV"
- name: Wait for the cluster to be ready to serve writes
run: |
# Defence in depth. bootstrap-dev-cluster.sh already waits for
# the leader to become active, but every step below writes to
# Vault, and "unsealed" is not the same as "active" — writing in
# that window fails with "local node not active but active
# cluster node not found". Cheap to re-check here so a failure
# points at the cluster rather than at whichever write happened
# to go first.
for i in $(seq 1 30); do
if curl --cacert docker/dev/tls/ca.crt -fsS -o /dev/null https://127.0.0.1:8200/v1/sys/health; then
echo "Cluster is active and ready for writes."
exit 0
fi
echo "Waiting for an active node... ($i/30)"
sleep 2
done
echo "No active node after 60s. Seal status:"
curl --cacert docker/dev/tls/ca.crt -fsS https://127.0.0.1:8200/v1/sys/seal-status | jq . || true
echo "Leader status:"
curl --cacert docker/dev/tls/ca.crt -fsS https://127.0.0.1:8200/v1/sys/leader | jq . || true
exit 1
- name: Verify cluster health
run: |
STATUS=$(curl --cacert docker/dev/tls/ca.crt -fsS -o /dev/null -w '%{http_code}' https://127.0.0.1:8200/v1/sys/health)
echo "Health endpoint returned: $STATUS"
# 200 = initialized+unsealed+active, 429 = unsealed+standby,
# 472/473 = DR/perf standby — all indicate a working node.
case "$STATUS" in
200|429|472|473) echo "Cluster is healthy." ;;
*) echo "Unexpected health status: $STATUS"; exit 1 ;;
esac
- name: Smoke test — write and read a secret
run: |
cd docker/dev
docker compose exec -T -e VAULT_TOKEN="$VAULT_ROOT_TOKEN" vault-0 vault secrets enable -path=secret -version=2 kv
docker compose exec -T -e VAULT_TOKEN="$VAULT_ROOT_TOKEN" vault-0 vault kv put secret/ci-smoke-test value=ok
docker compose exec -T -e VAULT_TOKEN="$VAULT_ROOT_TOKEN" vault-0 vault kv get secret/ci-smoke-test
- name: Verify Vault metrics are queryable in Prometheus
run: |
# The bootstrap script already waited for the scrape targets to be
# up. This goes further and asserts a real Vault metric has landed
# with the right value — proving the telemetry stanza works, not
# just that the endpoint answers.
RESULT=$(curl -fsS 'http://127.0.0.1:9090/api/v1/query?query=vault_core_unsealed')
echo "$RESULT" | jq .
UNSEALED=$(echo "$RESULT" | jq '[.data.result[] | select(.value[1] == "1")] | length')
echo "Nodes reporting unsealed via Prometheus: $UNSEALED"
[ "$UNSEALED" -eq 3 ] || { echo "Expected 3 unsealed nodes in metrics, got $UNSEALED"; exit 1; }
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v
secret-rotation-test:
name: AppRole bootstrap + secret_id rotation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install vault CLI
run: |
VAULT_VERSION=1.17.2
curl -fsSL -o vault.zip "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip"
curl -fsSL -o SHA256SUMS "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_SHA256SUMS"
EXPECTED=$(grep "vault_${VAULT_VERSION}_linux_amd64.zip" SHA256SUMS | awk '{print $1}')
ACTUAL=$(sha256sum vault.zip | awk '{print $1}')
[ -n "$EXPECTED" ] && [ "$EXPECTED" = "$ACTUAL" ] || { echo "Checksum mismatch or missing entry"; exit 1; }
unzip -o -q vault.zip vault
sudo mv vault /usr/local/bin/vault
vault version
- name: Bring up a single Vault node (Transit auto-unseal)
run: |
ROOT_TOKEN=$(./scripts/bootstrap-dev-cluster.sh --nodes vault-0)
echo "::add-mask::$ROOT_TOKEN"
echo "VAULT_ADDR=https://127.0.0.1:8200" >> "$GITHUB_ENV"
# The CLI verifies against the dev CA rather than skipping
# verification, so a broken chain fails the job.
echo "VAULT_CACERT=$PWD/docker/dev/tls/ca.crt" >> "$GITHUB_ENV"
echo "VAULT_TOKEN=$ROOT_TOKEN" >> "$GITHUB_ENV"
- name: Enable KV v2 and seed a test secret
run: |
vault secrets enable -path=secret -version=2 kv
vault kv put secret/app/config greeting=hello
- name: Bootstrap AppRole role
run: |
./scripts/bootstrap-approle.sh \
--role app \
--policy-file examples/policies/app-readonly.hcl \
--secret-id-ttl 1h \
--token-ttl 15m \
--token-max-ttl 30m
- name: First rotation — issue and use a secret_id
run: |
ROLE_ID=$(vault read -field=role_id auth/approle/role/app/role-id)
echo "ROLE_ID=$ROLE_ID" >> "$GITHUB_ENV"
SECRET_ID_1=$(./scripts/rotate-secret-id.sh --role app 2>rotate1.log)
cat rotate1.log
echo "::add-mask::$SECRET_ID_1"
echo "SECRET_ID_1=$SECRET_ID_1" >> "$GITHUB_ENV"
CLIENT_TOKEN_1=$(vault write -field=token auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID_1")
echo "::add-mask::$CLIENT_TOKEN_1"
VAULT_TOKEN="$CLIENT_TOKEN_1" vault kv get secret/app/config
- name: Second rotation — old secret_id must be rejected, new one must work
run: |
SECRET_ID_2=$(./scripts/rotate-secret-id.sh --role app 2>rotate2.log)
cat rotate2.log
echo "::add-mask::$SECRET_ID_2"
echo "Confirming the first secret_id no longer authenticates..."
if vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID_1" >should_fail.log 2>&1; then
echo "ERROR: old secret_id still authenticated after rotation"
cat should_fail.log
exit 1
fi
echo "Old secret_id correctly rejected."
echo "Confirming the new secret_id authenticates and can read the seeded secret..."
CLIENT_TOKEN_2=$(vault write -field=token auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID_2")
echo "::add-mask::$CLIENT_TOKEN_2"
VAULT_TOKEN="$CLIENT_TOKEN_2" vault kv get secret/app/config
echo "Rotation verified end to end."
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v
jwt-github-oidc-test:
name: GitHub Actions OIDC login (JWT auth)
runs-on: ubuntu-latest
permissions:
contents: read
# Required to mint the OIDC token this job authenticates with. Note
# that pull requests from forks never get this, so a fork PR will not
# be able to run this job — that restriction is GitHub's, and it's
# also exactly why bound claims matter.
id-token: write
steps:
- uses: actions/checkout@v7
- name: Install vault CLI
run: |
VAULT_VERSION=1.17.2
curl -fsSL -o vault.zip "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip"
curl -fsSL -o SHA256SUMS "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_SHA256SUMS"
EXPECTED=$(grep "vault_${VAULT_VERSION}_linux_amd64.zip" SHA256SUMS | awk '{print $1}')
ACTUAL=$(sha256sum vault.zip | awk '{print $1}')
[ -n "$EXPECTED" ] && [ "$EXPECTED" = "$ACTUAL" ] || { echo "Checksum mismatch or missing entry"; exit 1; }
unzip -o -q vault.zip vault
sudo mv vault /usr/local/bin/vault
vault version
- name: Bring up a single Vault node (Transit auto-unseal)
run: |
ROOT_TOKEN=$(./scripts/bootstrap-dev-cluster.sh --nodes vault-0)
echo "::add-mask::$ROOT_TOKEN"
echo "VAULT_ADDR=https://127.0.0.1:8200" >> "$GITHUB_ENV"
# The CLI verifies against the dev CA rather than skipping
# verification, so a broken chain fails the job.
echo "VAULT_CACERT=$PWD/docker/dev/tls/ca.crt" >> "$GITHUB_ENV"
echo "VAULT_TOKEN=$ROOT_TOKEN" >> "$GITHUB_ENV"
- name: Enable KV v2 and seed a CI secret
run: |
vault secrets enable -path=secret -version=2 kv
vault kv put secret/ci/build greeting=hello-from-ci
- name: Bootstrap the JWT role for this repository
run: |
./scripts/bootstrap-jwt-github.sh \
--role github-ci \
--policy-file examples/policies/ci-readonly.hcl \
--repository "${{ github.repository }}" \
--audience vault \
--token-ttl 15m \
--token-max-ttl 30m
- name: Mint a real GitHub OIDC token
run: |
# GitHub mints this fresh for this job. It is never stored
# anywhere — that is the whole point compared to AppRole.
JWT=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=vault" | jq -r '.value')
[ -n "$JWT" ] && [ "$JWT" != "null" ] || { echo "Failed to mint an OIDC token"; exit 1; }
echo "::add-mask::$JWT"
echo "GH_JWT=$JWT" >> "$GITHUB_ENV"
# Claims are not secret (the signature is what matters), so show
# what Vault will be matching against — invaluable when a bound
# claim doesn't match and login fails.
echo "Token claims:"
echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{iss, aud, repository, ref, sub}' || true
- name: Log in to Vault with the OIDC token and read the secret
run: |
CI_TOKEN=$(vault write -field=token auth/jwt/login role=github-ci jwt="$GH_JWT")
echo "::add-mask::$CI_TOKEN"
echo "Logged in via GitHub OIDC — no stored credential involved."
VAULT_TOKEN="$CI_TOKEN" vault kv get secret/ci/build
- name: A token from another repository must be rejected
run: |
# The signature on our token is valid GitHub-issued, so without
# bound claims this role would accept it. Binding to a different
# repository is what must make it fail — this proves the claim
# check is actually enforced rather than just configured.
./scripts/bootstrap-jwt-github.sh \
--role github-ci-otherrepo \
--policy-file examples/policies/ci-readonly.hcl \
--repository "some-other-org/some-other-repo" \
--audience vault
if vault write auth/jwt/login role=github-ci-otherrepo jwt="$GH_JWT" >should_fail.log 2>&1; then
echo "ERROR: token authenticated against a role bound to a different repository"
cat should_fail.log
exit 1
fi
echo "Correctly rejected — bound claims are enforced:"
grep -i "claim\|denied\|error" should_fail.log | head -5 || true
- name: The read-only policy must not permit writes
run: |
CI_TOKEN=$(vault write -field=token auth/jwt/login role=github-ci jwt="$GH_JWT")
echo "::add-mask::$CI_TOKEN"
if VAULT_TOKEN="$CI_TOKEN" vault kv put secret/ci/build greeting=tampered >should_fail.log 2>&1; then
echo "ERROR: ci-readonly policy allowed a write"
cat should_fail.log
exit 1
fi
echo "Write correctly denied by the read-only policy."
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v
human-oidc-test:
name: Human OIDC login (Dex)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install vault CLI
run: |
VAULT_VERSION=1.17.2
curl -fsSL -o vault.zip "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip"
curl -fsSL -o SHA256SUMS "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_SHA256SUMS"
EXPECTED=$(grep "vault_${VAULT_VERSION}_linux_amd64.zip" SHA256SUMS | awk '{print $1}')
ACTUAL=$(sha256sum vault.zip | awk '{print $1}')
[ -n "$EXPECTED" ] && [ "$EXPECTED" = "$ACTUAL" ] || { echo "Checksum mismatch or missing entry"; exit 1; }
unzip -o -q vault.zip vault
sudo mv vault /usr/local/bin/vault
vault version
- name: Make "dex" resolve on the host too
run: |
# The OIDC issuer has to be one URL that both Vault (inside the
# compose network) and the browser (out here) can reach, or the
# issuer check fails. Compose DNS handles the container side;
# this handles the host side, pointing at the published port.
echo "127.0.0.1 dex" | sudo tee -a /etc/hosts
getent hosts dex
- name: Bring up Vault + Dex
run: |
ROOT_TOKEN=$(./scripts/bootstrap-dev-cluster.sh --nodes vault-0 --with-oidc)
echo "::add-mask::$ROOT_TOKEN"
echo "VAULT_ADDR=https://127.0.0.1:8200" >> "$GITHUB_ENV"
# The CLI verifies against the dev CA rather than skipping
# verification, so a broken chain fails the job.
echo "VAULT_CACERT=$PWD/docker/dev/tls/ca.crt" >> "$GITHUB_ENV"
echo "VAULT_TOKEN=$ROOT_TOKEN" >> "$GITHUB_ENV"
- name: Seed secrets the developer policy grants access to
run: |
vault secrets enable -path=secret -version=2 kv
vault kv put secret/app/config greeting=hello-developer
- name: Configure OIDC auth and map IdP groups to policies
run: |
./scripts/bootstrap-oidc.sh \
--discovery-url http://dex:5556/dex \
--client-id vault \
--client-secret vault-dev-client-secret \
--group vault-developers:examples/policies/developer.hcl \
--group vault-operators:examples/policies/operator.hcl
- name: Log in as a developer through the real OIDC flow
run: |
TOKEN=$(./scripts/oidc-login-test.sh \
--username developer@example.com \
--password password)
echo "::add-mask::$TOKEN"
echo "DEV_TOKEN=$TOKEN" >> "$GITHUB_ENV"
echo "Token identity and policies:"
# identity_policies is the one that matters here: policies that
# come from an identity group land there, not in policies.
VAULT_TOKEN="$TOKEN" vault token lookup -format=json \
| jq '.data | {display_name, entity_id, policies, identity_policies}'
- name: Group membership must have produced the developer policy
run: |
# This is the assertion that matters. Login succeeding only
# proves the IdP authenticated someone; it says nothing about
# authorization. If the group alias were missing or misspelled,
# login would still succeed and the token would carry only
# "default" — which is exactly the failure this catches.
LOOKUP=$(VAULT_TOKEN="$DEV_TOKEN" vault token lookup -format=json)
echo "$LOOKUP" | jq '.data | {policies, identity_policies, entity_id}'
# Policies granted through an identity group appear under
# identity_policies, not policies — check both so this asserts
# the thing it claims to.
POLICIES=$(echo "$LOOKUP" | jq -r \
'[(.data.policies // [])[], (.data.identity_policies // [])[]] | join(",")')
echo "Effective policies: $POLICIES"
echo "$POLICIES" | grep -q "vault-developers" || {
echo "ERROR: group claim did not map to the vault-developers policy"
exit 1
}
echo "IdP group mapped to a Vault policy correctly."
- name: Developer can read app secrets
run: |
VAULT_TOKEN="$DEV_TOKEN" vault kv get secret/app/config
- name: Developer must NOT have operator privileges
run: |
# Proves the policies are actually distinct rather than everyone
# collapsing to the same access.
if VAULT_TOKEN="$DEV_TOKEN" vault operator raft snapshot save /tmp/x.snap >should_fail.log 2>&1; then
echo "ERROR: developer policy allowed a raft snapshot"
cat should_fail.log
exit 1
fi
echo "Correctly denied — developers don't get operator access."
- name: An operator logs in and gets a different policy set
run: |
OP_TOKEN=$(./scripts/oidc-login-test.sh \
--username operator@example.com \
--password password)
echo "::add-mask::$OP_TOKEN"
POLICIES=$(VAULT_TOKEN="$OP_TOKEN" vault token lookup -format=json \
| jq -r '[(.data.policies // [])[], (.data.identity_policies // [])[]] | join(",")')
echo "Operator effective policies: $POLICIES"
echo "$POLICIES" | grep -q "vault-operators" || {
echo "ERROR: operator did not receive the vault-operators policy"
exit 1
}
# Same IdP, same Vault, different group — and therefore the
# mirror image of the developer's access.
if VAULT_TOKEN="$OP_TOKEN" vault kv get secret/app/config >should_fail.log 2>&1; then
echo "ERROR: operator policy allowed reading application secrets"
cat should_fail.log
exit 1
fi
echo "Operator correctly cannot read application secrets."
- name: Bad credentials must be rejected
run: |
if ./scripts/oidc-login-test.sh \
--username developer@example.com \
--password wrong-password >should_fail.log 2>&1; then
echo "ERROR: login succeeded with the wrong password"
exit 1
fi
echo "Wrong password correctly rejected."
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v
dr-drill-test:
name: DR restore drill
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run the disaster recovery drill
run: ./scripts/dr-drill.sh
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v
upgrade-test:
name: Rolling upgrade tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/vault-upgrade.sh
run: ./tests/upgrade/run-tests.sh
ansible-handoff-test:
name: Terraform to Ansible handoff tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
# jinja2 renders the role template the way Ansible would, so the
# tests exercise the real template rather than a copy of it.
- name: Install rendering dependencies
run: pip install jinja2 pyyaml
- name: Exercise the Terraform to Ansible handoff
run: ./tests/ansible/run-tests.sh
snapshot-test:
name: Scheduled snapshot tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/snapshot.sh
run: ./tests/snapshot/run-tests.sh
pki-test:
name: Vault PKI node certificate tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/bootstrap-pki.sh and scripts/issue-node-cert.sh
run: ./tests/pki/run-tests.sh
integration-test:
name: Integration (real cluster)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The scripts run as a host process against the cluster's published
# ports, the way they would on a real node — so the runner needs the
# CLI. Pinned to the version the container image uses, because
# `snapshot inspect` and the PKI endpoints are version-sensitive and
# a floating CLI would make failures hard to attribute.
- name: Install the Vault CLI
run: |
VAULT_VERSION=1.17.2
curl -fsSL -o /tmp/vault.zip "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip"
unzip -o -d /tmp /tmp/vault.zip vault
sudo install -m 0755 /tmp/vault /usr/local/bin/vault
vault version
- name: Run the operational scripts against a real cluster
run: ./tests/integration/run-tests.sh
- name: Tear down
if: always()
run: |
cd docker/dev
docker compose down -v || true
database-test:
name: Dynamic database credentials tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/bootstrap-database-secrets.sh
run: ./tests/database/run-tests.sh
alert-routing-test:
name: Alert routing tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install PyYAML
run: pip install --quiet pyyaml
# The amtool cases run `amtool` out of the pinned Alertmanager
# image, so which route matches a label set is answered by
# Alertmanager rather than by a reimplementation of its matching
# rules in the test. Runners already have Docker; the suite skips
# those cases anywhere it is missing.
- name: Exercise the Alertmanager routing tree
run: ./tests/alert-routing/run-tests.sh
alerting-test:
name: Alerting rule tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# promtool ships with Prometheus. Pinned to the same version the
# compose profile runs, so a rule that passes here is one that
# version will actually load.
- name: Install promtool
run: |
PROM_VERSION=2.54.1
curl -fsSL -o /tmp/prom.tar.gz "https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz"
tar -xzf /tmp/prom.tar.gz -C /tmp
sudo install -m 0755 "/tmp/prometheus-${PROM_VERSION}.linux-amd64/promtool" /usr/local/bin/promtool
promtool --version
- name: Exercise the alerting rules
run: ./tests/alerting/run-tests.sh
audit-test:
name: Audit device tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/bootstrap-audit.sh
run: ./tests/audit/run-tests.sh
audit-chain-test:
name: Audit chain and anchor tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise the collector's chaining and its verifier
run: ./tests/audit-chain/run-tests.sh
pki-migration-test:
name: PKI migration driver tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/migrate-to-vault-pki.sh
run: ./tests/pki-migration/run-tests.sh
agent-test:
name: Vault Agent bootstrap tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Exercise scripts/bootstrap-agent.sh
run: ./tests/agent/run-tests.sh
cloud-preflight-test:
name: Cloud pre-flight and teardown tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# These cover the decisions the scripts make — whether a missing
# key pair fails or warns, whether teardown empties the bucket
# before calling destroy, whether the paging loop terminates.
#
# They cannot cover whether AWS agrees; nothing here has ever been
# applied. docs/cloud-apply.md lists what a human has to observe
# once, against a real account, for that gap to close.
- name: Exercise scripts/preflight-cloud.sh and teardown-cloud.sh
run: ./tests/cloud-preflight/run-tests.sh
security-scan:
name: Security scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# gitleaks needs history to scan past commits, not just the tip.
# A secret committed and then removed is still a leaked secret.
fetch-depth: 0
- name: Scan for committed secrets
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Scan Terraform and Dockerfiles for misconfigurations
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: config
scan-ref: .
trivy-config: ""
ignore-unfixed: false
# Fail on HIGH and above. Accepted findings live in
# .trivyignore.yaml, each with the reason it is accepted — a
# suppression with no justification is indistinguishable from
# never having run the scanner.
severity: HIGH,CRITICAL
exit-code: "1"
trivyignores: .trivyignore.yaml