Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Securing a deployment

What the runtime guarantees, what it leaves to you, and where the boundaries actually are.

What the runtime handles

  • Access control, compiled into every query. There is no code path that can forget a row constraint or return a field a role cannot read.
  • Token verification — asymmetric algorithms only, exp/iss/aud/sub all required, keys discovered from the issuer and cached.
  • The script sandbox — no filesystem, no network, no process, and an instruction and memory budget.
  • SQL injection, structurally: every identifier is quoted by the query builder and every value is a bound parameter. Entity and attribute names come from customer YAML, so this is not theoretical.

What you handle

The admin port

The single most important thing on this page. Port 9090 carries no authentication — health, introspection, and an endpoint that reloads the model. The only thing making that acceptable is that nothing routes to it.

  • Do not put it behind an Ingress, a Service of type LoadBalancer, or a hostPort.
  • It defaults to loopback. You have to widen it deliberately to run in a container; widen it to the pod, not to the network.
  • A NetworkPolicy that admits only the kubelet is worth writing.

Reload takes no request body precisely because of this: it re-reads a mounted path, so the worst an attacker on that port can do is make the runtime re-read a file they cannot change. That is a small blast radius by construction rather than by policy — but it is not zero, and it depends on the port staying unroutable.

TLS

The runtime speaks plain HTTP. Terminate TLS at your ingress, and use TLS to PostgreSQL — sslmode=require at minimum, verify-full if you can:

RUNTIME_DATABASE_URL=postgres://user:pass@host/db?sslmode=verify-full

Secrets

RUNTIME_DATABASE_URL contains a password. It arrives as an environment variable, so:

  • Mount it from a secret store rather than baking it into a manifest.
  • The runtime never logs it. Error messages name the variable, never the value, because an error message is the easiest way for a secret to reach a log aggregator.
  • runtime config prints the effective configuration — including that URL. It is a debugging command, not something to wire into a dashboard.

The model is code

A model bundle contains flow scripts, and scripts execute. Whoever can write to the mounted bundle can run code in the runtime’s process, as whoever calls the flow.

Treat the bundle as you would a deployment artefact: version controlled, reviewed, and mounted read-only. Do not give an application’s users write access to the volume it is served from.

Identity

Configure both RUNTIME_OIDC_ISSUER and RUNTIME_OIDC_AUDIENCE. The audience check is not decoration — without it, a token minted for any other service at the same issuer is accepted here.

Set auth.anonymousRole in the model only if you mean it. It is how a public read-only surface is expressed, and it is also how an application accidentally becomes public.

The blast radius of a sandbox escape

Worth stating plainly, because it is smaller than people assume and not zero.

A flow runs as its caller, so a script has no authority its caller lacks. An escape from the Lua sandbox gets an attacker what that caller could already have done through the REST API — not the database, and not other tenants’ data.

That is a reason the design is what it is, not a reason to relax the sandbox.

What is genuinely missing

Be aware of these before putting this in front of untrusted users:

  • No rate limiting and no per-caller quota. A body limit and a request timeout exist; nothing bounds how many requests one caller may make.
  • Single tenant. One model and one database per deployment. Isolation between customers is deployment-level, not runtime-level.
  • No audit log retention or tamper-evidence. The audit stream goes to stdout like everything else. Retention and integrity are your log pipeline’s job.
  • No hard CPU bound on a script. The instruction budget stops interpreted loops and the memory cap stops the realistic runaways, but a script inside a single long-running C function cannot be interrupted.

A checklist

[ ] Admin port not routable, NetworkPolicy in place
[ ] TLS terminated at the ingress; sslmode=verify-full to the database
[ ] Database credentials from a secret store, not a manifest
[ ] OIDC issuer and audience both set
[ ] anonymousRole absent, or deliberate
[ ] Model bundle mounted read-only, from a reviewed artefact
[ ] terminationGracePeriodSeconds above 35
[ ] Rate limiting at the ingress, since the runtime has none