diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c6d05b9..9996aa9 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -18,24 +18,46 @@ jobs: timeout-minutes: 10 environment: release-please permissions: + id-token: write actions: write contents: write pull-requests: write env: - HAS_APP: ${{ secrets.RELEASE_PLEASE_CLIENT_ID != '' && secrets.RELEASE_PLEASE_PRIVATE_KEY != '' }} + HAS_APP: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID != '' }} steps: - name: Require GitHub App configuration for manual runs if: ${{ github.event_name == 'workflow_dispatch' && env.HAS_APP != 'true' }} run: | echo "::error::release-please GitHub App credentials are not configured" exit 1 + - name: Fetch automation credentials + id: doppler_automation_1 + if: "env.HAS_APP == 'true'" + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }} + doppler-project: snaply + doppler-config: automation + inject-env-vars: false + - name: Reject missing or unresolved automation credentials + if: >- + ${{ steps.doppler_automation_1.outcome == 'success' && ( + steps.doppler_automation_1.outputs.GITHUB_APP_CLIENT_ID == '' || + startsWith(steps.doppler_automation_1.outputs.GITHUB_APP_CLIENT_ID, '${') || + steps.doppler_automation_1.outputs.GITHUB_APP_PRIVATE_KEY == '' || + startsWith(steps.doppler_automation_1.outputs.GITHUB_APP_PRIVATE_KEY, '${') + ) }} + shell: bash + run: exit 1 + - name: Mint GitHub App token id: app-token if: env.HAS_APP == 'true' uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - client-id: ${{ secrets.RELEASE_PLEASE_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }} + client-id: ${{ steps.doppler_automation_1.outputs.GITHUB_APP_CLIENT_ID }} + private-key: ${{ steps.doppler_automation_1.outputs.GITHUB_APP_PRIVATE_KEY }} - name: Update release pull request id: release if: env.HAS_APP == 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 967b9c2..03ef7bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -127,9 +127,10 @@ jobs: timeout-minutes: 25 environment: release permissions: + id-token: write contents: read env: - HAVE_SIGNING: ${{ secrets.ES_USERNAME != '' && secrets.ES_PASSWORD != '' && secrets.CREDENTIAL_ID != '' && secrets.ES_TOTP_SECRET != '' }} + HAVE_SIGNING: ${{ vars.DOPPLER_SIGN_WINDOWS_IDENTITY_ID != '' }} SIGNER_SUBJECT_CONTAINS: ${{ vars.SIGNER_SUBJECT_CONTAINS }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -154,26 +155,74 @@ jobs: if ([string]::IsNullOrWhiteSpace($env:SIGNER_SUBJECT_CONTAINS)) { throw "SIGNER_SUBJECT_CONTAINS is required." } + - name: Fetch sign_windows credentials + id: doppler_sign_windows_5 + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_SIGN_WINDOWS_IDENTITY_ID }} + doppler-project: snaply + doppler-config: sign_windows + inject-env-vars: false + - name: Reject missing or unresolved sign_windows credentials + if: >- + ${{ steps.doppler_sign_windows_5.outcome == 'success' && ( + steps.doppler_sign_windows_5.outputs.SSLDOTCOM_CREDENTIAL_ID == '' || + startsWith(steps.doppler_sign_windows_5.outputs.SSLDOTCOM_CREDENTIAL_ID, '${') || + steps.doppler_sign_windows_5.outputs.SSLDOTCOM_PASSWORD == '' || + startsWith(steps.doppler_sign_windows_5.outputs.SSLDOTCOM_PASSWORD, '${') || + steps.doppler_sign_windows_5.outputs.SSLDOTCOM_TOTP_SECRET == '' || + startsWith(steps.doppler_sign_windows_5.outputs.SSLDOTCOM_TOTP_SECRET, '${') || + steps.doppler_sign_windows_5.outputs.SSLDOTCOM_USERNAME == '' || + startsWith(steps.doppler_sign_windows_5.outputs.SSLDOTCOM_USERNAME, '${') + ) }} + shell: bash + run: exit 1 + - name: Sign portable first-party binaries uses: SSLcom/esigner-codesign@cf5f6c1d38ad10f47e3ed9aca873f429b1a8d85b # v1.3.2 with: command: batch_sign - username: ${{ secrets.ES_USERNAME }} - password: ${{ secrets.ES_PASSWORD }} - credential_id: ${{ secrets.CREDENTIAL_ID }} - totp_secret: ${{ secrets.ES_TOTP_SECRET }} + username: ${{ steps.doppler_sign_windows_5.outputs.SSLDOTCOM_USERNAME }} + password: ${{ steps.doppler_sign_windows_5.outputs.SSLDOTCOM_PASSWORD }} + credential_id: ${{ steps.doppler_sign_windows_5.outputs.SSLDOTCOM_CREDENTIAL_ID }} + totp_secret: ${{ steps.doppler_sign_windows_5.outputs.SSLDOTCOM_TOTP_SECRET }} dir_path: ${{ github.workspace }}\build\release\sign-stage\portable output_path: ${{ github.workspace }}\build\release\signed\portable environment_name: PROD malware_block: "true" + - name: Fetch sign_windows credentials + id: doppler_sign_windows_6 + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_SIGN_WINDOWS_IDENTITY_ID }} + doppler-project: snaply + doppler-config: sign_windows + inject-env-vars: false + - name: Reject missing or unresolved sign_windows credentials + if: >- + ${{ steps.doppler_sign_windows_6.outcome == 'success' && ( + steps.doppler_sign_windows_6.outputs.SSLDOTCOM_CREDENTIAL_ID == '' || + startsWith(steps.doppler_sign_windows_6.outputs.SSLDOTCOM_CREDENTIAL_ID, '${') || + steps.doppler_sign_windows_6.outputs.SSLDOTCOM_PASSWORD == '' || + startsWith(steps.doppler_sign_windows_6.outputs.SSLDOTCOM_PASSWORD, '${') || + steps.doppler_sign_windows_6.outputs.SSLDOTCOM_TOTP_SECRET == '' || + startsWith(steps.doppler_sign_windows_6.outputs.SSLDOTCOM_TOTP_SECRET, '${') || + steps.doppler_sign_windows_6.outputs.SSLDOTCOM_USERNAME == '' || + startsWith(steps.doppler_sign_windows_6.outputs.SSLDOTCOM_USERNAME, '${') + ) }} + shell: bash + run: exit 1 + - name: Sign MSIX bundle uses: SSLcom/esigner-codesign@cf5f6c1d38ad10f47e3ed9aca873f429b1a8d85b # v1.3.2 with: command: batch_sign - username: ${{ secrets.ES_USERNAME }} - password: ${{ secrets.ES_PASSWORD }} - credential_id: ${{ secrets.CREDENTIAL_ID }} - totp_secret: ${{ secrets.ES_TOTP_SECRET }} + username: ${{ steps.doppler_sign_windows_6.outputs.SSLDOTCOM_USERNAME }} + password: ${{ steps.doppler_sign_windows_6.outputs.SSLDOTCOM_PASSWORD }} + credential_id: ${{ steps.doppler_sign_windows_6.outputs.SSLDOTCOM_CREDENTIAL_ID }} + totp_secret: ${{ steps.doppler_sign_windows_6.outputs.SSLDOTCOM_TOTP_SECRET }} dir_path: ${{ github.workspace }}\build\release\sign-stage\msix output_path: ${{ github.workspace }}\build\release\signed\msix environment_name: PROD diff --git a/.github/workflows/verify-doppler.yml b/.github/workflows/verify-doppler.yml new file mode 100644 index 0000000..e417175 --- /dev/null +++ b/.github/workflows/verify-doppler.yml @@ -0,0 +1,115 @@ +--- +name: Verify credential access + +'on': + workflow_dispatch: + +permissions: {} + +concurrency: + group: verify-credentials + cancel-in-progress: false + +jobs: + sign_windows_0: + name: Verify sign_windows access + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + environment: release + permissions: + id-token: write + steps: + - name: Fetch sign_windows credentials + id: credentials + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_SIGN_WINDOWS_IDENTITY_ID }} + doppler-project: snaply + doppler-config: sign_windows + inject-env-vars: false + - name: Reject missing or unresolved sign_windows credentials + if: >- + ${{ steps.credentials.outcome == 'success' && ( + steps.credentials.outputs.SSLDOTCOM_CREDENTIAL_ID == '' || + startsWith(steps.credentials.outputs.SSLDOTCOM_CREDENTIAL_ID, '${') || + steps.credentials.outputs.SSLDOTCOM_PASSWORD == '' || + startsWith(steps.credentials.outputs.SSLDOTCOM_PASSWORD, '${') || + steps.credentials.outputs.SSLDOTCOM_TOTP_SECRET == '' || + startsWith(steps.credentials.outputs.SSLDOTCOM_TOTP_SECRET, '${') || + steps.credentials.outputs.SSLDOTCOM_USERNAME == '' || + startsWith(steps.credentials.outputs.SSLDOTCOM_USERNAME, '${') + ) }} + shell: bash + run: exit 1 + + - name: Attempt unrelated credential access + id: forbidden + continue-on-error: true + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_SIGN_WINDOWS_IDENTITY_ID }} + doppler-project: snaply + doppler-config: automation + inject-env-vars: false + + - name: Reject unrelated credential access + if: ${{ always() && steps.forbidden.outcome != 'failure' }} + shell: bash + run: exit 1 + + automation_0: + name: Verify automation access + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + environment: release-please + permissions: + id-token: write + steps: + - name: Fetch automation credentials + id: credentials + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }} + doppler-project: snaply + doppler-config: automation + inject-env-vars: false + - name: Reject missing or unresolved automation credentials + if: >- + ${{ steps.credentials.outcome == 'success' && ( + steps.credentials.outputs.GITHUB_APP_CLIENT_ID == '' || + startsWith(steps.credentials.outputs.GITHUB_APP_CLIENT_ID, '${') || + steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY == '' || + startsWith(steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY, '${') + ) }} + shell: bash + run: exit 1 + + - name: Verify a read-only repository token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 + with: + client-id: ${{ steps.credentials.outputs.GITHUB_APP_CLIENT_ID }} + private-key: ${{ steps.credentials.outputs.GITHUB_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: read + + - name: Attempt unrelated credential access + id: forbidden + continue-on-error: true + uses: dopplerhq/secrets-fetch-action@451892f16195f9ac360e1a5bcbf0b5fd0e957534 + with: + auth-method: oidc + doppler-identity-id: ${{ vars.DOPPLER_AUTOMATION_IDENTITY_ID }} + doppler-project: snaply + doppler-config: sign_windows + inject-env-vars: false + + - name: Reject unrelated credential access + if: ${{ always() && steps.forbidden.outcome != 'failure' }} + shell: bash + run: exit 1 diff --git a/docs/credential-delivery.md b/docs/credential-delivery.md new file mode 100644 index 0000000..113f55a --- /dev/null +++ b/docs/credential-delivery.md @@ -0,0 +1,33 @@ +--- +doppler_automation_app: release_please +--- + +# Credential delivery + +GitHub Actions exchanges its workload identity for a short-lived Doppler token at the credential-consuming step. +Each purpose has its own read-only service account, config, and environment variable. +Shared signing credentials stay in the canonical signing project and are referenced from the consumer config. +The shared credential still carries the signing provider's authority after retrieval. + +| Purpose | Doppler config | GitHub environment variable | Environments | +| --- | --- | --- | --- | +| sign_windows | `snaply.sign_windows` | `DOPPLER_SIGN_WINDOWS_IDENTITY_ID` | release | +| automation | `snaply.automation` | `DOPPLER_AUTOMATION_IDENTITY_ID` | release-please | + +The manual **Verify credential access** workflow checks required fields and rejects access to an unrelated config. +Automation also creates a repository-scoped App token with contents read access. +This workflow performs no signing, notarization, registry publication, release creation, or deployment. +Existing environment protections remain in force; their approval does not grant authority for another operation. + +After the reviewed workflows reach the default branch, run the credential-access check for that exact revision. +Keep the current credential delivery until every purpose and environment succeeds and the denial checks pass. +Confirm an explicit access-denied response for each negative check. +Keep the selected identity fixed for that scope check. +A workflow can exchange for every identity whose claims it satisfies; separate accounts alone don't isolate stages with matching claim policies. +Network, authentication, and runner failures leave that evidence inconclusive. +Then disconnect the corresponding Doppler-to-GitHub sync while preserving its target fields, check for remaining consumers, and remove the obsolete GitHub fields and legacy configs. +An expired or revoked registry key is a retirement candidate and must not be copied into a replacement config. +Use the registry's native trusted publisher where supported; an unconfigured publisher stays unavailable until its owner configures the exact binding. + +Store only field names, identity metadata, source revisions, and verification outcomes in issues and logs. +Keep credential values and native authentication state out of the repository.