The most common way an infrastructure secret leaks is not a sophisticated attack. It is a database password typed into a Terraform variable default, an API token pasted into a provider block, or a connection string that lands in plaintext state and gets committed to Git. Once a credential is in HCL or in state, it is in your version history, in every clone, and in every backup. Rotating it afterward does not undo the exposure. Preventing it from ever being written down is the only reliable fix.
ops0 is built so that secret values never live in your infrastructure code, never sit in plaintext state, and never enter the context the AI reasons over. Secrets are stored and resolved as managed references, injected only at apply, and masked everywhere else. The goal is to remove the class of mistake entirely, not to scan for it after the leak.
Why secrets leak in infrastructure code
Infrastructure code is good at leaking secrets because it describes real systems, and real systems need real credentials. A database wants a password, a provider block wants an access key, a queue wants a connection string. The path of least resistance is to put the value inline, and once it is inline it flows everywhere the code flows.
State makes this worse. Terraform records resource attributes in state, and many are sensitive by nature, so a hardcoded input is duplicated in state too. A plaintext state file in a shared bucket is a credential dump waiting to happen. The fix is architectural: keep the value out of the code and out of state, and hand it to the engine only when needed.
Two tiers: organization and project
ops0 stores secrets in two tiers. Organization-level secrets are defined once and available to every project, the right home for shared credentials like a common registry token. Project-level secrets belong to a single project and are visible only inside it, the right home for anything specific to one environment or team.
Resolution is hierarchical. When a project asks for a secret by name, ops0 checks the project tier first and falls back to the organization tier if there is no match. A project can override a shared value without renaming anything, and a team can keep a production credential inside its project while still inheriting shared defaults. The name is the contract; the value is resolved at the most specific tier that has it.
Two ways to store a secret
Every secret is stored one of two ways, and both keep the plaintext out of your code.
The first is encrypted inline at rest. The value is encrypted with AES-256-GCM before it is written, so what sits in the ops0 datastore is ciphertext, not the credential. This is the simplest option for values that do not already live in a vault.
The second is a reference to an external vault. ops0 supports AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, and Oracle Vault. Here ops0 stores only the pointer, the identifier of the secret in your vault, never a copy of the value, and fetches the current value from the vault at deploy time. Because only the pointer is stored, rotation is automatic: rotate the secret in your vault and the next deploy picks up the new value, no change on the ops0 side. Your vault stays the single source of truth.
How injection works at apply time
Secrets are injected only when needed, and only into the process that runs the engine. When a deploy starts, ops0 resolves each referenced secret by decrypting the inline ciphertext or fetching it from the vault. Those values become Terraform variable declarations passed to the engine as TF_VAR environment variables, so the running Terraform, OpenTofu, or Oxid process receives them the way it expects, with no value written to disk.
A log redactor sits over the whole run, masking resolved secret values in plan output, apply output, and diagnostic logging, so the value never touches disk or a log line. The credential exists as plaintext only in the memory of the applying process, for exactly as long as that apply runs.
A reference in your code stays a reference. Instead of a literal, your variable reads from a managed pointer, and ops0 supplies the value at apply time:
# The value never appears in code, state, or logs.
# ops0 resolves this reference at apply time.
variable "database_url" {
type = string
sensitive = true
default = "ops0.secret.database_url" # managed reference, not a value
}
provider "postgresql" {
# Injected as TF_VAR_database_url at apply, masked in all logs
url = var.database_url
}
The architectural guarantee: values stay server-side
The strongest protection is structural. Secret values are resolved only on the server, inside the deploy path, and are never returned to the frontend. The interface you use, and the AI assistant you talk to, operate on secret names and metadata: which secrets exist, where they live, when they were last used. Neither ever receives a decrypted value.
This is what keeps secrets out of AI context. When the assistant helps you write or review code, it reasons about the reference ops0.secret.database_url, not the connection string behind it. It can tell you a secret is referenced, resolves at the organization tier, or has not been used in ninety days, all without the value entering the prompt. The model sees the shape of your secrets, never their contents.
A secret-hygiene check on generated code
Prevention also means catching the mistake at authoring time. When code is generated or edited, ops0 runs a secret-hygiene check that flags hardcoded credentials: a password in a variable default, an access key in a provider block, a token in a connection string. Rather than accept the literal, the check prompts you to lift it into a managed secret and replace it with a reference, so the bad pattern is intercepted before it is ever committed.
Every fetch is audited
Because secrets are resolved server-side, every resolution is a recorded event. Each time a secret is decrypted or fetched from a vault, ops0 logs who triggered it, when, and why: which user or deploy, which project, which apply. Rotation flows through the same path, so a rotated vault value is picked up on the next deploy and audited like any other. You get a complete trail of every time a credential was touched, which turns secret handling from a hope into something you can prove.
How do I keep secrets out of Terraform state?
Keep the value out of the code in the first place. In ops0 a secret is a managed reference, not a literal, so the plaintext is never written into a variable default and never duplicated into plaintext state. The value is resolved only at apply time and injected into the engine process, so there is nothing sensitive to record in state or commit to Git.
Does the AI ever see my secret values?
No. Secret values are resolved server-side, inside the deploy path, and are never returned to the frontend or included in the context the model reasons over. The assistant sees secret names and metadata, such as which secrets exist and when they were last used, but never a decrypted value.
Can I use an external vault instead of storing secrets in ops0?
Yes. ops0 supports AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, and Oracle Vault. In vault mode ops0 stores only the pointer to the secret, never a copy of the value, and fetches the current value directly from your vault at deploy time. Your vault stays the single source of truth.
Are secrets encrypted at rest?
Yes. Secrets stored inline in ops0 are encrypted with AES-256-GCM before they are written, so what sits in the datastore is ciphertext rather than the credential. Secrets stored as vault references keep no copy of the value at all; only the pointer is stored.
Is secret access audited?
Yes. Because every value is resolved server-side, every decryption and every vault fetch is a recorded event. ops0 logs who triggered it, when, and why, tying each resolution to a user or deploy, a project, and a specific apply, so you have a complete trail of when each credential was touched.
What about rotation?
When a secret is stored as an external vault reference, rotation is automatic. Because ops0 keeps only the pointer and fetches the current value at deploy time, rotating the secret in your vault means the next deploy picks up the new value with no change on the ops0 side, and that fetch is audited like any other.