|
| 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