Skip to content

Commit 5da89fe

Browse files
committed
feat(ci): add and test supply-chain hardening in release workflows
GitHub Actions cache poisoning is a known attack[^1] against credential-bearing workflows: - a lower-privileged run plants a malicious entry under a deterministic cache key - a privileged workflow restores a cache from that cache key and executes it - secrets are exfiltrated The `release.yml` workflow publishes packages to npm using OIDC trusted publishing and an `NPM_TOKEN`. Before this change, `release.yml` was vulnerable to this class of attack. Mitigate this by explicitly disabling all build caching in `release.yml`: - `pnpm/action-setup` sets `cache: false` - `actions/setup-node` sets `package-manager-cache: false` - `actions/setup-node` does not set `cache:` - no `actions/cache` step is used Also add a check to the repo (`tests-supply-chain.yml`) to test that high-risk workflows (`release.yml`) are not using caching, and it is not added back in. [^1]: https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/
1 parent 8195af7 commit 5da89fe

14 files changed

Lines changed: 436 additions & 19 deletions

.github/workflows/release.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@ jobs:
2626
name: Install pnpm
2727
with:
2828
run_install: false
29+
# Supply-chain hardening — never cache the pnpm store; a poisoned
30+
# cache entry would execute in this credential-bearing workflow.
31+
cache: false
2932

3033
- name: Install Node.js
3134
uses: actions/setup-node@v6
3235
with:
3336
node-version: 22
34-
cache: 'pnpm'
37+
# No `cache:`, and package-manager-cache disabled. release.yml
38+
# publishes to npm (NPM_TOKEN + OIDC) and must not restore the
39+
# GitHub Actions cache — a cache-poisoning / supply-chain vector.
40+
# Enforced by .github/workflows/tests-supply-chain.yml.
41+
package-manager-cache: false
3542

3643
# node-pty's install hook falls back to `node-gyp rebuild` when no
3744
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Test supply chain security
2+
3+
# Supply-chain gate: asserts that release.yml (and this workflow) never
4+
# restore the GitHub Actions cache. A workflow that both restores a cache
5+
# and holds publish credentials is a cache-poisoning target — see
6+
# https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/
7+
#
8+
# The check (scripts/lint-no-workflow-caching.mjs) requires caching to be
9+
# disabled *explicitly*, not just left at its default:
10+
# - `pnpm/action-setup` must set `cache: false`
11+
# - `actions/setup-node` must set `package-manager-cache: false`
12+
# - no `cache:` input and no `actions/cache` step anywhere
13+
# See the "CI/CD Supply-Chain Hardening" section of SECURITY.md.
14+
#
15+
# Deliberately minimal:
16+
# - GitHub-hosted runner (no Blacksmith transparent cache)
17+
# - contents:read only
18+
# - no secrets
19+
# - no caching
20+
21+
on:
22+
push:
23+
branches:
24+
- main
25+
paths:
26+
- '.github/workflows/release.yml'
27+
- '.github/workflows/tests-supply-chain.yml'
28+
- 'scripts/lint-no-workflow-caching.mjs'
29+
- 'scripts/__tests__/lint-no-workflow-caching.test.mjs'
30+
- 'scripts/__tests__/fixtures/lint-no-workflow-caching/**'
31+
pull_request:
32+
branches:
33+
- '**'
34+
paths:
35+
- '.github/workflows/release.yml'
36+
- '.github/workflows/tests-supply-chain.yml'
37+
- 'scripts/lint-no-workflow-caching.mjs'
38+
- 'scripts/__tests__/lint-no-workflow-caching.test.mjs'
39+
- 'scripts/__tests__/fixtures/lint-no-workflow-caching/**'
40+
41+
permissions:
42+
contents: read
43+
44+
jobs:
45+
verify-no-caching-in-release-workflows:
46+
name: Verify no caching in release workflows
47+
runs-on: ubuntu-latest
48+
49+
steps:
50+
- name: Checkout Repo
51+
uses: actions/checkout@v6
52+
53+
- uses: pnpm/action-setup@v6
54+
name: Install pnpm
55+
with:
56+
run_install: false
57+
# Do not use caching in this cache-testing workflow
58+
cache: false
59+
60+
- name: Install Node.js
61+
uses: actions/setup-node@v6
62+
with:
63+
node-version: 22
64+
# Do not use caching in this cache-testing workflow
65+
package-manager-cache: false
66+
67+
# node-pty's install hook falls back to `node-gyp rebuild` when no
68+
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
69+
# node-gyp on PATH, so install it explicitly.
70+
- name: Install node-gyp
71+
run: npm install -g node-gyp
72+
73+
- name: Install dependencies
74+
run: pnpm install --frozen-lockfile
75+
76+
- name: Run lint script self-tests
77+
run: pnpm run test:scripts
78+
79+
- name: Verify no caching in release.yml and tests-supply-chain.yml
80+
run: pnpm run lint:workflow-cache

