Skip to content

Commit fa89275

Browse files
committed
Initial build secrets blog post for Pro Builder
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
1 parent a387018 commit fa89275

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
---
2+
title: "Encrypt build-time secrets for the Function Builder"
3+
description: "Learn how to pass private registry tokens and credentials into the Function Builder, encrypted end-to-end."
4+
date: 2026-03-24
5+
categories:
6+
- kubernetes
7+
- faas
8+
- functions
9+
- builder
10+
- enterprise
11+
dark_background: true
12+
author_staff_member: alex
13+
hide_header_image: true
14+
---
15+
16+
Learn how to pass private registry tokens, API keys, and certificates into the Function Builder - encrypted end-to-end.
17+
18+
## Introduction
19+
20+
Build secrets are already supported for [local builds and CI jobs](https://docs.openfaas.com/cli/build/#plugins-and-build-time-secrets) using `faas-cli pro build`. In that workflow, the secret files live on the build machine and are mounted directly into Docker's BuildKit. There's no network transport involved.
21+
22+
The [Function Builder API](https://docs.openfaas.com/openfaas-pro/builder/) is different. It's designed for building untrusted code from third parties - your customers. A SaaS platform takes user-supplied source code, sends it to the builder over HTTP, and gets back a container image. The build happens in-cluster, without Docker, without root, and without sharing a Docker socket.
23+
24+
```
25+
Kubernetes cluster
26+
┌──────────────────────────────┐
27+
faas-cli / │ │
28+
Your API/dashboard │ pro-builder buildkit │ registry
29+
┌───────────────┐ │ ┌──────────┐ ┌──────────┐ │ ┌─────────┐
30+
│ source code │──tar──│─▶│ unseal │──│ build │──│─▶│ image │
31+
│ + sealed │ HTTP │ │ secrets │ │ + push │ │ │ │
32+
│ secrets │ HMAC │ └──────────┘ └──────────┘ │ └─────────┘
33+
└───────────────┘ │ │
34+
└──────────────────────────────┘
35+
```
36+
37+
The question is: what happens when those builds need access to private resources? A Python function might need to `pip install` from a private PyPI registry. A Node.js function might need packages from a private npm registry. A function might need a private CA certificate to pull dependencies from an internal mirror.
38+
39+
Since the Function Builder launched, most customers haven't needed build-time credentials - Go users vendor their dependencies, and many teams use public registries. Others have found workarounds where they could. But as platforms mature and customer requirements evolve, the need for private package registries comes up.
40+
41+
[Waylay.io](https://waylay.io) has been using the Function Builder since 2021 to build functions for their industrial IoT and automation platform. As their customers started needing pip modules from private registries, they reached out and we worked together to develop a proper solution. Build secrets use Docker's `--mount=type=secret` mechanism, which means credentials are only available during the specific `RUN` instruction that needs them - they never end up in image layers and they're not visible in `docker history`. We added NaCl box encryption (Curve25519 + XSalsa20-Poly1305) on top so that secrets are protected over the wire between the client and the builder, even over plain HTTP.
42+
43+
The result is a new feature in the Function Builder that lets you pass secrets into `RUN --mount=type=secret` instructions in your Dockerfiles. The secrets are encrypted client-side by `faas-cli` using the builder's public key, included in the build tar, and decrypted in-memory by the builder just before the build runs. They never appear in image layers, they're never written to disk in plaintext, and they never travel in plaintext over the wire - even if the connection between your client and the builder is plain HTTP.
44+
45+
## How it works
46+
47+
The builder generates a Curve25519 keypair at startup. The public key is available via a `/publickey` endpoint. When `faas-cli` sends a build with secrets, it:
48+
49+
1. Encrypts each secret value independently using NaCl box
50+
2. Includes the sealed secrets in the build tar as `com.openfaas.secrets`
51+
3. Signs the entire tar with HMAC-SHA256 (as before)
52+
53+
The builder receives the tar, validates the HMAC, extracts the sealed file, decrypts each value using its private key, and passes them to BuildKit as `--mount=type=secret` mounts. After the build, the decrypted values are discarded.
54+
55+
The sealed file format uses per-value encryption with visible key names, so you can see which secrets are included without being able to read their values:
56+
57+
```yaml
58+
version: v1
59+
algorithm: nacl/box
60+
key_id: TrZKmwyy
61+
public_key: TrZKmwyyTHBflZBF98y/j/2vn8wDZsMkX7yvUUGLUUM=
62+
secrets:
63+
api_key: <encrypted>
64+
pip_index_url: <encrypted>
65+
```
66+
67+
This means the file is safe to commit to git. You get an audit trail of which keys were added or removed, and you can see when a value has changed by its ciphertext - all without needing the private key.
68+
69+
## Part A: Setting up the builder with build secrets
70+
71+
The following steps let you try the full workflow on a local KinD cluster before moving to a live environment. You'll need `faas-cli` 0.18.6 or later, `helm`, `kubectl`, `kind`, and an OpenFaaS for Enterprises license.
72+
73+
### Create a test cluster
74+
75+
```bash
76+
kind create cluster --name build-secrets-test
77+
```
78+
79+
### Create the namespace and license secret
80+
81+
```bash
82+
kubectl create namespace openfaas
83+
84+
kubectl create secret generic openfaas-license \
85+
-n openfaas \
86+
--from-file license=$HOME/.openfaas/LICENSE
87+
```
88+
89+
### Create a registry credential secret
90+
91+
For testing, we'll use [ttl.sh](https://ttl.sh) which is a free ephemeral registry that doesn't require authentication:
92+
93+
```bash
94+
cat <<'EOF' > ttlsh-config.json
95+
{"auths":{}}
96+
EOF
97+
98+
kubectl create secret generic registry-secret \
99+
-n openfaas \
100+
--from-file config.json=./ttlsh-config.json
101+
```
102+
103+
For a private registry, see the [helm chart README](https://github.com/openfaas/faas-netes/tree/master/chart/pro-builder) for how to configure authentication.
104+
105+
### Generate secrets
106+
107+
Two things are needed: a keypair for encrypting build secrets, and a payload secret for HMAC request signing.
108+
109+
```bash
110+
faas-cli secret keygen
111+
faas-cli secret generate -o payload.txt
112+
```
113+
114+
```
115+
Wrote private key: key
116+
Wrote public key: key.pub
117+
Key ID: TrZKmwyy
118+
```
119+
120+
### Create the Kubernetes secrets
121+
122+
```bash
123+
kubectl create secret generic -n openfaas \
124+
payload-secret --from-file payload-secret=payload.txt
125+
126+
kubectl create secret generic -n openfaas \
127+
pro-builder-build-secrets-key --from-file key=./key
128+
```
129+
130+
### Deploy the builder
131+
132+
```bash
133+
helm repo add openfaas https://openfaas.github.io/faas-netes/
134+
helm repo update
135+
136+
helm upgrade pro-builder openfaas/pro-builder \
137+
--install -n openfaas \
138+
--set buildSecrets.privateKeySecret=pro-builder-build-secrets-key
139+
```
140+
141+
Wait for it to be ready:
142+
143+
```bash
144+
kubectl rollout status deployment/pro-builder -n openfaas
145+
```
146+
147+
### Verify
148+
149+
Port-forward and check the public key endpoint:
150+
151+
```bash
152+
kubectl port-forward -n openfaas deploy/pro-builder 8081:8080 &
153+
154+
curl -s http://127.0.0.1:8081/publickey | jq
155+
```
156+
157+
```json
158+
{
159+
"key_id": "TrZKmwyy",
160+
"algorithm": "nacl/box",
161+
"public_key": "TrZKmwyyTHBflZBF98y/j/2vn8wDZsMkX7yvUUGLUUM="
162+
}
163+
```
164+
165+
The `key_id` is derived from the public key automatically. You don't need to configure it. The builder is ready.
166+
167+
## Part B: Building a function with secrets
168+
169+
Let's walk through a complete example. We'll create a function that reads a secret at build time using the classic watchdog.
170+
171+
### Create the function
172+
173+
```bash
174+
faas-cli new --prefix ttl.sh/test-build-secrets \
175+
--lang dockerfile sealed-test
176+
```
177+
178+
Replace `sealed-test/Dockerfile` with:
179+
180+
```Dockerfile
181+
FROM ghcr.io/openfaas/classic-watchdog:latest AS watchdog
182+
183+
FROM alpine:3.22.0
184+
185+
COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
186+
187+
RUN mkdir -p /home/app
188+
189+
RUN --mount=type=secret,id=api_key \
190+
cat /run/secrets/api_key > /home/app/api_key.txt
191+
192+
ENV fprocess="cat /home/app/api_key.txt"
193+
194+
CMD ["fwatchdog"]
195+
```
196+
197+
The `--mount=type=secret,id=api_key` line tells BuildKit to mount the secret at `/run/secrets/api_key` during that `RUN` step. It's only available during the build - it doesn't end up in any image layer.
198+
199+
Edit `stack.yaml` to add `build_secrets`:
200+
201+
```yaml
202+
version: 1.0
203+
provider:
204+
name: openfaas
205+
gateway: http://127.0.0.1:8080
206+
functions:
207+
sealed-test:
208+
lang: dockerfile
209+
handler: ./sealed-test
210+
image: ttl.sh/test-build-secrets/sealed-test:2h
211+
build_secrets:
212+
api_key: sk-live-my-secret-key
213+
```
214+
215+
### Build with the remote builder
216+
217+
If you don't already have the payload secret file locally, fetch it from the cluster:
218+
219+
```bash
220+
export PAYLOAD=$(kubectl get secret -n openfaas payload-secret \
221+
-o jsonpath='{.data.payload-secret}' | base64 --decode)
222+
echo $PAYLOAD > payload.txt
223+
```
224+
225+
If you don't have the public key file, fetch it from the builder:
226+
227+
```bash
228+
curl -s http://127.0.0.1:8081/publickey | jq -r '.public_key' > key.pub
229+
```
230+
231+
Then publish:
232+
233+
```bash
234+
faas-cli publish \
235+
-f stack.yaml \
236+
--remote-builder http://127.0.0.1:8081 \
237+
--payload-secret ./payload.txt \
238+
--builder-public-key ./key.pub
239+
```
240+
241+
The secrets are encrypted by `faas-cli` before sending. You'll see the build logs streamed back:
242+
243+
```
244+
[0] > Building sealed-test.
245+
Building: ttl.sh/test-build-secrets/sealed-test:2h with dockerfile template. Please wait..
246+
2026-03-24T11:15:13Z [stage-1 2/4] COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
247+
2026-03-24T11:15:13Z [stage-1 3/4] RUN mkdir -p /home/app
248+
2026-03-24T11:15:13Z [stage-1 4/4] RUN --mount=type=secret,id=api_key ...
249+
2026-03-24T11:15:14Z exporting to image
250+
sealed-test success building and pushing image: ttl.sh/test-build-secrets/sealed-test:2h
251+
```
252+
253+
### Verify
254+
255+
Run the image and invoke the watchdog:
256+
257+
```bash
258+
docker run --rm -d -p 8081:8080 --name sealed-test \
259+
ttl.sh/test-build-secrets/sealed-test:2h
260+
261+
curl -s http://127.0.0.1:8081
262+
263+
docker stop sealed-test
264+
```
265+
266+
```
267+
sk-live-my-secret-key
268+
```
269+
270+
The secret was encrypted on the client, sent over the wire inside the build tar, decrypted by the builder, and mounted into the Dockerfile during the build.
271+
272+
### A real-world example: private PyPI registry
273+
274+
In production, you'd use this to pass credentials for private package registries. Here's what that would look like for a Python function using the `python3-http` template.
275+
276+
In your `stack.yaml`:
277+
278+
```yaml
279+
functions:
280+
data-processor:
281+
lang: python3-http
282+
handler: ./data-processor
283+
image: registry.example.com/data-processor:latest
284+
build_secrets:
285+
pip_index_url: https://token:pypi-secret@my-org.jfrog.io/artifactory/api/pypi/python-local/simple
286+
```
287+
288+
Then in the template's Dockerfile, you'd change the `pip install` line to mount the secret:
289+
290+
```diff
291+
-RUN pip install --no-cache-dir --user -r requirements.txt
292+
+RUN --mount=type=secret,id=pip_index_url \
293+
+ pip install --no-cache-dir --user \
294+
+ --index-url "$(cat /run/secrets/pip_index_url)" \
295+
+ -r requirements.txt
296+
```
297+
298+
The same pattern works for npm, Go private modules, or any package manager that takes credentials at install time.
299+
300+
Binary values like CA certificates are also supported. You can seal them from files instead of literals:
301+
302+
```bash
303+
faas-cli secret seal key.pub \
304+
--from-file ca.crt=./certs/internal-ca.crt \
305+
--from-literal pip_index_url=https://token:secret@registry.example.com/simple
306+
```
307+
308+
## Sealing secrets for CI pipelines
309+
310+
If you're integrating with a CI system rather than using `faas-cli publish` directly, you can seal secrets into a file ahead of time:
311+
312+
```bash
313+
faas-cli secret seal key.pub \
314+
--from-literal api_key=sk-live-my-secret-key
315+
```
316+
317+
This writes `com.openfaas.secrets` in the current directory. Include it in the build tar alongside `com.openfaas.docker.config` and the `context/` folder, and the builder will pick it up.
318+
319+
You can inspect a sealed file without the builder:
320+
321+
```bash
322+
faas-cli secret unseal key
323+
```
324+
325+
```
326+
api_key=sk-live-my-secret-key
327+
```
328+
329+
## New faas-cli commands
330+
331+
We've added four new subcommands to `faas-cli secret`:
332+
333+
| Command | Purpose |
334+
|---------|---------|
335+
| `faas-cli secret keygen` | Generate a Curve25519 keypair |
336+
| `faas-cli secret generate` | Generate a random secret value for the pro-builder's HMAC signing key |
337+
| `faas-cli secret seal key.pub --from-literal k=v` | Seal secrets into `com.openfaas.secrets` |
338+
| `faas-cli secret unseal key` | Decrypt and inspect a sealed file (requires access to the private key) |
339+
340+
## Wrapping up
341+
342+
Build secrets for local builds and CI have been available for a while via `faas-cli pro build`. This feature brings the same capability to the Function Builder API, where builds happen in-cluster on behalf of third-party users and the secrets need to be protected over the wire.
343+
344+
We developed this together with [Waylay](https://waylay.io) based on their production requirements, using NaCl box encryption to protect secrets over the wire. The `seal` package in the [Go SDK](https://github.com/openfaas/go-sdk) is generic and could be reused for other use-cases in the future.
345+
346+
If you're already using the Function Builder, you can start using build secrets by upgrading the helm chart and `faas-cli`. If you're new to the builder, see the [Function Builder API docs](https://docs.openfaas.com/openfaas-pro/builder/) for the full setup guide.
347+
348+
If you have questions, feel free to [reach out to us](https://openfaas.com/pricing).
349+
350+
### See also
351+
352+
* [Function Builder API docs](https://docs.openfaas.com/openfaas-pro/builder/)
353+
* [Go SDK `seal` package](https://github.com/openfaas/go-sdk/tree/master/seal)
354+
* [Pro-builder Helm chart](https://github.com/openfaas/faas-netes/tree/master/chart/pro-builder)
355+
* [How to Build Functions with the Go SDK for OpenFaaS](https://www.openfaas.com/blog/building-functions-via-api-golang/)

0 commit comments

Comments
 (0)