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/suball 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
NetworkPolicythat 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 configprints 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