SECURITY.md

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Security Policy
22

3-
CipherStash takes the security of our software, infrastructure, and customers extremely seriously.
3+
CipherStash takes the security of our software, infrastructure, and customers extremely seriously.
44
This document describes the security posture, reporting process, and guidelines for this repository and associated packages.
55

66
## Supported Packages
@@ -80,13 +80,13 @@ We will acknowledge receipt within **48 hours** and provide regular updates unti
8080

8181
CipherStash follows a **coordinated responsible disclosure** process:
8282

83-
1. **Submit report** privately via `security@cipherstash.com`.
84-
2. **Acknowledgement** within 48 hours.
85-
3. **Assessment** of severity using CVSS and internal risk models.
86-
4. **Fix development** and patch release in a private branch.
83+
1. **Submit report** privately via `security@cipherstash.com`.
84+
2. **Acknowledgement** within 48 hours.
85+
3. **Assessment** of severity using CVSS and internal risk models.
86+
4. **Fix development** and patch release in a private branch.
8787
5. **Coordinated disclosure**, including:
8888
- New patch release(s)
89-
- Security advisory on GitHub
89+
- Security advisory on GitHub
9090
- Credit to reporter (optional)
9191

9292
We will never take legal action against good-faith security researchers who follow this policy.
@@ -102,14 +102,14 @@ The following are **in scope**:
102102
- Protect.js cryptographic implementations, configuration layers, and CLI tooling
103103
- Key-handling, authenticated encryption behaviour, JSON/JSONB field-level encryption flows
104104
- Documentation or code examples that could lead to insecure usage
105-
- CipherStash’s internal infrastructure
105+
- CipherStash’s internal infrastructure
106106
- CipherStash Proxy, ZeroKMS, or other backend products
107107

108108
The following are **out of scope**:
109109

110110
- Example applications in the `examples` dir (though we are still grateful for any relevant disclosires there)
111-
- Social engineering, physical attacks, or denial-of-service
112-
- Attacks requiring privileged access to developer machines or CI/CD infrastructure
111+
- Social engineering, physical attacks, or denial-of-service
112+
- Attacks requiring privileged access to developer machines or CI/CD infrastructure
113113

114114
---
115115

@@ -118,23 +118,46 @@ The following are **out of scope**:
118118
To maintain a strong security posture, contributors MUST:
119119

120120
### ⚙️ Follow cryptographic safety rules
121-
- Do **not** modify cryptographic primitives without prior discussion
122-
- Avoid introducing new crypto dependencies without prior discussion
123-
- Never check in test keys, secrets, or example credentials
121+
- Do **not** modify cryptographic primitives without prior discussion
122+
- Avoid introducing new crypto dependencies without prior discussion
123+
- Never check in test keys, secrets, or example credentials
124124

125125
### 🛡 Coding & dependency hygiene
126-
- Avoid adding dependencies unless necessary
127-
- Keep dependencies updated and vetted
128-
- Use TypeScript for all new code
129-
- Ensure all code paths that handle keys or encrypted data include type-safe boundaries
126+
- Avoid adding dependencies unless necessary
127+
- Keep dependencies updated and vetted
128+
- Use TypeScript for all new code
129+
- Ensure all code paths that handle keys or encrypted data include type-safe boundaries
130130

