diff --git a/.github/workflows/security-utils-cert-validity.yaml b/.github/workflows/security-utils-cert-validity.yaml new file mode 100644 index 000000000..a816ede73 --- /dev/null +++ b/.github/workflows/security-utils-cert-validity.yaml @@ -0,0 +1,112 @@ +name: Check pinned CA validity + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/security-utils-cert-validity.yaml' + - 'security-utils/certs/*.pem' + +permissions: + contents: read + issues: write + +jobs: + check-pinned-cas: + name: Check pinned CA validity + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check pinned CA certificates + id: cert_check + run: | + set -euo pipefail + + threshold_seconds=$((60 * 24 * 60 * 60)) + expiring_certs=() + + for cert in security-utils/certs/*.pem; do + echo "Checking certificate validity for ${cert}" + + if ! openssl x509 -in "${cert}" -noout >/dev/null; then + echo "::error file=${cert}::Invalid certificate format" + exit 1 + fi + + expiration="$(openssl x509 -in "${cert}" -noout -enddate | sed 's/notAfter=//')" + echo "${cert} notAfter=${expiration}" + + if ! openssl x509 -in "${cert}" -checkend 0 -noout >/dev/null; then + echo "::error file=${cert}::Certificate is expired (notAfter=${expiration})" + exit 1 + fi + + if ! openssl x509 -in "${cert}" -checkend "${threshold_seconds}" -noout >/dev/null; then + echo "::error file=${cert}::Certificate expires in less than 60 days (notAfter=${expiration})" + expiring_certs+=("${cert} (notAfter=${expiration})") + fi + done + + if ((${#expiring_certs[@]} > 0)); then + { + echo 'expiring_certs<>"$GITHUB_OUTPUT" + + exit 1 + fi + + - name: Notify maintainers via GitHub issue + if: failure() && steps.cert_check.outputs.expiring_certs != '' + uses: actions/github-script@v7 + with: + script: | + const title = 'security-utils pinned CA certificates expire in less than 60 days'; + const body = [ + 'The nightly pinned CA validity workflow detected certificates expiring within 60 days.', + '', + 'Please rotate affected certificates in `security-utils/certs`.', + '', + 'Affected certificates:', + '```', + `${{ toJSON(steps.cert_check.outputs.expiring_certs) }}`, + '```', + '', + `Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + creator: 'github-actions[bot]', + per_page: 100, + }); + + const existing = issues.find((issue) => issue.title === title); + + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + core.info(`Updated existing issue #${existing.number}`); + return; + } + + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['security'], + }); + + core.info(`Created issue #${issue.number}`);