The console checks every rule before anything runs, and admission policies in the cluster check them again on every step's Job. This page is the short version of the security model: what's enforced, by which layer, and what isn't yet.
Every run passes four places on its way to a Pod. Each refusal comes back on the step as a code that says why, and the ones in the cluster are non-retryable.
Within a team, secrets are shared on purpose: the team chooses which step gets which. Between teams, and between reviewed and unreviewed code, nothing is.
ValidatingAdmissionPolicies installed by the charts, plus Sigstore's policy-controller where you want signatures. The console keeps their lists of products, secrets and digests current.
| Policy | What it refuses | Refusal |
|---|---|---|
| dispatch-policy.yaml | A Job outside a namespace labelled vennir/team=true, or in the platform's own; a Job that isn't hardened while taskPodHardening is on | DISPATCH_REJECTED |
| image-policy.yaml | An image outside the source registry set at install and the sources admins add | IMAGE_NOT_ALLOWED |
| scan-policy.yaml | An image not pinned by digest, or a digest that failed its scan, unless an admin accepted it | IMAGE_NOT_PINNED IMAGE_SCAN_FAILED |
| identity-policy.yaml | Any ServiceAccount but default unless it's the registered product's own; any Secret not registered for its team; Secrets any way but a volume; a token for an audience the product didn't declare | IDENTITY_NOT_ALLOWED |
| image-signature.yaml | An image catalog CI didn't sign (a generated key, a cloud KMS key, or keyless), where policy-controller is installed | IMAGE_NOT_SIGNED |
| purge-policy.yaml | A purger deleting a team namespace that isn't labelled retired. None is configured by default, so whoever retires a namespace can't also delete it | refused delete |
| namespace-policy.yaml | The console making or adopting a namespace that isn't a team's, changing a team namespace beyond its team label, or touching kube-* or the platform's own | refused write |
| write-fence-policy.yaml | The console writing Secrets, ServiceAccounts, RBAC, ExternalSecrets, NetworkPolicies or quotas outside team namespaces, or in any shape but its own | refused write |
| console-jobs-policy.yaml | A Job the console makes in the platform's namespace that isn't one of its image scans or catalog verifications as it builds them: another image, identity, Secret or token, root, or anything from the node | refused Job |
The dispatch, image, scan and identity policies check the scheduler's requests only. Anyone else your cluster lets create Pods in a team namespace, such as a cluster administrator, meets only Pod Security and, where installed, policy-controller.
All of it needs a network plugin that enforces NetworkPolicy: Calico, Cilium, GKE Dataplane V2, or on EKS the VPC CNI's policy agent, which vennir turns on.
Every team namespace holds vennir-default-deny: its Pods accept connections only from their own namespace, and from the API gateway to the backends a team publishes.
Only the platform's own Pods reach its Postgres, and only the console reaches kernel-api. A step reaches neither, nor the scanner, Loki or any metrics port.
On GKE, Workload Identity's: a step gets only its own ServiceAccount's Google identity. On EKS, vennir-no-metadata blocks 169.254.169.254. Elsewhere, it's yours to close.
On by default: each chart makes its own CA, kernel-api serves only HTTPS, Postgres accepts only TLS, and every client checks the server with verify-full. Metrics, scanner and Loki ports stay plain, fenced by policy.
The console writes Secrets, product ServiceAccounts, ExternalSecrets, SecretStores and each team's network policy through one cluster-wide role. The write fence confines that role to team namespaces. The console can't change the fence, checks it's in force before it starts, and refuses to start without it.
Its only RBAC write is one fixed RoleBinding per team namespace, so the scheduler can read its steps' logs there.
A session only says who you are. Teams, roles and admin are read from the database on every request, so removing someone takes effect at once.
An image is the same secret-free bytes for every team. What a step authenticates with is attached when it runs, and it's short-lived wherever the target supports it.
| Who | Signs in to | With |
|---|---|---|
| A step | Its cloud | Its product's own ServiceAccount: GKE Workload Identity, EKS Pod Identity, or an hour's token per declared audience for another cloud |
| Catalog CI | The registry, and the signing key | GitHub's OIDC token, trusted only for the repository's main; no console or cluster credential at all |
| A team's vault | GCP Secret Manager, AWS Secrets Manager | External Secrets Operator as the team's own workload identity; one cloud identity per team's store |
| The platform | A managed database | IAM database authentication: each service as its own identity, a short-lived token, no password of Vennir's |
| Another install | Your catalog's registry | vennir catalog grant, naming exactly its identity; never a key |
| The installer | Your cloud and git host | Your own sign-ins; vennir holds no credentials, and its config file holds only names |
Each one is tracked in TODO.md and described in the security model.
Kubernetes can't guard an admission policy from its editor, so the console could allow any image, ServiceAccount or Secret. Accepted for now: treat the console as the most privileged component.
Someone who leaves a group the provider reports in its token stays in its team until the session ends. Google Workspace groups are read again every hour. Shorten JWT_TTL where that matters.
Who looked at a run's outputs or logs, or a team's secrets' names, isn't recorded. Not decided yet.
It lives in the console's database. Someone who can write there could rewrite the chain after a change. Turn on audit.export, or ship the audit log lines to a SIEM.
With taskPodHardening: false, only Pod Security holds a step, and only in namespaces the console created; in an adopted one it runs as root with an API token.
Every team named in a raw DAG sees all its steps' outputs. A template run's providers see only their own steps in full. To decide: filter by team, or require every team for reads.
No second factor for password accounts (use single sign-on). Outgoing traffic from steps isn't restricted. Encrypting etcd, Postgres disks and backups is the operator's. Nothing verifies Vennir's own signatures at install.
Syncing from Azure Key Vault, HashiCorp Vault and 1Password: the console recognises them, but only GCP Secret Manager and AWS Secrets Manager are proven end to end. Azure isn't an install target yet.
A malicious product that a reviewer approved; a compromised cluster administrator, node or control plane; side channels between Pods on one node; the security of a team's own vault and the identities it trusts.
Use the repository's Security tab, Report a vulnerability, never a public issue. You'll hear back within 5 working days, and a fix is released before details are published, within 90 days unless we agree otherwise. Only the latest 0.x release gets security fixes.