131131
### 🔍 Testing & review
132-
- Submit PRs with tests covering edge cases and misuse-resistant behaviour
133-
- Flag any changes involving key derivation, key wrapping, AAD, or encryption modes for mandatory security review
132+
- Submit PRs with tests covering edge cases and misuse-resistant behaviour
133+
- Flag any changes involving key derivation, key wrapping, AAD, or encryption modes for mandatory security review
134134
- Do not merge PRs that downgrade security controls or introduce unsafe defaults
135135

136136
---
137137

138+
## CI/CD Supply-Chain Hardening
139+
140+
The `release.yml` workflow publishes packages to npm using OIDC trusted
141+
publishing and an `NPM_TOKEN`.
142+
143+
[GitHub Actions cache poisoning is a known attack][1] against credential-bearing
144+
workflows. The mechanism is:
145+
146+
- A lower-privileged workflow run plants a malicious entry under a deterministic
147+
cache key
148+
- A privileged workflow restores a cache from that deterministic cache key
149+
- The malicious entry is executed by the privileged workflow, and secrets
150+
are exfiltrated
151+
152+
We mitigate this by:
153+
154+
- Explicitly disabling all caching in `release.yml`
155+
- Automated checks for disabled caching on high-risk workflows
156+
157+
[1]: https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/
158+
159+
---
160+
138161
## Questions?
139162

140163
For general questions about CipherStash security practices (not security incidents), contact:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules",
4949
"code:fix": "biome check --write",
5050
"lint:runners": "node scripts/lint-no-hardcoded-runners.mjs",
51+
"lint:workflow-cache": "node scripts/lint-no-workflow-caching.mjs",
5152
"release": "pnpm run build && changeset publish",
5253
"test": "turbo test --filter './packages/*'",
5354
"test:e2e": "turbo run test:e2e",
@@ -57,6 +58,7 @@
5758
"@biomejs/biome": "^1.9.4",
5859
"@changesets/cli": "^2.29.6",
5960
"@types/node": "^22.15.12",
61+
"js-yaml": "^3.14.2",
6062
"rimraf": "^6.1.2",
6163
"turbo": "2.1.1",
6264
"vitest": "catalog:repo"

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Actions Cache Restore
2+
on:
3+
push:
4+
branches: [main]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v6
10+
- name: Restore build cache
11+
uses: actions/cache/restore@v4
12+
with:
13+
path: .turbo
14+
key: turbo-${{ github.sha }}
15+
- run: pnpm install --frozen-lockfile
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Actions Cache Save
2+
on:
3+
push:
4+
branches: [main]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v6
10+
- name: Save build cache
11+
uses: actions/cache/save@v4
12+
with:
13+
path: .turbo
14+
key: turbo-${{ github.sha }}
15+
- run: pnpm install --frozen-lockfile
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Actions Cache
2+
on:
3+
push:
4+
branches: [main]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v6
10+
- name: Restore build cache
11+
uses: actions/cache@v4
12+
with:
13+
path: .turbo
14+
key: turbo-${{ github.sha }}
15+
- run: pnpm install --frozen-lockfile
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Clean
2+
on:
3+
push:
4+
branches: [main]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v6
10+
- uses: pnpm/action-setup@v6
11+
with:
12+
run_install: false
13+
cache: false
14+
- name: Install Node.js
15+
uses: actions/setup-node@v6
16+
with:
17+
node-version: 22
18+
package-manager-cache: false
19+
- run: pnpm install --frozen-lockfile
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Missing Explicit False
2+
on:
3+
push:
4+
branches: [main]
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v6
10+
- uses: pnpm/action-setup@v6
11+
with:
12+
run_install: false
13+
- name: Install Node.js
14+
uses: actions/setup-node@v6
15+
with:
16+
node-version: 22
17+
- run: pnpm install --frozen-lockfile

0 commit comments

Comments
 (0)