Skip to content

Commit 6dd9c34

Browse files
PREQ-3880 Introduce JS credential guard to resolve AWS authentication issues
1 parent 9d7be1f commit 6dd9c34

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+113479
-217
lines changed

.github/workflows/test-action.yml

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,174 @@ jobs:
120120
run: go mod download
121121
- name: Build
122122
run: go build -o hello main.go
123+
124+
test-s3-cache-with-credential-interference:
125+
runs-on: github-ubuntu-latest-s
126+
permissions:
127+
id-token: write
128+
contents: read
129+
steps:
130+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
131+
- uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4
132+
with:
133+
version: 2025.7.12
134+
135+
# Step 1: Use our cache action (should restore and later save)
136+
- name: Cache with S3
137+
id: cache-test
138+
uses: ./
139+
with:
140+
path: ~/.cache/pip
141+
key: interference-test-${{ runner.os }}-${{ github.run_id }}
142+
restore-keys: interference-test-${{ runner.os }}-
143+
environment: dev
144+
backend: s3
145+
146+
# Step 2: Simulate user overwriting AWS credentials
147+
# This is the scenario that caused production failures
148+
- name: Overwrite AWS credentials (simulating user workflow)
149+
run: |
150+
echo "AWS_ACCESS_KEY_ID=FAKE_KEY_TO_OVERRIDE" >> "$GITHUB_ENV"
151+
echo "AWS_SECRET_ACCESS_KEY=FAKE_SECRET_TO_OVERRIDE" >> "$GITHUB_ENV"
152+
echo "AWS_SESSION_TOKEN=FAKE_TOKEN_TO_OVERRIDE" >> "$GITHUB_ENV"
153+
echo "Simulated credential override via GITHUB_ENV"
154+
155+
# Step 3: Create something to cache
156+
- name: Install dependencies
157+
run: |
158+
python -m pip install --upgrade pip
159+
pip install pytest requests
160+
161+
# Post-step: credential-guard restores real creds, then runs-on/cache saves
162+
# If this job succeeds, the credential guard is working correctly
163+
164+
# Reproduces: "Unable to parse config file C:\Users\runneradmin/.aws/config"
165+
# https://github.com/SonarSource/peachee-cfamily/actions/runs/21646222381/job/62398839588#step:26:263
166+
test-s3-cache-windows:
167+
runs-on: github-windows-latest-s
168+
permissions:
169+
id-token: write
170+
contents: read
171+
steps:
172+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
173+
174+
- name: Cache with S3 on Windows
175+
id: cache-test
176+
uses: ./
177+
with:
178+
path: ~\AppData\Local\pip\Cache
179+
key: windows-test-${{ runner.os }}-${{ github.run_id }}
180+
restore-keys: windows-test-${{ runner.os }}-
181+
environment: dev
182+
backend: s3
183+
184+
- name: Create something to cache
185+
run: |
186+
python -m pip install --upgrade pip
187+
pip install requests
188+
189+
# Reproduces: ~/.aws/config corruption from multiple credential_process entries
190+
test-s3-cache-multiple-invocations:
191+
runs-on: github-ubuntu-latest-s
192+
permissions:
193+
id-token: write
194+
contents: read
195+
steps:
196+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
197+
- uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4
198+
with:
199+
version: 2025.7.12
200+
201+
# First cache invocation
202+
- name: Cache pip dependencies
203+
id: cache-pip
204+
uses: ./
205+
with:
206+
path: ~/.cache/pip
207+
key: multi-pip-${{ runner.os }}-${{ github.run_id }}
208+
restore-keys: multi-pip-${{ runner.os }}-
209+
environment: dev
210+
backend: s3
211+
212+
# Second cache invocation in same job
213+
# Old approach would append duplicate profile to ~/.aws/config
214+
- name: Cache npm dependencies
215+
id: cache-npm
216+
uses: ./
217+
with:
218+
path: ~/.npm
219+
key: multi-npm-${{ runner.os }}-${{ github.run_id }}
220+
restore-keys: multi-npm-${{ runner.os }}-
221+
environment: dev
222+
backend: s3
223+
224+
- name: Create something to cache
225+
run: |
226+
python -m pip install --upgrade pip
227+
pip install pytest
228+
npm init -y
229+
230+
# Reproduces: pre-existing AWS config from configure-aws-credentials
231+
# https://github.com/SonarSource/sonarsource-iam/actions/runs/21951781857/job/63404298650#step:5:9
232+
test-s3-cache-with-preset-aws-config:
233+
runs-on: github-ubuntu-latest-s
234+
permissions:
235+
id-token: write
236+
contents: read
237+
steps:
238+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
239+
- uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4
240+
with:
241+
version: 2025.7.12
242+
243+
# Simulate pre-existing AWS config (as if configure-aws-credentials ran before)
244+
- name: Create pre-existing AWS config
245+
run: |
246+
mkdir -p ~/.aws
247+
cat <<'AWSCONFIG' > ~/.aws/config
248+
[default]
249+
region = us-east-1
250+
output = json
251+
[profile some-other-profile]
252+
region = us-west-2
253+
AWSCONFIG
254+
cat <<'AWSCREDS' > ~/.aws/credentials
255+
[default]
256+
aws_access_key_id = AKIAFAKEDEFAULT
257+
aws_secret_access_key = fakesecretdefault
258+
[some-other-profile]
259+
aws_access_key_id = AKIAFAKEOTHER
260+
aws_secret_access_key = fakesecretother
261+
AWSCREDS
262+
echo "Pre-existing AWS config created"
263+
cat ~/.aws/config
264+
265+
- name: Set conflicting AWS env vars
266+
run: |
267+
echo "AWS_ACCESS_KEY_ID=AKIAFAKEENV" >> "$GITHUB_ENV"
268+
echo "AWS_SECRET_ACCESS_KEY=fakesecretenv" >> "$GITHUB_ENV"
269+
echo "AWS_SESSION_TOKEN=faketokenenv" >> "$GITHUB_ENV"
270+
echo "AWS_PROFILE=some-other-profile" >> "$GITHUB_ENV"
271+
echo "AWS_DEFAULT_PROFILE=some-other-profile" >> "$GITHUB_ENV"
272+
273+
# Cache action should override the conflicting credentials
274+
- name: Cache with S3
275+
id: cache-test
276+
uses: ./
277+
with:
278+
path: ~/.cache/pip
279+
key: preset-aws-${{ runner.os }}-${{ github.run_id }}
280+
restore-keys: preset-aws-${{ runner.os }}-
281+
environment: dev
282+
backend: s3
283+
284+
- name: Re-override with fake credentials (simulating mid-job auth change)
285+
run: |
286+
echo "AWS_ACCESS_KEY_ID=AKIAFAKEOVERRIDE" >> "$GITHUB_ENV"
287+
echo "AWS_SECRET_ACCESS_KEY=fakesecretoverride" >> "$GITHUB_ENV"
288+
echo "AWS_SESSION_TOKEN=faketokenoverride" >> "$GITHUB_ENV"
289+
290+
- name: Create something to cache
291+
run: |
292+
python -m pip install --upgrade pip
293+
pip install pytest requests
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: Test Credential Isolation
3+
run-name: Test credential isolation (${{ github.head_ref || github.ref_name }})
4+
5+
on:
6+
push:
7+
branches: [master]
8+
pull_request:
9+
workflow_dispatch:
10+
11+
jobs:
12+
credential-isolation-tests:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
id-token: write
16+
contents: read
17+
steps:
18+
- name: Vault
19+
id: secrets
20+
uses: SonarSource/vault-action-wrapper@545e7cfbb5528e7009a1edcc83e073898d292627 # 3.2.0
21+
with:
22+
secrets: |
23+
development/github/token/{REPO_OWNER_NAME_DASH}-sonar-dummy-workflows token | GITHUB_TOKEN;
24+
25+
- name: Trigger and wait for credential isolation tests
26+
uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5
27+
with:
28+
owner: SonarSource
29+
repo: sonar-dummy
30+
github_token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }}
31+
ref: master
32+
workflow_file_name: test-cache-isolation.yaml
33+
client_payload: |
34+
{
35+
"cache_action_ref": "${{ github.head_ref || github.ref_name }}"
36+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
.claude
2+
node_modules/
3+
lib/
4+
*.js.map

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ repos:
1212
- id: trailing-whitespace
1313
- id: end-of-file-fixer
1414
- id: check-added-large-files
15+
exclude: ^(credential-setup/dist/|credential-guard/dist/)
1516
- repo: https://github.com/igorshubovych/markdownlint-cli
1617
rev: f295829140d25717bc79368d3f966fc1f67a824f # frozen: v0.41.0
1718
hooks:

README.md

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,42 @@ Adaptive cache action that automatically chooses the appropriate caching backend
1010

1111
## Requirements
1212

13-
- `aws` (AWS CLI)
14-
- `jq`
13+
- `jq` (used by the cache key preparation script)
14+
- `id-token: write` permission (required for OIDC authentication with S3 backend)
15+
16+
## Development
17+
18+
### Prerequisites
19+
20+
- Node.js 20+
21+
- npm
22+
23+
### Install dependencies
24+
25+
```bash
26+
npm install
27+
```
28+
29+
### Run tests
30+
31+
```bash
32+
npm test # single run
33+
npm run test:watch # watch mode
34+
```
35+
36+
### Build
37+
38+
The JS sub-actions (`credential-setup`, `credential-guard`) are bundled with
39+
`@vercel/ncc` into self-contained dist files. Rebuild after changing TypeScript source:
40+
41+
```bash
42+
npm run build # build all sub-actions
43+
npm run build:setup # build credential-setup only
44+
npm run build:guard-main # build credential-guard main only
45+
npm run build:guard-post # build credential-guard post only
46+
```
47+
48+
Bundled output goes to `credential-setup/dist/` and `credential-guard/dist/`. These must be committed since GitHub Actions runs them directly.
1549

1650
## Usage
1751

@@ -112,35 +146,46 @@ Each environment has its own preconfigured S3 bucket and AWS Cognito pool for is
112146

113147
### AWS Credential Isolation
114148

115-
This action configures a dedicated AWS profile (`gh-action-cache`) backed by `credential_process`.
116-
Credentials are fetched on demand using GitHub OIDC + Cognito and never exported to `GITHUB_ENV`.
117-
This ensures cache operations work correctly even when you configure your own AWS credentials later in the workflow.
149+
This action uses a JS-based credential guard to ensure cache operations work correctly even when
150+
other steps in your workflow configure different AWS credentials.
151+
152+
**How it works:**
153+
154+
1. `credential-setup` obtains temporary AWS credentials via GitHub OIDC + Cognito
155+
2. Credentials are saved to a protected temp file and passed to the cache step via outputs
156+
3. The cache restore step uses step-level `env:` from outputs — isolated from GITHUB_ENV
157+
4. `credential-guard` post-step re-exports credentials to GITHUB_ENV for cache save (LIFO ordering)
118158

119-
**Why this matters**: The cache save operation happens in a GitHub Actions post-step (after your job completes).
120-
If you use `aws-actions/configure-aws-credentials` during your job, it would normally override the cache action's credentials,
121-
causing cache save to fail. The cache profile stays isolated and is only used by the cache step.
159+
**This protects against:**
122160

123-
**Example workflow that works correctly**:
161+
- `aws-actions/configure-aws-credentials` overwriting credentials mid-job
162+
- `aws-actions/configure-aws-credentials` cleanup clearing credentials
163+
- Any step writing to `GITHUB_ENV` with different AWS credential values
164+
- Pre-existing `AWS_PROFILE` or `AWS_DEFAULT_PROFILE` in the environment
165+
- Users configuring AWS credentials before or after the cache action
166+
167+
**Works regardless of credential ordering** — you can configure your AWS credentials
168+
before or after the cache action:
124169

125170
```yaml
126171
jobs:
127172
build:
128173
steps:
129-
# Cache action authenticates and uses isolated profile
130-
- uses: SonarSource/gh-action_cache@v1
131-
with:
132-
path: ~/.cache
133-
key: my-cache-${{ hashFiles('**/lockfile') }}
134-
135-
# Your own AWS authentication - does NOT affect cache credentials
174+
# Your own AWS authentication — order does not matter
136175
- uses: aws-actions/configure-aws-credentials@v4
137176
with:
138177
role-to-assume: arn:aws:iam::123456789:role/my-role
139178
aws-region: us-east-1
140179
141-
- run: aws s3 ls # Uses YOUR credentials
180+
# Cache action authenticates independently via OIDC + Cognito
181+
- uses: SonarSource/gh-action-cache@v2
182+
with:
183+
path: ~/.cache
184+
key: my-cache-${{ hashFiles('**/lockfile') }}
185+
186+
- run: aws s3 ls # Uses YOUR credentials (cache never touches GITHUB_ENV)
142187
143-
# Post-step: Cache save uses isolated profile - works correctly!
188+
# Post-step: credential-guard restores cache creds, then cache saves!
144189
```
145190

146191
### Cleanup Policy

0 commit comments

Comments
 (0)