Learn how to pass private registry tokens, API keys, and certificates into the Function Builder - encrypted end-to-end.
Introduction
Build secrets are already supported for local builds and CI jobs 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.
The Function Builder API 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.
Kubernetes cluster
┌──────────────────────────────┐
faas-cli / │ │
Your API/dashboard │ pro-builder buildkit │ registry
┌───────────────┐ │ ┌──────────┐ ┌──────────┐ │ ┌─────────┐
│ source code │──tar──│─▶│ unseal │──│ build │──│─▶│ image │
│ + sealed │ HTTP │ │ secrets │ │ + push │ │ │ │
│ secrets │ HMAC │ └──────────┘ └──────────┘ │ └─────────┘
└───────────────┘ │ │
└──────────────────────────────┘
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.
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.
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.
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.
How it works
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:
- Encrypts each secret value independently using NaCl box
- Includes the sealed secrets in the build tar as
com.openfaas.secrets - Signs the entire tar with HMAC-SHA256 (as before)
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.
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:
version: v1
algorithm: nacl/box
key_id: TrZKmwyy
public_key: TrZKmwyyTHBflZBF98y/j/2vn8wDZsMkX7yvUUGLUUM=
secrets:
api_key: <encrypted>
pip_index_url: <encrypted>
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.
Part A: Setting up the builder with build secrets
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.
Create a test cluster
kind create cluster --name build-secrets-test
Create the namespace and license secret
kubectl create namespace openfaas
kubectl create secret generic openfaas-license \
-n openfaas \
--from-file license=$HOME/.openfaas/LICENSE
Create a registry credential secret
For testing, we’ll use ttl.sh which is a free ephemeral registry that doesn’t require authentication:
cat <<'EOF' > ttlsh-config.json
{"auths":{}}
EOF
kubectl create secret generic registry-secret \
-n openfaas \
--from-file config.json=./ttlsh-config.json
For a private registry, see the helm chart README for how to configure authentication.
Generate secrets
Two things are needed: a keypair for encrypting build secrets, and a payload secret for HMAC request signing.
faas-cli secret keygen
faas-cli secret generate -o payload.txt
Wrote private key: key
Wrote public key: key.pub
Key ID: TrZKmwyy
Create the Kubernetes secrets
kubectl create secret generic -n openfaas \
payload-secret --from-file payload-secret=payload.txt
kubectl create secret generic -n openfaas \
pro-builder-build-secrets-key --from-file key=./key
Deploy the builder
helm repo add openfaas https://openfaas.github.io/faas-netes/
helm repo update
helm upgrade pro-builder openfaas/pro-builder \
--install -n openfaas \
--set buildSecrets.privateKeySecret=pro-builder-build-secrets-key
Wait for it to be ready:
kubectl rollout status deployment/pro-builder -n openfaas
Verify
Port-forward and check the public key endpoint:
kubectl port-forward -n openfaas deploy/pro-builder 8081:8080 &
curl -s http://127.0.0.1:8081/publickey | jq
{
"key_id": "TrZKmwyy",
"algorithm": "nacl/box",
"public_key": "TrZKmwyyTHBflZBF98y/j/2vn8wDZsMkX7yvUUGLUUM="
}
The key_id is derived from the public key automatically. You don’t need to configure it. The builder is ready.
Part B: Building a function with secrets
Let’s walk through a complete example. We’ll create a function that reads a secret at build time using the classic watchdog.
Create the function
faas-cli new --prefix ttl.sh/test-build-secrets \
--lang dockerfile sealed-test
Replace sealed-test/Dockerfile with:
FROM ghcr.io/openfaas/classic-watchdog:latest AS watchdog
FROM alpine:3.22.0
COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
RUN mkdir -p /home/app
RUN --mount=type=secret,id=api_key \
cat /run/secrets/api_key > /home/app/api_key.txt
ENV fprocess="cat /home/app/api_key.txt"
CMD ["fwatchdog"]
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.
Edit stack.yaml to add build_secrets:
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
sealed-test:
lang: dockerfile
handler: ./sealed-test
image: ttl.sh/test-build-secrets/sealed-test:2h
build_secrets:
api_key: sk-live-my-secret-key
Build with the remote builder
If you don’t already have the payload secret file locally, fetch it from the cluster:
export PAYLOAD=$(kubectl get secret -n openfaas payload-secret \
-o jsonpath='{.data.payload-secret}' | base64 --decode)
echo $PAYLOAD > payload.txt
If you don’t have the public key file, fetch it from the builder:
curl -s http://127.0.0.1:8081/publickey | jq -r '.public_key' > key.pub
Then publish:
faas-cli publish \
-f stack.yaml \
--remote-builder http://127.0.0.1:8081 \
--payload-secret ./payload.txt \
--builder-public-key ./key.pub
The secrets are encrypted by faas-cli before sending. You’ll see the build logs streamed back:
[0] > Building sealed-test.
Building: ttl.sh/test-build-secrets/sealed-test:2h with dockerfile template. Please wait..
2026-03-24T11:15:13Z [stage-1 2/4] COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
2026-03-24T11:15:13Z [stage-1 3/4] RUN mkdir -p /home/app
2026-03-24T11:15:13Z [stage-1 4/4] RUN --mount=type=secret,id=api_key ...
2026-03-24T11:15:14Z exporting to image
sealed-test success building and pushing image: ttl.sh/test-build-secrets/sealed-test:2h
Verify
Run the image and invoke the watchdog:
docker run --rm -d -p 8081:8080 --name sealed-test \
ttl.sh/test-build-secrets/sealed-test:2h
curl -s http://127.0.0.1:8081
docker stop sealed-test
sk-live-my-secret-key
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.
A real-world example: private PyPI registry
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.
In your stack.yaml:
functions:
data-processor:
lang: python3-http
handler: ./data-processor
image: registry.example.com/data-processor:latest
build_secrets:
pip_index_url: https://token:pypi-secret@my-org.jfrog.io/artifactory/api/pypi/python-local/simple
Then in the template’s Dockerfile, you’d change the pip install line to mount the secret:
-RUN pip install --no-cache-dir --user -r requirements.txt
+RUN --mount=type=secret,id=pip_index_url \
+ pip install --no-cache-dir --user \
+ --index-url "$(cat /run/secrets/pip_index_url)" \
+ -r requirements.txt
The same pattern works for npm, Go private modules, or any package manager that takes credentials at install time.
Binary values like CA certificates are also supported. You can seal them from files instead of literals:
faas-cli secret seal key.pub \
--from-file ca.crt=./certs/internal-ca.crt \
--from-literal pip_index_url=https://token:secret@registry.example.com/simple
Sealing secrets for CI pipelines
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:
faas-cli secret seal key.pub \
--from-literal api_key=sk-live-my-secret-key
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.
You can inspect a sealed file without the builder:
faas-cli secret unseal key
api_key=sk-live-my-secret-key
New faas-cli commands
We’ve added four new subcommands to faas-cli secret:
| Command | Purpose |
|---|---|
faas-cli secret keygen |
Generate a Curve25519 keypair |
faas-cli secret generate |
Generate a random secret value for the pro-builder’s HMAC signing key |
faas-cli secret seal key.pub --from-literal k=v |
Seal secrets into com.openfaas.secrets |
faas-cli secret unseal key |
Decrypt and inspect a sealed file (requires access to the private key) |
Wrapping up
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.
We developed this together with Waylay based on their production requirements, using NaCl box encryption to protect secrets over the wire. The seal package in the Go SDK is generic and could be reused for other use-cases in the future.
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 for the full setup guide.
If you have questions, feel free to reach out to us.