Build Cache Node migration checklist for Gradle and Maven
+
A short checklist for moving builds from the free Develocity Build Cache Node to FosterStack Cache, with a real run to back it: the same project built against an old node, then against FosterStack Cache 0.2.1, then back. Read why the node is going away in Build Cache Node end of life.
+
+
What I tested
+
Gradle 9.8.0 and a small Java project, built twice against each server. The old side was the final Build Cache Node image (version 21.2), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.
+
+
The checklist
+
1. Find every place your builds point at the old node
+
Search your repositories and CI settings for HttpBuildCache and for the node's address. The old node's address ends in /cache/. Write the list down: each entry is one line to change.
+
+
2. Start the new server
+
Follow your first 15 minutes. Turn on a password before other machines can reach it.
Nothing is copied over, and nothing needs to be: a cache holds results your builds can always rebuild. The first build on the new server runs everything and fills it; the second hits.
+
+
5. Decide about the /cache/ part of the URL
+
You can drop it, as above, or keep it. FosterStack Cache uses the whole request path as the key, so a prefix works. Entries under different paths are separate, so pick one and use it everywhere.
+
+
6. Keep the old node running until you are sure
+
Rolling back is the same one-line change in reverse.
+
+
7. Retire the old node, and check the new one
+
When builds are hitting the new server, stop the old node. Watch the new one with monitoring, and size its disk with server sizing.
+
+
What happened in the run
+
Each row is two builds of the same project with gradle clean in between and Gradle's own local cache turned off, so a hit can only come from the server:
+
+
Pointed at
First build
Second build
+
The old node, URL ending in /cache/
:compileJava ran
:compileJava FROM-CACHE
+
FosterStack Cache, only the URL changed (no /cache/)
:compileJava ran: the new server was empty
:compileJava FROM-CACHE
+
FosterStack Cache, URL kept with /cache/
:compileJava ran: separate entries
:compileJava FROM-CACHE
+
Back to the old node (rollback)
:compileJava FROM-CACHE: its entries were still there
not run
+
+
The new server's status page agreed: two entries stored, two hits, two misses. One small difference you may notice: a successful upload returns 200 from the old node and 201 from FosterStack Cache. Gradle accepted both.
+
+
+
+
diff --git a/sitemap.xml b/sitemap.xml
index 315bc16..b00311b 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -81,4 +81,8 @@
https://fosterstack.com/first-15-minutes-build-cache/2026-10-08
+
+ https://fosterstack.com/build-cache-node-migration-checklist/
+ 2026-10-08
+
From a5c9577a5efb6c1a2d76ed5beddbae0079df789f Mon Sep 17 00:00:00 2001
From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com>
Date: Thu, 8 Oct 2026 22:50:35 -0400
Subject: [PATCH 02/25] Migration page: describe the old node image only as
what was observed
---
build-cache-node-migration-checklist/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build-cache-node-migration-checklist/index.html b/build-cache-node-migration-checklist/index.html
index 2b82d4a..1c9feba 100644
--- a/build-cache-node-migration-checklist/index.html
+++ b/build-cache-node-migration-checklist/index.html
@@ -90,7 +90,7 @@
Build Cache Node migration checklist for Gradle and Maven
A short checklist for moving builds from the free Develocity Build Cache Node to FosterStack Cache, with a real run to back it: the same project built against an old node, then against FosterStack Cache 0.2.1, then back. Read why the node is going away in Build Cache Node end of life.
What I tested
-
Gradle 9.8.0 and a small Java project, built twice against each server. The old side was the final Build Cache Node image (version 21.2), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.
+
Gradle 9.8.0 and a small Java project, built twice against each server. The old side was the latest published Build Cache Node image (version 21.2, built June 2025), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.
The checklist
1. Find every place your builds point at the old node
Production build cache: Docker Compose, nginx and TLS
+
A Compose file for a shared build cache that only answers over HTTPS, with a password, behind nginx. Everything here was run for real on FosterStack Cache 0.2.1 and Gradle 9.8.0. The exact list of what was and was not tested is just below.
+
+
What was tested, and what was not
+
+
Tested: the two files below, with Docker Compose on a Mac, nginx 1.29 as the proxy, a self-signed certificate for localhost, and a real Gradle build over HTTPS.
+
Not tested: a certificate from a public authority or automatic renewal, a public server on port 443, other proxies or load balancers, Kubernetes ingress, and long-running use.
+
+
+
compose.yaml
+
services:
+ fscache:
+ image: ghcr.io/fosterstack/cache:0.2.1
+ restart: unless-stopped
+ # no "ports:" on purpose: only the proxy is reachable from outside
+ volumes:
+ - fscache-data:/home/nonroot
+ environment:
+ FSCACHE_MAX_BYTES: "53687091200" # 50 GiB
+ FSCACHE_USERNAME: gradle
+ FSCACHE_PASSWORD: change-me # generate one: openssl rand -base64 24
+
+ proxy:
+ image: nginx:1.29-alpine
+ restart: unless-stopped
+ depends_on:
+ - fscache
+ ports:
+ - "127.0.0.1:8443:443" # use 443:443 on a real server
+ volumes:
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ - ./certs:/etc/nginx/certs:ro
+
+volumes:
+ fscache-data:
+
The cache has no ports entry on purpose. Only the proxy is reachable from outside. Generate a real password, for example with openssl rand -base64 24, and never keep change-me.
+
+
nginx.conf
+
server {
+ listen 443 ssl;
+ server_name localhost;
+ ssl_certificate /etc/nginx/certs/cert.pem;
+ ssl_certificate_key /etc/nginx/certs/key.pem;
+
+ # the cache accepts entries up to 1 GiB by default; nginx's own default is 1 MB
+ client_max_body_size 1g;
+
+ location / {
+ proxy_pass http://fscache:8080;
+ proxy_set_header Host $host;
+ }
+}
+
The client_max_body_size line matters. See the tests below.
+
+
What I checked, and what I saw
+
+
Check
Result
+
https://localhost:8443/healthz with the certificate trusted
ok
+
The same without trusting the certificate
curl refuses (exit 60)
+
The cache's own port 8080 from the host
not reachable
+
Plain http:// to the proxy's TLS port
400
+
Upload with no password, through the proxy
401
+
Upload with the password, through the proxy
201, and the value reads back
+
Uploads of about 1 MB and 2 MB with nginx's default settings
413 from nginx, not from the cache
+
The same uploads, and 20 MB, after client_max_body_size 1g;
201 each
+
+
The size limit is the one to watch. nginx rejects bodies over 1 MB by default, and Gradle's cache entries are often bigger, so builds would quietly stop storing. The cache itself accepts up to 1 GiB per entry by default, which is why the example uses 1g. See the status codes page.
+
+
Gradle over HTTPS
+
With a certificate Gradle does not trust, the build still succeeds, but without the cache, and prints:
+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
+
For my self-signed test certificate I made Java trust it:
With that, the first build ran :compileJava and the second printed > Task :compileJava FROM-CACHE, through the proxy, over HTTPS, with the password. With a certificate from an authority Java already trusts you would not need the truststore lines. The Gradle side of the setup is in the tutorial.
Many companies make builds reach the network through a proxy. This page shows how Gradle's remote build cache behaves behind one: the settings, the ways it goes wrong, and how to tell a proxy problem from a cache problem. Everything was run for real with Gradle 9.8.0 and FosterStack Cache 0.2.1.
+
+
What was tested, and what was not
+
+
Tested: a small proxy I wrote for the test (it forwards plain HTTP, tunnels HTTPS with CONNECT, can refuse a host, and logs every request), the cache on a non-loopback address, once over plain HTTP and once over HTTPS through nginx with a self-signed certificate that Java was told to trust.
+
Not tested: a real corporate proxy product, a proxy that asks for a password (407), a proxy that inspects and re-signs TLS, automatic proxy configuration files, the HTTP_PROXY variables, and Maven's proxy settings.
+
+
+
The settings
+
Gradle takes proxy settings from Java system properties. Put them in ~/.gradle/gradle.properties or the project's gradle.properties, with the systemProp. prefix:
Use the https pair for an https:// cache URL and the http pair for plain HTTP. The last line lists hosts that should skip the proxy.
+
+
What I saw
+
+
Setup
Result
What the proxy logged
+
No proxy settings, cache at <cache-ip>
second build FROM-CACHE
nothing (not used)
+
Proxy settings, same plain-HTTP URL
FROM-CACHE
GET http://<cache-ip>:18130/<key>, one per build
+
Proxy settings, but the cache URL is 127.0.0.1
FROM-CACHE
nothing: Java skips the proxy for loopback addresses
+
Proxy settings, with the cache's address in nonProxyHosts
FROM-CACHE
nothing
+
Proxy settings, HTTPS cache, tunnel allowed
FROM-CACHE
CONNECT <cache-ip>:18443, one per build
+
Proxy settings, proxy refuses the cache host
the build succeeds but without the cache (log below)
the request, answered 403
+
+
+
Three things that catch people
+
1. Testing against localhost skips the proxy. With the cache at 127.0.0.1, Gradle never used the proxy even though it was configured. A setup that works on your laptop against a local cache proves nothing about the proxy path. Test against the real address.
+
2. A refusing proxy looks like a cache error. When the proxy answered 403 to the tunnel, Gradle printed:
+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'https://<cache-ip>:18443/d4f2de693b561fd2c5d60b17a9232d6f' response status 403: Forbidden
+
That is the same text you would get from the cache refusing a write with the read-only login (see the status codes). To tell them apart, take Gradle out of it and try the same route with curl:
+
$ curl -sS -x http://proxy.example.com:3128 https://cache.example.com/healthz
+curl: (56) CONNECT tunnel failed, response 403 # the proxy said no
+$ curl -sS --noproxy '*' https://cache.example.com/healthz
+ok # the cache is fine
+
Those two lines are what I got from my test proxy refusing, and the cache answering directly. If the first one fails and the second works, ask whoever runs the proxy to allow the cache's host and port.
+
3. Over HTTPS the proxy only sees the address. A CONNECT tunnel shows the proxy the host and port and nothing inside, so the password and the cache keys are not visible to it. That also means any rule on the path cannot be applied. This is how the proxy log looked in my run: one CONNECT line per build.
+
+
Checklist
+
+
Ask for the cache's host and port to be allowed through the proxy (HTTPS, usually 443).
+
Put the proxy lines in gradle.properties, with the systemProp. prefix, and list internal hosts in nonProxyHosts.
+
+
+
+
diff --git a/sitemap.xml b/sitemap.xml
index 072da52..0735c4b 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -89,4 +89,8 @@
https://fosterstack.com/build-cache-docker-compose-production/2026-10-08
+
+ https://fosterstack.com/gradle-build-cache-corporate-proxy/
+ 2026-10-08
+
From 66db511664dc22258544b87225480b049626eda1 Mon Sep 17 00:00:00 2001
From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com>
Date: Thu, 8 Oct 2026 22:58:32 -0400
Subject: [PATCH 05/25] Page: upgrade a self-hosted build cache and keep its
data (real run, 0.1.0 to 0.2.1)
---
sitemap.xml | 4 +
upgrade-build-cache-server/index.html | 143 ++++++++++++++++++++++++++
2 files changed, 147 insertions(+)
create mode 100644 upgrade-build-cache-server/index.html
diff --git a/sitemap.xml b/sitemap.xml
index 0735c4b..e470de5 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -93,4 +93,8 @@
https://fosterstack.com/gradle-build-cache-corporate-proxy/2026-10-08
+
+ https://fosterstack.com/upgrade-build-cache-server/
+ 2026-10-08
+
diff --git a/upgrade-build-cache-server/index.html b/upgrade-build-cache-server/index.html
new file mode 100644
index 0000000..81e0cc3
--- /dev/null
+++ b/upgrade-build-cache-server/index.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+Upgrade a self-hosted build cache and keep its data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
Upgrade a self-hosted build cache and keep its data
+
Can you move a build cache to a newer release without losing what it holds? I tested it with the only two published releases: I stored entries on 0.1.0, started 0.2.1 on the same data volume, wrote more, and then went back to 0.1.0. This is what happened.
+
+
What was tested, and what was not
+
+
Tested: Docker, one named volume at /home/nonroot, release 0.1.0 and then 0.2.1 (and back), no password, stopped and started cleanly between versions.
+
Not tested: any other pair of releases (there is no 0.2.0 release; see the changelog), upgrading while the server is running, Kubernetes or Compose rollouts, the FIPS image, or a cache that was close to its size cap.
Use the same volume and the same settings (environment variables for the password and size cap) as the old container. Pin the exact tag you want, as above, rather than latest.
+
+
What I saw
+
+
Step
Result
+
0.1.0: store three entries on a fresh volume
201 each; its metrics show 3 entries, 29 bytes
+
Stop 0.1.0, start 0.2.1 on the same volume
starts normally; the status page shows "version": "v0.2.1", 3 entries, 29 bytes; all three values read back
+
0.2.1: store two more
201 each; 5 entries
+
Stop 0.2.1, start 0.1.0 on the same volume again
starts normally; all values read back, including the two written by 0.2.1
+
+
So in my test an upgrade lost nothing, and a rollback to the old release could still read what the new one had written.
+
+
Two differences between the releases
+
+
0.1.0 has no status page./statusz returned 404 there. 0.2.1 has it. The metrics page is on both.
+
0.1.0's entry count resets after a restart. After the rollback its metric fscache_store_entries read 0 although every value was readable. I checked whether that was caused by the upgrade: it is not. A volume only 0.1.0 ever wrote showed 3 entries before a plain restart and 0 after it, with the data still readable. 0.2.1, restarted the same way, reported 3 entries and 9 bytes correctly.
+
+
+
+
From 9b774a85db81d730de9a8ba601fbac3c58aaa8f9 Mon Sep 17 00:00:00 2001
From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com>
Date: Thu, 8 Oct 2026 23:00:37 -0400
Subject: [PATCH 06/25] Page: build cache server exits with permission denied
on its data directory (real run)
---
.../index.html | 137 ++++++++++++++++++
sitemap.xml | 4 +
2 files changed, 141 insertions(+)
create mode 100644 build-cache-permission-denied-data-directory/index.html
diff --git a/build-cache-permission-denied-data-directory/index.html b/build-cache-permission-denied-data-directory/index.html
new file mode 100644
index 0000000..0817d67
--- /dev/null
+++ b/build-cache-permission-denied-data-directory/index.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+Build cache server exits: permission denied on its data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
Build cache server exits with "permission denied" on its data
+
You start the container and it stops straight away. The log ends with permission denied. The cause is almost always who owns the data directory. This page shows the failure and the fix, reproduced for real with the 0.2.1 image.
+
+
What you see
+
The container exits with status 1 within a few seconds, and its log says:
+
{{"level":"ERROR","msg":"fscache: fatal","error":"write shutdown marker: open data/.unclean-shutdown: permission denied"}}
+
+
Why it happens
+
The server does not run as root. It runs as user 65532, and it keeps its files in a data folder inside /home/nonroot. If that folder already exists and belongs to someone else, the server cannot write to it and stops.
+
I reproduced it with a volume that already held data owned by root, which is what you get after restoring a backup as root:
Give the whole tree to user 65532. The image has no shell, so use a throwaway container to do it:
+
docker run --rm -v fscache-data:/d ubuntu:24.04 chown -R 65532:65532 /d
+
After that the same image started normally: /healthz answered ok and an upload returned 201.
+
+
What does not cause it
+
+
A new, empty named volume. This is what our first-run steps create, and the volume then belongs to 65532. I also tried a new empty volume whose top folder I had set to root. The server still started, and afterwards the folder belonged to 65532, because Docker copies the image's ownership into an empty volume the first time it is used.
+
+
+
What I did not test
+
+
A bind mount of a host folder on Linux. The same rule should apply (the folder has to be writable by 65532), but I ran on a Mac, where bind mounts behave differently.
+
Kubernetes without the fsGroup setting. The manifests in our Kubernetes page set fsGroup: 65532, and I ran them with it; I did not run them without it.
+
Running the container as root to avoid the problem. It is not what we document.
Maven build cache extension behind a corporate proxy
+
Maven has its own proxy settings, and the Apache Build Cache Extension follows them. This page shows what the extension sent through a proxy, how it differs from Gradle, and what you see when the proxy says no. All of it was run for real with Maven 3.10.0, extension 1.2.3 and FosterStack Cache 0.2.1.
+
+
What was tested, and what was not
+
+
Tested: a small proxy I wrote for the test, which forwards plain HTTP, can refuse a host, and logs every request (I made it close each connection after one request so that none go unlogged). The cache was on a non-loopback address, over plain HTTP, with a password.
+
Not tested: HTTPS through a proxy for Maven, a proxy that asks for a password, other Maven or extension versions, and any real proxy product. Gradle is on its own page.
+
+
+
The settings
+
In ~/.m2/settings.xml, next to the cache's password entry:
The cache itself is set up as in the Maven setup. The server id must still match the remote's.
+
+
What went through the proxy
+
With the proxy configured, all of the extension's cache traffic went through it: the lookups, the download that restores a build, and the uploads. This is what the proxy logged for two builds, the first filling the cache and the second restoring from it (the proxy also saw Maven's own traffic to repository.apache.org, which is not shown):
The second build logged Found cached build, restoring demo:demo from cache.
+
+
Other cases
+
+
Setup
What happened
+
Proxy configured, cache URL is 127.0.0.1
Still went through the proxy (4 requests logged). Unlike Gradle, Maven does not skip the proxy for loopback.
+
Proxy configured, the cache's address in <nonProxyHosts>
Nothing reached the proxy. The build still restored from the cache.
+
Proxy refuses the cache's host
Nothing was stored or restored. The build succeeded without the cache, and logged Error downloading cache item with HTTP Status: 403.
+
Proxy not running at all
The lookup failed with Connect to 127.0.0.1:18131 ... Connection refused. The build succeeded without the cache, and nothing was uploaded.
+
+
+
Things that catch people
+
+
A local test can fail because of the proxy. If you try the cache on 127.0.0.1 with a proxy configured in settings.xml, Maven sends it to the proxy, which usually cannot reach your machine's loopback. List it in <nonProxyHosts>.
+
A refusing proxy looks like a cache error. The 403 comes from the proxy, not from the cache. To check, run the same request from the build machine with and without the proxy; see the curl check on the Gradle page.
+
Failure does not break the build. A build behind a blocking proxy just stops using the cache, so look for Error downloading cache item and for missing Saved to remote cache lines.
Tested: a small proxy I wrote for the test (it forwards plain HTTP, tunnels HTTPS with CONNECT, can refuse a host, and logs every request), the cache on a non-loopback address, once over plain HTTP and once over HTTPS through nginx with a self-signed certificate that Java was told to trust.
-
Not tested: a real corporate proxy product, a proxy that asks for a password (407), a proxy that inspects and re-signs TLS, automatic proxy configuration files, the HTTP_PROXY variables, and Maven's proxy settings.
+
Not tested: a real corporate proxy product, a proxy that asks for a password (407), a proxy that inspects and re-signs TLS, automatic proxy configuration files, and the HTTP_PROXY variables. Maven is covered on its own page, where it behaves differently: for example it does not skip the proxy for loopback addresses.
Uploads of about 1 MB and 2 MB with nginx's default settings
413 from nginx, not from the cache
The same uploads, and 20 MB, after client_max_body_size 1g;
201 each
-
The size limit is the one to watch. nginx rejects bodies over 1 MB by default, and Gradle's cache entries are often bigger, so builds would quietly stop storing. The cache itself accepts up to 1 GiB per entry by default, which is why the example uses 1g. See the status codes page.
+
The size limit is the one to watch. nginx rejects bodies over 1 MB by default, and Gradle's cache entries are often bigger, so uploads of larger entries would be refused. The cache itself accepts up to 1 GiB per entry by default, which is why the example uses 1g. See the status codes page.
Gradle over HTTPS
With a certificate Gradle does not trust, the build still succeeds, but without the cache, and prints:
diff --git a/build-cache-node-migration-checklist/index.html b/build-cache-node-migration-checklist/index.html
index 1c9feba..b0660ab 100644
--- a/build-cache-node-migration-checklist/index.html
+++ b/build-cache-node-migration-checklist/index.html
@@ -3,9 +3,9 @@
-Build Cache Node migration checklist for Gradle and Maven
+Build Cache Node migration checklist for Gradle
-
+
@@ -86,11 +86,11 @@
Infrastructure worth keeping — and kept patched.
-
Build Cache Node migration checklist for Gradle and Maven
-
A short checklist for moving builds from the free Develocity Build Cache Node to FosterStack Cache, with a real run to back it: the same project built against an old node, then against FosterStack Cache 0.2.1, then back. Read why the node is going away in Build Cache Node end of life.
+
Build Cache Node migration checklist for Gradle
+
A short checklist for moving builds from the Develocity Build Cache Node to FosterStack Cache, with a real run to back it: the same project built against an old node, then against FosterStack Cache 0.2.1, then back. Read why the node is going away in Build Cache Node end of life.
What I tested
-
Gradle 9.8.0 and a small Java project, built twice against each server. The old side was the latest published Build Cache Node image (version 21.2, built June 2025), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.
+
Gradle 9.8.0 and a small Java project, built twice against each server. The old side was a Build Cache Node image (version 21.2, built June 2025), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.
The checklist
1. Find every place your builds point at the old node
@@ -116,7 +116,7 @@
5. Decide about the /cache/ part of the URL
You can drop it, as above, or keep it. FosterStack Cache uses the whole request path as the key, so a prefix works. Entries under different paths are separate, so pick one and use it everywhere.
6. Keep the old node running until you are sure
-
Rolling back is the same one-line change in reverse.
+
With Gradle, rolling back is the same one-line change in reverse; I tested that.
7. Retire the old node, and check the new one
When builds are hitting the new server, stop the old node. Watch the new one with monitoring, and size its disk with server sizing.
Build cache server exits with "permission denied" on its data
-
You start the container and it stops straight away. The log ends with permission denied. The cause is almost always who owns the data directory. This page shows the failure and the fix, reproduced for real with the 0.2.1 image.
+
You start the container and it stops straight away. The log ends with permission denied. The cause I reproduced is who owns the data directory. This page shows the failure and the fix, reproduced for real with the 0.2.1 image.
What you see
The container exits with status 1 within a few seconds, and its log says:
-
{{"level":"ERROR","msg":"fscache: fatal","error":"write shutdown marker: open data/.unclean-shutdown: permission denied"}}
+
{"level":"ERROR","msg":"fscache: fatal","error":"write shutdown marker: open data/.unclean-shutdown: permission denied"}
Why it happens
The server does not run as root. It runs as user 65532, and it keeps its files in a data folder inside /home/nonroot. If that folder already exists and belongs to someone else, the server cannot write to it and stops.
Use the https pair for an https:// cache URL and the http pair for plain HTTP. The last line lists hosts that should skip the proxy.
What I saw
+
For the plain-HTTP rows I set isAllowInsecureProtocol = true, which Gradle requires for non-loopback http://. That is for the test rig only; use HTTPS for real.
Setup
Result
What the proxy logged
No proxy settings, cache at <cache-ip>
second build FROM-CACHE
nothing (not used)
@@ -124,12 +125,12 @@
Three things that catch people
curl: (56) CONNECT tunnel failed, response 403 # the proxy said no
$ curl -sS --noproxy '*' https://cache.example.com/healthz
ok # the cache is fine
-
Those two lines are what I got from my test proxy refusing, and the cache answering directly. If the first one fails and the second works, ask whoever runs the proxy to allow the cache's host and port.
+
I substituted example hostnames in those commands; the two output lines are the real ones, from my test proxy refusing and from the cache answering directly. If the first one fails and the second works, ask whoever runs the proxy to allow the cache's host and port.
3. Over HTTPS the proxy only sees the address. A CONNECT tunnel shows the proxy the host and port and nothing inside, so the password and the cache keys are not visible to it. That also means any rule on the path cannot be applied. This is how the proxy log looked in my run: one CONNECT line per build.
Checklist
-
Ask for the cache's host and port to be allowed through the proxy (HTTPS, usually 443).
+
Ask for the cache's host and port to be allowed through the proxy (HTTPS).
Put the proxy lines in gradle.properties, with the systemProp. prefix, and list internal hosts in nonProxyHosts.
A local test can fail because of the proxy. If you try the cache on 127.0.0.1 with a proxy configured in settings.xml, Maven sends it to the proxy, which usually cannot reach your machine's loopback. List it in <nonProxyHosts>.
+
A local test can fail because of the proxy. If you try the cache on 127.0.0.1 with a proxy configured in settings.xml, Maven sends it to the proxy, which may not be able to reach your machine's loopback. List it in <nonProxyHosts>.
A refusing proxy looks like a cache error. The 403 comes from the proxy, not from the cache. To check, run the same request from the build machine with and without the proxy; see the curl check on the Gradle page.
Failure does not break the build. A build behind a blocking proxy just stops using the cache, so look for Error downloading cache item and for missing Saved to remote cache lines.
From 683c1948cfccc77406d7786149bab5a88725dec2 Mon Sep 17 00:00:00 2001
From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com>
Date: Thu, 8 Oct 2026 23:19:32 -0400
Subject: [PATCH 10/25] Owner's copy edit: new home headline and new security
heading
---
build-cache-node-end-of-life/index.html | 2 +-
index.html | 4 ++--
migrate-build-cache-node/index.html | 2 +-
regulated/index.html | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/build-cache-node-end-of-life/index.html b/build-cache-node-end-of-life/index.html
index 12f4f27..8583f2a 100644
--- a/build-cache-node-end-of-life/index.html
+++ b/build-cache-node-end-of-life/index.html
@@ -118,7 +118,7 @@
What FosterStack Cache is today
It is early. The paid tiers are not built. Everything you can download today is the free core.
-
Security is the difference
+
More than kept patched.
Security is what we are building this product around. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
Signed and verifiable
diff --git a/index.html b/index.html
index 4e4f460..8cab950 100644
--- a/index.html
+++ b/index.html
@@ -93,7 +93,7 @@
What that means and your options.
-
Your build cache shouldn't die with its vendor.
+
Fast builds from cache are worth keeping.
FosterStack is a self-hosted remote build cache server — a Develocity Build Cache Node
replacement that speaks the same Gradle remote build cache HTTP protocol, so migrating is
@@ -140,7 +140,7 @@
What you get that a bare HTTP endpoint doesn't give you
30-minute migration
A step-by-step guide for existing Build Cache Node deployments. Same protocol, same CI config shape — migrating is mostly a URL change.
-
Security is the difference
+
More than kept patched.
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
Once builds hit the new server, stop the old Build Cache Node container.
-
Security is the difference
+
More than kept patched.
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
+
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
What you get that a bare HTTP endpoint doesn't give you
Cache management
Size-capped LRU eviction that keeps a busy CI cache healthy without hand-tending — set the cap, and the oldest-unused entries make room.
Authentication
HTTP Basic Auth over TLS, wired the way Gradle and Maven already expect credentials. Constant-time comparison, no credential ever logged.
Metrics & status
Prometheus metrics, a read-only status page with hit rates and size-vs-cap, and a Grafana dashboard in the repo — so you know the cache is earning its keep.
-
Maintenance target
Dependency CVEs remediated fast — target within 48 hours of disclosure. A stated intention until the paid tiers exist, and the release history is the track record either way.
+
Maintenance target
Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
Acceptance-tested
Every change runs against a real multi-module Gradle build in CI: a from-scratch second build must produce real remote-cache hits, or the change does not merge.
30-minute migration
A step-by-step guide for existing Build Cache Node deployments. Same protocol, same CI config shape — migrating is mostly a URL change.
@@ -163,7 +163,7 @@
Small, quiet and patched
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
+
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
diff --git a/llms.txt b/llms.txt
index 032c825..48bf21a 100644
--- a/llms.txt
+++ b/llms.txt
@@ -101,9 +101,7 @@ Not yet shipped:
not yet purchasable; the license key that unlocks them is not built.
Everything in the repository is the free MIT core, including the -fips image
and all release evidence.
-- A CVE patch commitment that can be relied on. The project aims to ship fixes for
- dependency CVEs within 48 hours of public disclosure; that is a stated intention,
- not a contractual promise.
+- A CVE patch commitment that can be relied on. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
Please do not describe unshipped items as available, and do not attach dates to them.
diff --git a/long-term-support-build-cache/index.html b/long-term-support-build-cache/index.html
index 99acb4d..df53773 100644
--- a/long-term-support-build-cache/index.html
+++ b/long-term-support-build-cache/index.html
@@ -96,7 +96,7 @@
What that means for you
To get a fix, move to the latest release. Older releases are not on the supported list.
Security patches are never withheld from the free tier.
-
We aim to ship fixes for dependency vulnerabilities within 48 hours of public disclosure. That is a target, not a promise.
+
Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
Every published image is rescanned daily, so a vulnerability disclosed against something we already shipped raises an issue.
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
+
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
+
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
Send the report, not a patch. Please do not post details publicly before a fix ships.
What we aim for
-
We aim to ship fixes for dependency vulnerabilities within 48 hours of public disclosure. That is a target, not a promise you can hold us to. Security patches are never withheld from the free tier: the same fix ships to everyone on the same day.
+
Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier: the same fix ships to everyone on the same day.
The supported version is the latest minor release.
Look for signatures, a software bill of materials, build provenance and vulnerability statements, and check whether you need an account to see them. FosterStack Cache: signed images and checksums, an SBOM per binary archive, build provenance and a VEX file, all public. See how we build.
4. Is it kept patched?
-
Ask how often published files are rescanned and how fast fixes ship. FosterStack Cache: a release is blocked on scanner findings unless a VEX statement explains them, and published images are rescanned daily. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise.
+
Ask how often published files are rescanned and how fast fixes ship. FosterStack Cache: a release is blocked on scanner findings unless a VEX statement explains them, and published images are rescanned daily. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
5. Does it call home?
A server in your build network should not need to reach out. FosterStack Cache: no telemetry, no update check, no licence check. It can run on a network with no route to the internet.
federal or compliance requirements attached — FIPS-validated cryptography,
SBOMs, provenance, a security questionnaire with two hundred rows — and
suddenly every tool in your build pipeline needs paperwork it has never
- had. Big vendors sell their way through that pile with compliance teams
- and six-figure contracts. Small teams mostly just lose the deal.
+ had. Big vendors have compliance teams to get through that pile. Small teams
+ mostly just lose the deal.
The cost of compliance shouldn't determine who gets to
From 8f356a6abcd557d9ac429759e0dff915cf90803a Mon Sep 17 00:00:00 2001
From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com>
Date: Thu, 8 Oct 2026 23:43:16 -0400
Subject: [PATCH 13/25] Security block: the owner's short list; auditor line
held until v0.3.0 ships
---
build-cache-node-end-of-life/index.html | 30 ++++++-------------------
index.html | 30 ++++++-------------------
migrate-build-cache-node/index.html | 30 ++++++-------------------
regulated/index.html | 30 ++++++-------------------
4 files changed, 28 insertions(+), 92 deletions(-)
diff --git a/build-cache-node-end-of-life/index.html b/build-cache-node-end-of-life/index.html
index 1102f35..be700bd 100644
--- a/build-cache-node-end-of-life/index.html
+++ b/build-cache-node-end-of-life/index.html
@@ -119,31 +119,15 @@
What FosterStack Cache is today
It is early. The paid tiers are not built. Everything you can download today is the free core.
More than kept patched.
-
Security is what we are building this product around. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
-
Signed and verifiable
-
Signed releases. Images and the checksums file are signed with Sigstore, with no signing key stored anywhere. How to verify.
-
Build provenance. Every image and binary archive carries a signed statement naming the CI run that built it.
-
A bill of materials. Every binary archive ships with an SBOM.
-
Published vulnerability statements. A VEX file with each release says which known vulnerabilities do and do not affect it.
-
Reproducible. Each image is assembled a second time on a separate machine, and the digests must match or the release fails.
-
-
Supply chain
-
-
Pinned build steps. Every GitHub Action our releases use is pinned to an exact commit, and a check in CI enforces it on every change. The container base image is pinned by digest.
-
Hash-checked installs. Python tooling in our CI is installed with hash checking, so a swapped package is refused.
-
A 7-day wait. Our CI holds new dependency versions for 7 days before adopting them, and Dependabot has the same cooling-off period.
-
A daily supply-chain check. Once a day, everything our build and CI pull in (actions, tools, packages) is checked against public advisory databases and malicious-package reports. Any hit opens an issue.
-
Scanned before release. Two independent scanners with different databases check every release candidate, and any finding blocks the release unless a published VEX statement explains it. RELEASING.md.
-
Rescanned every day. Every published image is scanned again daily, so a new vulnerability in something we shipped raises an issue.
-
-
Small, quiet and patched
-
-
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
-
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
What you get that a bare HTTP endpoint doesn't give you
More than kept patched.
-
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
-
Signed and verifiable
-
Signed releases. Images and the checksums file are signed with Sigstore, with no signing key stored anywhere. How to verify.
-
Build provenance. Every image and binary archive carries a signed statement naming the CI run that built it.
-
A bill of materials. Every binary archive ships with an SBOM.
-
Published vulnerability statements. A VEX file with each release says which known vulnerabilities do and do not affect it.
-
Reproducible. Each image is assembled a second time on a separate machine, and the digests must match or the release fails.
-
-
Supply chain
-
-
Pinned build steps. Every GitHub Action our releases use is pinned to an exact commit, and a check in CI enforces it on every change. The container base image is pinned by digest.
-
Hash-checked installs. Python tooling in our CI is installed with hash checking, so a swapped package is refused.
-
A 7-day wait. Our CI holds new dependency versions for 7 days before adopting them, and Dependabot has the same cooling-off period.
-
A daily supply-chain check. Once a day, everything our build and CI pull in (actions, tools, packages) is checked against public advisory databases and malicious-package reports. Any hit opens an issue.
-
Scanned before release. Two independent scanners with different databases check every release candidate, and any finding blocks the release unless a published VEX statement explains it. RELEASING.md.
-
Rescanned every day. Every published image is scanned again daily, so a new vulnerability in something we shipped raises an issue.
-
-
Small, quiet and patched
-
-
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
-
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
We're a new vendor asking to sit in your build pipeline, so the burden of proof is on us.
diff --git a/migrate-build-cache-node/index.html b/migrate-build-cache-node/index.html
index e5db6d7..ab07c7c 100644
--- a/migrate-build-cache-node/index.html
+++ b/migrate-build-cache-node/index.html
@@ -170,31 +170,15 @@
Step 5: Retire the old node
Once builds hit the new server, stop the old Build Cache Node container.
More than kept patched.
-
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
-
Signed and verifiable
-
Signed releases. Images and the checksums file are signed with Sigstore, with no signing key stored anywhere. How to verify.
-
Build provenance. Every image and binary archive carries a signed statement naming the CI run that built it.
-
A bill of materials. Every binary archive ships with an SBOM.
-
Published vulnerability statements. A VEX file with each release says which known vulnerabilities do and do not affect it.
-
Reproducible. Each image is assembled a second time on a separate machine, and the digests must match or the release fails.
-
-
Supply chain
-
-
Pinned build steps. Every GitHub Action our releases use is pinned to an exact commit, and a check in CI enforces it on every change. The container base image is pinned by digest.
-
Hash-checked installs. Python tooling in our CI is installed with hash checking, so a swapped package is refused.
-
A 7-day wait. Our CI holds new dependency versions for 7 days before adopting them, and Dependabot has the same cooling-off period.
-
A daily supply-chain check. Once a day, everything our build and CI pull in (actions, tools, packages) is checked against public advisory databases and malicious-package reports. Any hit opens an issue.
-
Scanned before release. Two independent scanners with different databases check every release candidate, and any finding blocks the release unless a published VEX statement explains it. RELEASING.md.
-
Rescanned every day. Every published image is scanned again daily, so a new vulnerability in something we shipped raises an issue.
-
-
Small, quiet and patched
-
-
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
-
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
The old node had a small web page with a purge button. FosterStack Cache has no settings page. You set it with flags and environment variables, and a read-only status page shows size and hit counts. To empty the cache, remove its volume and start again.
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
-
Signed and verifiable
-
Signed releases. Images and the checksums file are signed with Sigstore, with no signing key stored anywhere. How to verify.
-
Build provenance. Every image and binary archive carries a signed statement naming the CI run that built it.
-
A bill of materials. Every binary archive ships with an SBOM.
-
Published vulnerability statements. A VEX file with each release says which known vulnerabilities do and do not affect it.
-
Reproducible. Each image is assembled a second time on a separate machine, and the digests must match or the release fails.
-
-
Supply chain
-
-
Pinned build steps. Every GitHub Action our releases use is pinned to an exact commit, and a check in CI enforces it on every change. The container base image is pinned by digest.
-
Hash-checked installs. Python tooling in our CI is installed with hash checking, so a swapped package is refused.
-
A 7-day wait. Our CI holds new dependency versions for 7 days before adopting them, and Dependabot has the same cooling-off period.
-
A daily supply-chain check. Once a day, everything our build and CI pull in (actions, tools, packages) is checked against public advisory databases and malicious-package reports. Any hit opens an issue.
-
Scanned before release. Two independent scanners with different databases check every release candidate, and any finding blocks the release unless a published VEX statement explains it. RELEASING.md.
-
Rescanned every day. Every published image is scanned again daily, so a new vulnerability in something we shipped raises an issue.
-
-
Small, quiet and patched
-
-
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
-
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
-
Patched for everyone. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier.
How our release pipeline maps to NIST SP 800-204C, 800-204D and SLSA
+
This page takes the release pipeline that built FosterStack Cache v0.2.1 and puts each stage next to the NIST practice it maps to. It says where a stage only partly implements a practice, and it says which SLSA Build level we assess v0.2.1 at.
+
Words used here: SBOM is a parts list of what is inside a build. VEX is a note saying a known flaw does not affect us. Provenance is a signed record of how and where something was built. Digest is the fingerprint of a file or image. SLSA is a public scale of how well a build can be trusted, from Level 1 to Level 4 in its Build track.
+
Guidance we designed against, not a certification we hold.
+
+
The two NIST documents
+
+
NIST SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh. We use its pipeline sections, mainly 4.6 "Securing the CI/CD Pipeline" and 4.8 "Security Testing".
+
NIST SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines. We use sections 4 and 5, mainly 5.1.1 "Secure Build" and 5.2 "Securing Workflows in CD Pipelines".
+
+
Parts of both documents are about running services in your own cluster: service mesh, runtime monitoring, deployment by GitOps. That part happens in your environment, not in our release pipeline, so this page does not map it.
+
+
Stage by stage: what runs, and which practice it maps to
+
This describes the workflow files as they were at the v0.2.1 tag. "Signs" means the stage writes a statement about its work and signs it with keyless signing tied to that stage's own workflow identity, logged in a public transparency log.
+
+
+
Stage
What it does in v0.2.1
NIST practice
How much
+
+
+
+
Source admission
+
Checks the tag and commit signatures against the allowed signers on main (or GitHub's own key for merges made on github.com), that the commit is on main, that every required check passed, and that an approved requirements list exists for the version. Signs.
+
800-204D 5.1.4 "Secure Code Commits" (commit-signing enforcement, COMMIT-REQ-2); 5.1.2 "Secure Pull-Push Operations on Repositories" (automated checks before code is trusted)
+
Partly. There is one maintainer, so no second person reviews or approves a change.
+
+
+
Build
+
Builds the binaries once on a fresh GitHub-hosted machine with the toolchain pinned, and checks that the dependency files did not change. Writes checksums and an SBOM per archive. Signs a build statement and SLSA provenance.
+
800-204D 5.1.1 "Secure Build"; 800-204C 3.3.3 (SBOM generation and code signing as pipeline tasks)
+
Partly. The signing runs in the same job as the build steps, not in a separate, more trusted process as 5.1.1 asks.
+
+
+
Image assembly
+
Checks every archive against the signed build statement before opening it. Adds one binary to a base image pinned by digest, with no commands run inside the image. Pushes by digest only, with no version tags, to a separate candidates package. Signs, recording the base digest and binary hashes, plus SLSA provenance.
+
800-204D 4.2: "The inputs and outputs of each build step should be verified"
+
Implements the practice.
+
+
+
Reproducibility check
+
Assembles the images again on a separate machine. The digests must match, or the release fails. Signs.
+
No direct match in either document.
+
Not mapped. It repeats the image assembly; it does not rebuild the binaries from source.
+
+
+
Scans
+
Two different scanners with different vulnerability databases, each on its own machine, check the image-assembly and reproducibility statements first, then scan every platform image of every variant. One also scans the archives, the other the Go module files. A finding at any severity blocks the release. For one of the two scanners, a published VEX statement can clear a finding. Each scanner signs its verdict for the exact digests.
+
800-204C 4.8 and 4.8.1 (container image scans, software composition analysis); 800-204D 5.2, DEPLOY_REQ-4 (evidence the image was scanned)
+
Partly. The release pipeline runs no static or dynamic application security testing itself. The signed verdict records a pass, not the full scan report.
+
+
+
Acceptance tests
+
Run against the exact candidate images: the Linux archives and binaries (macOS archives are checked, not run), the images on both CPU architectures, the image variants, the Gradle and Maven test suites, the Kubernetes setup from our docs, and a test that the server makes no outbound connections. A final job checks that every release-blocking acceptance test in the approved list passed, then signs one statement.
+
800-204C 3.3.1 (tests run in the pipeline; an organisation may choose to stop the pipeline when a test fails); 4.6 (stop further pipeline tasks if a test fails)
+
Implements the practice. The individual test jobs do not sign their own results.
+
+
+
Release authorization
+
Verifies every earlier statement: signed by the expected stage, naming this commit and these digests, scan verdicts pass, acceptance results complete. Anything missing stops the release. Records each statement's transparency-log entry. Signs the approval.
+
800-204C 4.6: "verify that all required signatures … are present to ensure that no one bypasses the pipeline"; 800-204D 5.1.1 (policy checks that the required attestations are found)
+
Partly. The policy is code in the workflow file, not a separate signed policy document as 5.1.1 describes.
+
+
+
Promotion and publish
+
Will not start without the signed approval. Copies the approved digests to GitHub's registry and the Docker Hub mirror with no rebuild, and checks the digests did not change. Signs the public images and the checksums file. Uploads the release files, re-checks the image signatures, the signed checksums and every tag the way a customer would, without credentials, then makes the release page public.
+
800-204D 5.2, DEPLOY-REQ-3 (only an artifact made by the approved build process is cleared)
+
Partly. We refuse to publish anything the approved build did not make. Enforcing that at deploy time is up to your own environment.
+
+
+
Signing, across all stages
+
Keyless signing: a short-lived signing identity per stage, no stored signing key, every signature logged publicly.
+
800-204C 4.6: "signing the release artifact during each required CI/CD stage (preferably multi-party signing)"; 800-204D 5.1.1 (attestations signed, stored tamper-proof)
+
Partly. Not multi-party: every signature is made by automation in one repository, run by one maintainer.
+
+
+
Evidence bundle
+
release-manifest.json lists every digest, the hash of the approved requirements list, per-test results, and the transparency-log entry of every statement the authorization verified. It is attached to each image in GitHub's registry as a signed statement and published with the SBOMs, VEX file and signed checksums.
+
800-204D 4 (record provenance, with a way to verify and authenticate it)
+
Implements the practice. Neither document has a section on VEX.
+
+
+
+
+
Which SLSA Build level does v0.2.1 meet?
+
We assess v0.2.1 at SLSA Build Level 2, against the SLSA v1.0 requirements. We do not claim Build Level 3.
+
+
Consistent build process (L1). Met. One workflow builds every release. Only a tag push starts it, and the first stage stops any tag not signed by an allowed signer.
+
Provenance exists (L1). Met. SLSA v1 provenance covers the binary archives, their SBOMs, checksums.txt, and the three container images, naming each digest, the source commit and the workflow run.
+
Provenance is distributed (L1). Met. It is in GitHub's attestation store, and gh attestation verify returns it.
+
Hosted build platform (L2). Met. The build ran on GitHub-hosted machines, never on a workstation.
+
Authentic provenance (L2). Met. The provenance is signed with an identity issued to the building workflow, and you can check the signature and the signing workflow yourself.
+
+
Level 3 is not claimed. It requires that the build steps cannot reach the material used to sign the provenance ("Unforgeable" and "Isolated" in the spec). In v0.2.1, the build steps and the provenance signing run in the same job, so that separation is not there. Build Level 3 is planned. It is not part of v0.2.1.
+
+
What is not met today
+
+
SLSA Build Level 3.
+
Evidence made by a separate, more trusted process than the build itself (800-204D 5.1.1).
+
A second person: one maintainer writes, approves and releases every change. No review by another person, and no multi-party signing (800-204D 5.1.2, 800-204C 4.6).
+
All signing is automated. 800-204D 5.1.3, written for software update systems, points the same way: multiple keys with threshold trust, and automated keys kept out of roles customers rely on.
+
The acceptance test jobs pass their results to the job that signs them; they do not sign or verify statements themselves.
+
No dynamic application security testing, and no static testing inside the release pipeline.
+
The release policy is workflow code, not a separate signed policy document.
+
The reproducibility check covers image assembly, not a second build of the binaries from source.
+
Promoted images are in the public registries a few steps before the final customer-style checks finish. Only the release page waits for those checks.
+
Scan verdicts are signed, but the full scan reports are not published as release files.
+
Checking provenance with gh attestation verify needs a GitHub token. The cosign checks need no account.
+
+
+
Check it yourself
+
This checks the SLSA provenance of the v0.2.1 image and the workflow that signed it:
Section numbers refer to the final published versions of NIST SP 800-204C and NIST SP 800-204D. Guidance we designed against, not a certification we hold.
What you get that a bare HTTP endpoint doesn't give you
Authentication
HTTP Basic Auth over TLS, wired the way Gradle and Maven already expect credentials. Constant-time comparison, no credential ever logged.
Metrics & status
Prometheus metrics, a read-only status page with hit rates and size-vs-cap, and a Grafana dashboard in the repo — so you know the cache is earning its keep.
Maintenance target
Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
-
Acceptance-tested
Every change runs against a real multi-module Gradle build in CI: a from-scratch second build must produce real remote-cache hits, or the change does not merge.
+
Designed against NIST guidance
Our release pipeline is designed against NIST SP 800-204C and 800-204D: each stage's result is recorded in a signed statement, and nothing is promoted to the public registries until a signed release authorization has verified every earlier statement. Guidance we designed against, not a certification we hold. See how each stage maps.
30-minute migration
A step-by-step guide for existing Build Cache Node deployments. Same protocol, same CI config shape — migrating is mostly a URL change.
How our release pipeline maps to NIST SP 800-204C, 800-204D and SLSA
This page takes the release pipeline that built FosterStack Cache v0.2.1 and puts each stage next to the NIST practice it maps to. It says where a stage only partly implements a practice, and it says which SLSA Build level we assess v0.2.1 at.
-
Words used here: SBOM is a parts list of what is inside a build. VEX is a note saying a known flaw does not affect us. Provenance is a signed record of how and where something was built. Digest is the fingerprint of a file or image. SLSA is a public scale of how well a build can be trusted, from Level 1 to Level 4 in its Build track.
+
Words used here: SBOM is a parts list of what is inside a build. VEX is a note saying a known flaw does not affect us. Provenance is a signed record of how and where something was built. Digest is the fingerprint of a file or image. SLSA is a public scale of how well a build can be trusted, from Level 1 to Level 3 in its Build track.
Guidance we designed against, not a certification we hold.
Gradle build cache for a monorepo: what a multi-module run shows
+
A monorepo is many Gradle modules in one build. A shared cache server lets CI and every developer reuse each module's work. Below is a real run with four modules against FosterStack Cache 0.2.1: which modules came from the cache, and which did not.
+
+
The short answer
+
+
Change one module's code and the other modules still come from the cache.
+
Change a module's public methods and every module that uses it rebuilds.
+
A teammate who makes the same change gets everything from the cache.
+
+
+
What we ran
+
A build with four modules. core has no dependencies. util and api use core. app uses util and api. Each module has 12 small Java classes. The cache server was release 0.2.1 (macOS, arm64), with Gradle 9.8.0.
+
Every run below used a brand-new copy of the project and an empty Gradle home folder, and the local cache was switched off. The only thing the runs shared was the cache server. Each run was gradle build --console=plain --no-daemon, and every build finished with BUILD SUCCESSFUL.
+
+
Results: what each module's compile step did
+
+
Run
core
util
api
app
+
+
1. First build, as CI would do it
ran
ran
ran
ran
+
2. Another machine, same code
from cache
from cache
from cache
from cache
+
3. Edit inside a method in core
ran
from cache
from cache
from cache
+
4. A teammate makes the same edit
from cache
from cache
from cache
from cache
+
5. Add a public method to core
ran
ran
ran
ran
+
6. Edit only app
from cache
from cache
from cache
ran
+
+
+
"Ran" means Gradle did the work and uploaded the result. "From cache" means Gradle downloaded it instead, and printed FROM-CACHE next to the task. This is what run 3 printed:
Edits inside a method stay local (run 3). Gradle compares the public shape of a module, not every byte, when it decides whether a module that depends on it must rebuild. A change inside a method body left the other three modules' inputs the same, so they came from the cache.
+
Changes to public methods spread (run 5). All four modules ran. In a big monorepo, a small, stable shared module with few public changes keeps more of the build cached.
+
The first person pays, everyone else downloads (runs 4 and 6). The edit in run 3 was uploaded once. In run 4 a fresh checkout made the same edit and ran nothing.
+
+
Only the compile step is shown here, because it is the step that showed the pattern clearly in a small project. The jar steps in this build are not cacheable by default in Gradle, so they ran every time. The project is small, so we are not quoting time saved: the cache pays off as your modules get bigger.
+
+
Set it up
+
This is the exact settings.gradle.kts we ran. Replace the address with your server's. We used 127.0.0.1 over plain HTTP because the test ran on one machine; use HTTPS across a network.