﻿# FlowMarkup Secrets Management

**Version:** 0.9.0
**Date:** 2026-03-28
**Designed by:** Łukasz Nawojczyk
**Copyright:** © 2026 Progralink Łukasz Nawojczyk. All rights reserved.
**License:** Open Web Foundation Agreement 1.0 (OWFa 1.0)

---

## 1. Overview & Motivation

The `ENV.*` scope alone has significant limitations for credential management:

- **No structured secret types** — `ENV.*` values are flat strings. Username+password pairs, client credentials, and certificates require multiple `ENV.*` lookups with naming conventions.
- **Best-effort redaction** — the spec says "MUST redact `ENV.*` values from logs." However, there is no content-based credential detection or taint-tracking enforcement for `ENV.*` values.
- **No pluggable backend** — secrets must be pre-loaded into the OS environment before engine startup. No integration with Vault, AWS Secrets Manager, Kubernetes Secrets, or any external store.
- **No access control** — any flow with `ENV` capability can read ALL environment variables, including ones that are not secrets. The `cap: { ENV: ["VAR"] }` pattern gives per-variable control, but environment variables mix config and secrets indiscriminately.
- **No secret typing** — the engine cannot distinguish an API token from a database password from a TLS certificate. All are opaque strings.
- **Leakable by design** — nothing prevents `set: { my_var: ENV.API_KEY }` which copies the secret into a loggable, returnable variable.

> **Content-based credential detection for ENV values.** The engine MUST apply content-based credential auto-detection to all `ENV.*` values at access time. Detection patterns MUST include: URI with embedded userinfo (`scheme://user:pass@host`), JWT tokens, connection strings containing `://...@`, and the engine-configurable API key pattern list. Detected credential values MUST be treated as `$exportable: false` and MUST be redacted from all log output, error messages, and return values. Static analysis rule SA-ENV-8 (ERROR) MUST flag `ENV.*` references in `log:`, `return:`, `emit.data:`, `yield:`, and `throw.message:` when the ENV variable name matches connection string patterns (`*_CONN`, `*_DSN`, `*_URL`, `*_URI`). *(CWE-522)*

This design introduces a dedicated `SECRET.*` scope with opaque values, mandatory redaction, pluggable backends, and full capability integration — following established patterns from Jenkins Credentials, Kubernetes Secrets, HashiCorp Vault, GitHub Actions Secrets, and AWS/GCP Secret Managers.

### Design Principles

1. **Deny-by-default** — follows the `exec` and `mail` pattern. No flow can access secrets without explicit capability grants.
2. **Opaque values** — secret values are NOT plain strings in CEL. They are typed handles that the engine resolves at action boundaries. Flows cannot inspect, concatenate, log, or return secret values.
3. **Pluggable backends** — the engine defines a Secrets Provider SPI (like Action Providers). Implementations for Vault, AWS SM, K8s Secrets, etc. are pluggable. Engine configuration is opaque to flows (like SMTP config for `mail`).
4. **Capability-gated** — `SECRET` is a capability category in `requires:` and `cap:`. Per-secret granularity. Intersection rule applies.
5. **Mandatory redaction** — engine MUST redact secret values from ALL output: logs, traces, error messages, debug endpoints. This is a MUST, not a SHOULD.

---

## 2. Core Concepts

### 2.1 What Is a Secret?

A **secret** is a named, typed credential managed by the engine's configured secrets provider. Secrets are:

- **Named** — referenced by a stable identifier (e.g., `api_token`, `db_credentials`).
- **Typed** — each secret has a declared type that determines its structure and valid injection points.
- **Opaque** — flow code receives a handle, not the raw value. The engine resolves handles at action boundaries.
- **External** — secrets are NOT declared in flow YAML. They are provisioned in the engine's secrets store by operators. Flows only *reference* secrets by name.
- **Scoped** — secrets have an access scope (engine-wide, per-flow, per-environment) determined by the secrets provider, not by flow code.

### 2.2 The `secret` CEL Binding

`SECRET` is a top-level CEL binding registered by the engine, alongside `ENV`, `SERVICES`, `GLOBAL`, `CONTEXT`, and `self`. It is a map of secret names to `SecretValue` objects.

```
SECRET.<name>              → SecretValue (opaque handle)
SECRET.<name>.username     → SecretValue (structured field access — user-password, client-credentials)
SECRET.<name>.password     → SecretValue (structured field access)
SECRET.<name>.client_id    → SecretValue (structured field access)
SECRET.<name>.client_secret → SecretValue (structured field access)
SECRET.<name>.token        → SecretValue (structured field access — token type alias)
SECRET.<name>.certificate  → SecretValue (structured field access — certificate)
SECRET.<name>.private_key  → SecretValue (structured field access — certificate, ssh-key)
```

`SecretValue` is an opaque type in the FlowMarkup type system. It has **no** CEL methods or operators — no `size()`, no `==`, no `+`, no `contains()`, no string conversion. It exists solely to be passed to action boundaries where the engine resolves it.

> **meta() on SECRET bindings.** When `meta()` is called on a `SECRET.*` binding, the engine MUST return `null` for the `name` field. Exposing the secret alias name enables secret enumeration attacks. The `type`, `kind`, and `readonly` fields MAY be returned as they do not reveal the secret identity. *(CWE-200)*

### 2.3 Secret vs. Environment Variable

| Aspect | `ENV.*` | `SECRET.*` |
|---|---|---|
| Source | OS process environment | Engine secrets provider (Vault, AWS SM, K8s, etc.) |
| Type | Always string | Typed (text, user-password, client-credentials, certificate, ssh-key) |
| CEL type | `string` | `SecretValue` (opaque) |
| Mutability | Read-only | Read-only |
| String operations | Yes (concatenation, `size()`, comparison) | No — opaque handle |
| Can assign to variable | Yes (but SHOULD NOT) | No — static analysis error (MUST) |
| Log redaction | MUST | MUST |
| Capability category | `ENV` | `SECRET` |
| Backend | OS environment only | Pluggable (Vault, AWS SM, K8s, env-backed, file-backed, etc.) |
| Rotation | Requires engine restart | Provider-dependent (Vault: dynamic leases; AWS SM: automatic rotation) |

---

## 3. Secret Types

Drawing from Jenkins Credential types, Kubernetes Secret types, and common credential patterns:

### 3.1 `text` — Opaque String

A single opaque string value. API keys, tokens, passphrases, connection strings.

```
Structure: { value }
CEL access: SECRET.api_key → SecretValue (the whole value)
```

**Valid injection points:** `auth.bearer`, `headers.*`, `input.*`, `smtp.auth.password`, `exec.env.*`, `ssh.env.*`, `query.*`

**Access pattern:** For `text`-type secrets, `SECRET.<name>` returns the `SecretValue` directly — no `.value` field access is needed. For composite types (`user-password`, `client-credentials`, etc.), field access is required: `SECRET.<name>.<field>` (e.g., `SECRET.db_creds.username`).

### 3.2 `user-password` — Username + Password Pair

A username and password. Database credentials, LDAP bind credentials, HTTP basic auth.

```
Structure: { username, password }
CEL access:
  SECRET.db_creds           → SecretValue (composite handle — use field access for injection)
  SECRET.db_creds.username   → SecretValue (username component)
  SECRET.db_creds.password   → SecretValue (password component)
```

**Valid injection points:** `auth.basic.username`, `auth.basic.password`, `input.*`, `smtp.auth.username`, `smtp.auth.password`, `exec.env.*`, `ssh.env.*`

### 3.3 `client-credentials` — OAuth2 Client Credentials

A client ID and client secret. OAuth2 client credentials grant, API key pairs.

```
Structure: { client_id, client_secret }
CEL access:
  SECRET.oauth_creds              → SecretValue (composite handle — use field access for injection)
  SECRET.oauth_creds.client_id     → SecretValue
  SECRET.oauth_creds.client_secret → SecretValue
```

**Valid injection points:** Individual fields: `auth.basic.username` (map `client_id`), `auth.basic.password` (map `client_secret`), `input.*`, `exec.env.*`, `ssh.env.*`. OAuth2 client_credentials flow uses Basic auth with client_id as username and client_secret as password.

### 3.4 `certificate` — X.509 Certificate + Private Key

A TLS client certificate with its private key and optional CA chain.

```
Structure: { certificate, private_key, ca_chain? }
CEL access:
  SECRET.tls_cert               → SecretValue (composite — valid for TLS client auth config)
  SECRET.tls_cert.certificate    → SecretValue (PEM-encoded cert)
  SECRET.tls_cert.private_key    → SecretValue (PEM-encoded key)
  SECRET.tls_cert.ca_chain       → SecretValue (PEM-encoded CA chain, optional)
```

**Valid injection points:** `input.*` (for services that accept TLS config), `request.tls`, engine-level service TLS config.

### 3.5 `ssh-key` — SSH Private Key

An SSH private key with optional passphrase.

```
Structure: { private_key, passphrase? }
CEL access:
  SECRET.deploy_key              → SecretValue (composite)
  SECRET.deploy_key.private_key   → SecretValue
  SECRET.deploy_key.passphrase    → SecretValue (optional)
```

**Valid injection points:** `input.*`, `exec.env.*`, `ssh.env.*` (e.g., `SSH_KEY`).

### 3.6 `token` — Bearer Token (Semantic Alias)

Semantically identical to `text` but explicitly marks the value as a bearer token. Enables engine optimizations (e.g., automatic `Authorization: Bearer` header injection).

```
Structure: { value }
CEL access: SECRET.github_token → SecretValue
```

**Valid injection points:** Same as `text`. When used in `auth.bearer:`, the engine knows to prepend `Bearer `.

### 3.7 `access-key` — AWS-Style Access Key Pair

An access key ID, secret access key, and optional session token. For S3, MinIO, GCS S3-compatible, and similar services.

```
Structure: { access_key_id, secret_access_key, session_token? }
CEL access:
  SECRET.aws_creds                      → SecretValue (composite)
  SECRET.aws_creds.access_key_id        → SecretValue
  SECRET.aws_creds.secret_access_key    → SecretValue
  SECRET.aws_creds.session_token        → SecretValue (optional)
```

**Valid injection points:** Engine-level storage configuration, `exec.env.*`, `ssh.env.*`.

**Why not `client-credentials`?** Different field names (`access_key_id` vs `client_id`) and the third optional field (`session_token`) would cause confusion. Keeping it separate is clearer.

### 3.8 Type Summary

| Type | Fields | Typical Use |
|---|---|---|
| `text` | `value` | API keys, connection strings, passphrases |
| `user-password` | `username`, `password` | DB creds, LDAP, HTTP basic auth, FTP, SMB |
| `client-credentials` | `client_id`, `client_secret` | OAuth2, API key pairs |
| `certificate` | `certificate`, `private_key`, `ca_chain?` | mTLS, client certificates |
| `ssh-key` | `private_key`, `passphrase?` | SSH auth, Git deploy keys, SFTP |
| `token` | `value` | Bearer tokens, JWTs |
| `access-key` | `access_key_id`, `secret_access_key`, `session_token?` | S3, MinIO, GCS S3-compat |

---

## 4. Engine Configuration — Secrets Provider SPI

### 4.1 Provider Architecture

The engine defines a **Secrets Provider** Service Provider Interface (SPI), following the same pattern as Action Providers. The provider is responsible for:

1. **Loading secrets** — fetching secret values from the backing store at engine startup or on-demand.
2. **Resolving secret handles** — converting `SecretValue` handles to raw values at action boundaries.
3. **Secret metadata** — providing name, type, and field structure for each secret.
4. **Lifecycle management** — lease renewal (Vault), rotation notification (AWS SM), cache invalidation.

```
┌─────────────────────────────────────────────┐
│                FlowMarkup Engine              │
│                                             │
│  ┌─────────────────────────────────────┐    │
│  │         Secrets Provider SPI         │    │
│  │                                     │    │
│  │  register(name, type, metadata)     │    │
│  │  resolve(SecretValue) → raw bytes   │    │
│  │  list() → [SecretMetadata]          │    │
│  │  refresh(name) → void               │    │
│  │  close() → void (lease cleanup)     │    │
│  └──────────┬──────────────────────────┘    │
│             │                               │
│    ┌────────┴─────────┐                     │
│    │   Implementations │                     │
│    ├──────────────────┤                     │
│    │ EnvironmentSecretsProvider  (built-in)  │
│    │ VaultSecretsProvider        (plugin)    │
│    │ AwsSecretsManagerProvider   (plugin)    │
│    │ GcpSecretManagerProvider    (plugin)    │
│    │ KubernetesSecretsProvider   (plugin)    │
│    │ FileSecretsProvider         (built-in)  │
│    │ CompositeSecretsProvider    (built-in)  │
│    └──────────────────┘                     │
│                                             │
└─────────────────────────────────────────────┘
```

### 4.2 Built-in Providers

#### `EnvironmentSecretsProvider`

Maps environment variables to secrets. Useful for simple deployments and compatibility with the `ENV.*` pattern.

Engine configuration (opaque to flows):
```yaml
# Engine config — NOT flow YAML
secrets:
  provider: environment
  mappings:
    api_token:
      type: text
      env: ANTHROPIC_API_KEY
    db_creds:
      type: user-password
      username-env: DB_USER
      password-env: DB_PASSWORD
```

> **Production advisory:** `EnvironmentSecretsProvider` is intended for development, testing, and simple single-tenant deployments only. It provides opacity (`SecretValue`) but lacks: rotation, fine-grained ACLs, audit logging of access, hardware-backed storage, and multi-tenant isolation. Production deployments SHOULD use a dedicated secrets backend (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, Kubernetes Secrets with encryption at rest).

#### `FileSecretsProvider`

Reads secrets from files on disk. Compatible with Kubernetes volume-mounted secrets, Docker Swarm `/run/secrets/`, and CI/CD file injection.

Engine configuration:
```yaml
secrets:
  provider: file
  base-path: /run/secrets
  mappings:
    api_token:
      type: text
      file: api_token        # → /run/secrets/api_token
    db_creds:
      type: user-password
      username-file: db_username
      password-file: db_password
```

**File permission requirements.** The engine MUST verify file permissions at startup: (1) Secret files MUST NOT be world-readable (POSIX: reject mode with 'other' read bit set; Windows: reject files with `Everyone` read access). Violation MUST raise `SecretConfigurationError`. (2) The engine SHOULD log WARNING when secret files reside on a persistent filesystem (not tmpfs/ramfs). (3) The engine MUST reject symlinked secret files unless the symlink target also passes permission checks. (4) The engine MUST refuse to read files outside the configured `base-path` (path traversal prevention). *(CWE-312, CWE-732)*

#### `CompositeSecretsProvider`

Chains multiple providers. First match wins. Enables layered configuration (e.g., Vault for production secrets, environment for local overrides).

```yaml
secrets:
  provider: composite
  providers:
    - provider: vault
      config: { ... }
    - provider: environment
      config: { ... }
```

> **Shadowing detection and deterministic resolution.** When multiple secret providers are composed via `CompositeSecretsProvider`, the resolution order MUST be deterministic and documented in the engine configuration. Providers are resolved in declaration order (first match wins), and this order MUST be stable across engine restarts. If a secret name resolves in multiple providers, the engine MUST log a WARNING-level audit event identifying all providers that can resolve the name, the provider that won resolution, and the provider(s) that were shadowed. The audit event MUST include the following fields: `event_type: "secret.shadow_detected"`, `secret_name`, `winning_provider` (name and index), `shadowed_providers` (list of name and index), `resolution_timestamp` (ISO 8601 with ms precision), `flow_id`, and `tenant_id`. Engines SHOULD provide a `strict` mode (configurable, default: false) where shadowed secrets cause a `ConfigurationError` at engine startup rather than silent priority resolution. This prevents a misconfigured lower-priority provider (e.g., environment variables) from accidentally overriding a production vault secret. *(CWE-694: Use of Multiple Resources with Duplicate Identifier)*

> **Runtime shadowing detection.** Shadowing detection MUST run at every resolution, not only at startup. The secret set MUST be frozen at startup — new secrets from lower-priority providers added after initialization MUST NOT be recognized. Providers MUST NOT dynamically register new secret names after engine startup.

### 4.3 Plugin Providers

#### `VaultSecretsProvider`

Integrates with HashiCorp Vault. Supports KV v1/v2, dynamic database credentials, PKI, and transit encryption.

Engine configuration:
```yaml
secrets:
  provider: vault
  address: https://vault.internal:8200
  auth:
    method: kubernetes          # or: token, approle, aws-iam, gcp-iam
    role: flowmarkup-engine
  engine: kv-v2
  base-path: secret/data/flowmarkup
  # Per-secret overrides (optional — default is name-based lookup under base-path)
  mappings:
    db_creds:
      path: database/creds/myapp-readonly   # dynamic credential
      type: user-password
      lease-renewal: true
    tls_cert:
      path: pki/issue/myapp
      type: certificate
```

**Dynamic secrets:** When the Vault path is a dynamic engine (e.g., `database/creds/`), each `resolve()` call may generate a fresh credential with a lease. The provider manages lease renewal transparently. When the engine shuts down, it revokes all active leases.

#### `AwsSecretsManagerProvider`

Integrates with AWS Secrets Manager.

```yaml
secrets:
  provider: aws-secrets-manager
  region: us-east-1
  # Auth: uses default AWS credential chain (IAM role, env vars, config file)
  mappings:
    api_token:
      secret-id: prod/myapp/api-token
      type: text
      # version-stage: AWSCURRENT (default)
    db_creds:
      secret-id: prod/myapp/db-credentials
      type: user-password
      json-keys:                    # AWS SM stores JSON; map keys to fields
        username: username
        password: password
  cache-ttl: "5m"                   # local cache TTL (default: 5m)
```

#### `GcpSecretManagerProvider`

```yaml
secrets:
  provider: gcp-secret-manager
  project: my-gcp-project
  mappings:
    api_token:
      secret-id: api-token
      version: latest
      type: text
```

#### `KubernetesSecretsProvider`

Reads from Kubernetes Secret objects. Useful when the engine runs as a pod.

```yaml
secrets:
  provider: kubernetes
  namespace: flowmarkup        # default: pod's own namespace
  mappings:
    db_creds:
      secret-name: myapp-db-credentials
      type: user-password
      keys:
        username: username      # k8s Secret data key → secret field
        password: password
```

### 4.4 Provider SPI

The engine MUST provide a pluggable Secrets Provider that implements the following operations:

| Operation | Description |
|---|---|
| `initialize` | Initialize the provider with engine configuration |
| `list` | List available secret metadata (names, types) — no values |
| `resolve` | Resolve a secret handle to its raw value. Called at action boundaries — the only point where raw values exist. Throws `SecretAccessError` if the secret cannot be resolved (not found or access denied by provider ACL). |
| `refresh` | Re-fetch a secret from the backend (for rotation support) |
| `close` | Cleanup — revoke leases, close connections |

> **Error type consolidation.** At the catch-handler level, `SecretNotFoundError` and `SecretAccessDeniedError` MUST be presented as a single `SecretAccessError` type. The engine's internal audit log MUST still distinguish between not-found and access-denied for operator debugging, but catch handlers MUST NOT be able to differentiate between them. This prevents secret name enumeration via error-type oracle attacks. *(CWE-204: Observable Response Discrepancy)*

**Secret access audit trail.** The engine MUST emit an audit event for every `resolve()` call. The event MUST include: `event_type: "secret.resolved"`, `secret_name`, `field` (if applicable), `flow_id`, `step_id`, `action_type` (the action consuming the secret), `provider_name`, `timestamp`, and `tenant_id`. The event MUST NOT include the secret value. Providers that support backend audit trails (Vault, AWS Secrets Manager, GCP Secret Manager) MUST enable them. *(CWE-778)*

> **Implementation note:** Providers supporting automatic rotation MUST implement a grace period of at least 60 seconds (configurable, minimum 30 seconds) during which both the previous and current secret values are accepted. The grace period duration MUST be configurable per secret type.
>
> During the grace period:
> - The engine MUST resolve the CURRENT value for new requests
> - Action providers MUST accept BOTH current and previous values
> - The engine MUST emit an audit event when a previous-version secret is used successfully (indicating in-flight usage during rotation)
> - After the grace period expires, the engine MUST revoke the previous value
>
> This prevents race conditions (CWE-367) where in-flight requests using a stale secret fail during rotation windows.
>
> **Double-rotation edge case.** If a secret is rotated more than once within a single grace period (e.g., emergency rotation after compromise), the engine MUST accept the current value and all previous values whose grace periods have not yet expired. The engine MUST NOT limit acceptance to only the immediately previous version. Each rotation event starts its own independent grace period. The engine MUST cap the maximum number of concurrently active versions (current + grace-period versions) at **5**. When a 6th rotation occurs while 5 versions are still in their grace periods, the oldest grace-period version MUST be forcibly expired regardless of its remaining grace time, and the engine MUST emit an audit event at ERROR severity indicating forced version eviction (including the secret name, evicted version timestamp, remaining grace time, and reason). This cap prevents unbounded memory growth during rapid rotation scenarios (e.g., automated rotation loops due to misconfiguration). The engine MUST emit an audit event with severity WARN when multiple grace periods overlap for the same secret, as this may indicate an incident response scenario.

> **Cache encryption.** When an engine caches resolved secret values (within the permitted action-execution boundary and provider-declared TTL), the cached values MUST be encrypted at rest using AES-256-GCM or equivalent authenticated encryption. The Additional Authenticated Data (AAD) for cache encryption MUST be the concatenation of: `secret_name` (UTF-8), `\x00` separator, `provider_name` (UTF-8), `\x00` separator, `tenant_id` (UTF-8), `\x00` separator, and `cache_entry_timestamp` (ISO 8601 UTC, UTF-8). This AAD binding ensures cached values cannot be replayed across secrets, providers, tenants, or time windows. Each cache entry MUST use a unique nonce (96-bit random for AES-256-GCM). Cached secret values MUST NOT be written to disk, swap space, or shared memory in plaintext. Implementations MUST use platform-appropriate memory-locking mechanisms (e.g., `mlock()` on POSIX, `VirtualLock()` on Windows) to prevent secret cache pages from being swapped to disk. *(CWE-316: Cleartext Storage of Sensitive Information in Memory)*
>
> **mlock failure handling.** When `mlock()` / `VirtualLock()` fails (e.g., due to `RLIMIT_MEMLOCK` limits in containerized environments), the engine MUST refuse to start by default and log an ERROR at startup with the specific error and the configured `RLIMIT_MEMLOCK` value. To allow operation without memory locking, operators MUST explicitly set `secrets.cache.allow-unprotected: true` in the engine configuration. When this opt-in flag is set and `mlock()` fails, the engine MUST log a WARNING at startup, MUST set `RUNTIME.SECRET_CACHE_MLOCKED` to `false`, and MUST proceed without memory locking. When `secrets.cache.allow-unprotected` is `false` (the default) and `mlock()` fails, the engine MUST NOT start — this prevents silent degradation of secret memory protection in production environments. *(CWE-316: Cleartext Storage of Sensitive Information in Memory)*
>
> **Persistent cache requirement.** Any secret cache that persists beyond the lifetime of a single flow execution MUST encrypt cached values at rest using AES-256-GCM or equivalent authenticated encryption with unique per-entry nonces. In-memory caches MUST be cleared (zeroed) when the owning flow completes. Engines MUST NOT retain decrypted secret material in any cache tier after the flow that requested the secret has finished execution, unless the cache is shared across flows and the cached entry is still within its provider-declared TTL. *(CWE-524: Use of Cache Containing Sensitive Information)*

> **Vault authentication token rotation.** Engines MUST support automatic rotation of vault access tokens and provider authentication credentials. When the engine authenticates to a secret provider using a long-lived token (e.g., Vault token, cloud IAM service account key), token refresh MUST happen before expiry with a configurable lead time (default: **5 minutes** before token expiration). The engine MUST NOT store provider authentication tokens in plaintext configuration files; tokens MUST be injected via environment variables, instance metadata, or dedicated credential helpers. The engine MUST monitor token TTL and initiate renewal when `remaining_ttl <= lead_time`. Failed token refresh MUST trigger an alert (audit log entry with severity CRITICAL, including provider name, token identity, and failure reason) but MUST NOT immediately invalidate cached secrets — the grace period defined in §4.4 applies, allowing in-flight operations to complete using cached values within their TTL. The engine MUST retry renewal with exponential backoff (maximum 5 retries, maximum backoff 60 seconds). Failure to renew after all retries MUST transition affected flows to SUSPENDED state. *(CWE-613: Insufficient Session Expiration)*

Secret types: `text`, `user-password`, `client-credentials`, `certificate`, `ssh-key`, `token`, `access-key`. Each secret has a name, type, and optional labels (key-value metadata).

---

## 5. Capability Integration

### 5.1 `SECRET` as a Capability Category

`SECRET` is a capability category following the same deny-by-default pattern as all other capability categories (`ENV`, `CONTEXT`, `GLOBAL`, `SERVICES`, `SUBFLOWS`, `REQUEST`, `EXEC`, `MAIL`, `RUNTIME`, `STORAGE`, `SSH`, `RESOURCES`). It appears in:

- `flow.requires.SECRET` — declare what secrets the flow needs (load-time validation)
- `run.cap.SECRET` — grant specific secrets to a sub-flow (allowlist)

### 5.2 Granularity

Per-secret name, array list, or object form:

```yaml
# requires: — flow declares its needs
requires:
  SECRET: [api_token, db_creds]        # specific secrets

# cap: — caller grants to sub-flow
cap:
  SECRET: [api_token]                   # only api_token
```

### 5.3 Intersection Rule

Effective secret capabilities use a three-way intersection:

```
effective = requires(sub-flow) ∩ capabilities(caller) ∩ cap(run-step, if present)
```

A sub-flow can only access secrets that satisfy all three conditions: (1) the sub-flow declares the secret in its `requires:`, (2) the caller possesses the secret capability, and (3) the `run` step's `cap:` grants it (if `cap:` is specified). A sub-flow can never access a secret the caller does not have access to, nor a secret it does not declare in `requires:`.

### 5.4 Unified Capability Model

There is no trusted/untrusted distinction for secret capabilities. Every sub-flow declares its own `requires:` and the effective capabilities are computed by the three-way intersection rule (see section 5.3). A sub-flow only receives the secrets it declares in `requires:`, intersected with what the caller possesses and what the `run` step grants via `cap:`.

### 5.5 INHERIT Semantics

`cap: { SECRET: INHERIT }` on a `run:` step forwards the caller's **entire** secret grant (the set given by the parent, not limited by the caller's own `requires:`) to the sub-flow as the *offered set*. The sub-flow still only receives secrets it explicitly declares in its own `requires:` — the intersection rule (§5.3) always applies. This is useful when a caller acts as a pure proxy and does not know which secrets the sub-flow needs.

`requires: { SECRET: INHERIT }` is **NOT valid** -- flows MUST enumerate every secret they need. A flow cannot declare "I need whatever my caller has." SA-CAP-3 (ERROR) flags `cap: { SECRET: INHERIT }` as overly broad forwarding.

The effective capabilities are still computed by intersection: `requires(sub-flow) ∩ given`. INHERIT only affects the forwarding set (what is offered), not the access set (what the sub-flow actually receives). The sub-flow still only gets secrets it explicitly declares in `requires:`.

```yaml
# Forwarding all caller secrets to sub-flow
- run:
    flow: analytics/report.flowmarkup.yaml
    cap:
      SECRET: INHERIT    # forward entire caller secret grant
```

### 5.6 Schema Shape

In `requires:`:
```json
"SECRET": {
  "description": "Secrets this flow requires. Array of secret names or objects with optional flag.",
  "type": "array",
  "items": {
    "oneOf": [
      { "type": "string", "minLength": 1 },
      {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "minLength": 1 },
          "optional": { "type": "boolean" }
        },
        "additionalProperties": false
      }
    ]
  },
  "minItems": 1
}
```

In `cap:`:
```json
"SECRET": {
  "description": "Secrets capability grant.",
  "oneOf": [
    { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1 },
    {
      "type": "object",
      "description": "Object form: keys are secret names.",
      "additionalProperties": true
    },
    { "const": "INHERIT", "description": "Forward entire caller secret grant to sub-flow." }
  ]
}
```

---

## 6. Injection Points & Resolution

### 6.1 Where SecretValues Are Accepted

`SecretValue` is accepted in CEL expression positions at the following **action boundary points** where the engine resolves the opaque handle to the raw value:

| Injection Point | Example | Resolution |
|---|---|---|
| `request.auth.bearer` | `SECRET.api_token` | Engine resolves → sets `Authorization: Bearer <value>` |
| `request.auth.basic.username` | `SECRET.db_creds.username` | Engine resolves field → username string |
| `request.auth.basic.password` | `SECRET.db_creds.password` | Engine resolves field → password string |
| `request.headers.*` (values) | `SECRET.api_key` | Engine resolves → header value string |
| ~~`request.query.*`~~ | ~~`SECRET.api_key`~~ | **PROHIBITED.** SA-SECRET-18 (ERROR). Query parameters leak to logs, CDN caches, and Referer headers. Use `request.headers` instead. |
| ~~`request.body.*`~~ | ~~`SECRET.auth_token`~~ | **PROHIBITED.** SA-SECRET-26 (ERROR). Request bodies leak through logging middleware, request recording, and server-side logging. Use `request.headers` or `request.auth` instead. |
| ~~`request.url`~~ | ~~`SECRET.api_key`~~ | **PROHIBITED.** SA-SECRET-16 (ERROR). URLs leak to access logs, proxy logs, CDN caches, and Referer headers. Use `request.headers` or `request.auth` instead. |
| `call.params.*` (values) | `SECRET.db_creds` | Service receives resolved value(s) |
| `run.params.*` (values) | `SECRET.api_token` | Sub-flow receives `SecretValue` (opaque, requires `SECRET` cap) |
| `exec.env.*` (values) | `SECRET.api_key` | Engine resolves → process environment variable (existing field — now also accepts SecretValue) |
| `ssh.env.*` (values) | `SECRET.api_key` | Engine resolves → remote process environment variable (same as `exec.env.*`) |
| `mail.smtp.auth.password` | `SECRET.smtp_pass` | Engine resolves → SMTP password |
| `mail.smtp.auth.username` | `SECRET.smtp_creds.username` | Engine resolves → SMTP username |

### 6.2 Where SecretValues Are NOT Accepted

`SecretValue` in these positions is a **static analysis error** (MUST reject):

| Rejected Position | Why |
|---|---|
| `set:` key values | Copies secret into a variable — leakable via logs, output, errors |
| `return:` values | Exposes secret to caller — violates opacity |
| `log:` expression | Writes secret to log output |
| `assert:` condition | Requires string comparison — opacity violation |
| `switch.value:` | Requires equality comparison |
| `if.condition:` / `while.condition:` | Requires boolean evaluation |
| `emit.data:` values | Publishes secret via event bus |
| `yield:` / `yield.*` values | Exposes secret to `onYield:` handler — violates opacity (SA-YIELD-9) |
| `forEach.items:` | Iterates secret — nonsensical |
| `throw.message:` | Exposes in error message |
| `throw.data:` values | Exposes in error payload |
| `request.body` values | Copies secret into request payload -- leakable via logging middleware, request recording, and server-side logging |
| `request.url` values | Copies secret into URL -- leakable via access logs, proxy logs, CDN caches, and Referer headers |
| Flow-level `output:` | Exposes secret as part of flow contract |
| `decode()`/`parse()` CEL expression | Attempts to inspect opaque secret contents |
| `vars:` initializers | Assigns to a variable at declaration time |

### 6.3 Resolution Semantics

When the engine encounters a `SecretValue` at a valid injection point:

1. **Resolve** — call `provider.resolve(name, field)` to obtain the raw value.
2. **Inject** — place the raw value at the action boundary (HTTP header, process env, service input parameter).
3. **Redact** — register the raw value with the redaction engine for log masking.
4. **Forget** — the raw value is NOT retained in the flow's variable scope. It exists only for the duration of the action execution.

**Memory zeroization.** The engine MUST explicitly zero (overwrite with null bytes) all memory buffers containing resolved secret values after action execution completes. In runtimes where strings are immutable (JVM, .NET, JavaScript): engines MUST use mutable byte arrays (`byte[]`, `Buffer`, `SecureString`) for resolved values, NOT immutable string types. Resolved values MUST NOT be converted to immutable strings at any point in the resolution-to-injection pipeline. In runtimes with deterministic memory management (Rust, C++): engines MUST use `zeroize` or `SecureZeroMemory` on deallocation. *(CWE-316, CWE-244)*

For `run.params.*`, the behavior is different: the `SecretValue` handle is passed through (not resolved). The sub-flow receives the opaque handle and can use it at its own action boundaries — provided it has the `SECRET` capability for that secret name. The sub-flow MUST declare the corresponding `SECRET` in its `requires:` to handle the opaque value, even if it only passes the value through to an action's `params:`. The three-way intersection rule (§5.3) still applies: the caller must possess the capability, the sub-flow must declare it, and the `run` step's `cap:` must grant it (if specified).

**Type checking at injection points:** If a composite secret (e.g., `user-password`) is used in a position that expects a single scalar (e.g., `auth.bearer:`), the engine MUST throw `SecretTypeError` at load time. Conversely, a `text` or `token` secret used where field access is attempted (e.g., `SECRET.text_val.username`) triggers `SecretTypeError` (also covered by SA-SECRET-15). Always use explicit field access for composite secrets at injection points — e.g., `SECRET.db_creds.username` and `SECRET.db_creds.password` for `auth.basic`.

> **Per-step resolution guarantee.** For a given step execution, the engine MUST resolve each distinct `(secret_name, field)` pair at most once and cache the resolved value for the duration of that step's execution. Multiple references to the same `(secret_name, field)` within a single step MUST receive the same resolved value. This prevents inconsistency from dynamic secret rotation mid-step.

> **Action provider runtime credential detection.** Action providers that return credential material in their output MUST declare `sensitive: true` on those output fields in their provider schema. At runtime, engines MUST additionally scan action output values for credential patterns (using the same auto-detection patterns as §2.4 of FLOWMARKUP-SPECIFICATION.md: URI with userinfo, JWT tokens, known API key formats). When a credential pattern is detected in an action output field that was NOT declared `sensitive: true`, the engine MUST: (1) apply `$exportable: false` taint to the receiving variable automatically; (2) emit an audit event at WARN severity identifying the action provider, field name, and detected pattern; (3) emit SA-TAINT-11 at WARN severity. This provides defence-in-depth against providers that fail to declare sensitive output fields. *(CWE-200: Exposure of Sensitive Information to an Unauthorized Actor)*

### 6.4 Provider Output Security

Action providers MAY return credential material (tokens, connection strings, dynamic credentials) in their output. Flow authors MUST protect returned credential values using `$secret: true` or `$exportable: false` on receiving variables. SA-TAINT-4 (ERROR): action provider output fields containing credential material without `sensitive: true` declaration. Prefer `$secret: true` for full opacity or `$exportable: false` for values needing computation.

Action providers SHOULD declare `sensitive: true` on output fields that contain credential material (tokens, connection strings, keys). SA-TAINT-4 MUST be ERROR severity (not WARN) when the action provider declares `sensitive: true` on an output field. Provider-declared `sensitive: true` output fields MUST automatically receive `$secret: true` taint — not just `$exportable: false`. This provides a provider-driven safety net for credential protection. SA-TAINT-4 (ERROR) flags cases where a provider declares `sensitive: true` but the receiving variable lacks `$exportable: false` or `$secret: true`.

```yaml
# Good: protect dynamic credentials with $secret
vars:
  db_creds:
    $kind: JSON
    $secret: true           # full opacity — cannot be logged, returned, or compared
    $value: null
do:
  - call:
      service: vault
      operation: issue_credentials
      result: db_creds       # SA-TAINT-4 satisfied — $secret: true declared
```

---

## 7. Redaction & Protection

### 7.1 Mandatory Redaction (MUST)

The engine MUST redact all resolved secret values from:

- **Log output** — `log:` steps, engine internal logging, action provider logging
- **Trace output** — distributed tracing spans, OpenTelemetry attributes
- **Error messages** — `ERROR.MESSAGE`, `ERROR.DATA`, stack traces
- **Debug/admin endpoints** — REST APIs, JMX, health checks
- **Variable dumps** — if the engine provides variable inspection, secret handles display as `[SECRET:name]`

Redaction uses **named placeholder replacement** of the resolved value with `{SECRET_NAME}` (the secret's declared name in braces). For example, `SECRET.google_maps_key` redacts to `{GOOGLE_MAPS_KEY}` in logs — a URL like `https://maps.google.com/?api_key=sk-abc123` appears as `https://maps.google.com/?api_key={GOOGLE_MAPS_KEY}`. For field access on composite secrets, the placeholder includes the field: `{DB_CREDS.PASSWORD}`. This makes redacted output meaningful for debugging while preventing credential exposure.

**Context-dependent redaction formats.** The `{SECRET_NAME}` named placeholder format applies to log output, trace output, and debug endpoints (all channels listed above). In `ERROR.DATA` fields exposed to `catch:` handlers, the engine uses `[REDACTED]` instead of named placeholders -- see FLOWMARKUP-ERRORS.md for per-error redaction requirements. Infrastructure detail stripping (private IPs, internal hostnames, sensitive paths) uses `[FILTERED]`. These distinct formats are intentional: named placeholders aid debugging in operator-visible logs, while generic placeholders prevent information disclosure through flow-visible error data.

The engine registers each resolved value with a per-request redaction filter. Secret redaction MUST cover all common encodings of each secret value. At minimum, engines MUST apply redaction patterns to all of the following encoding variants:

- **Raw:** Exact match of the raw value
- **Base64-encoded:** Standard and URL-safe Base64 representations (with and without padding)
- **URL-encoded:** Percent-encoded form (e.g., `%20` for spaces)
- **Hex-encoded:** Both upper and lowercase hexadecimal representations (e.g., `48656c6c6f`)
- **JSON-string-escaped:** JSON escape sequences (e.g., embedded quotes `\"`, Unicode `\uXXXX`)

> In addition to the required encoding forms above, engines MUST also detect and redact secret values in the following encodings:
> - **HTML-entity-encoded:** Numeric (`&#x41;`) and named (`&amp;`) entity forms
> - **Double-URL-encoded:** Values encoded through multiple rounds of percent-encoding
> - **Unicode-escaped:** JSON `\uXXXX` and JavaScript `\x` escape sequences
> - **XML CDATA-wrapped:** Secret values embedded within `<![CDATA[...]]>` sections
> - **Quoted-printable:** MIME quoted-printable encoding (e.g., `=3D` for `=`), relevant for mail contexts
>
> The engine MUST apply redaction to all output channels: logs, error messages, HTTP response bodies, event payloads, and audit trails. The redaction implementation MUST be tested against all listed encoding variants. *(CWE-116: Improper Encoding or Escaping of Output)*

### 7.2 Static Analysis Enforcement

Beyond runtime redaction, static analysis provides compile-time safety.

| Rule ID | Severity | Description |
|---|---|---|
| SA-SECRET-1 | ERROR | `SECRET.*` in `set:` value — assignment to variable |
| SA-SECRET-2 | ERROR | `SECRET.*` in `return:` value — exposure to caller |
| SA-SECRET-3 | ERROR | `SECRET.*` in `log:` expression — written to logs |
| SA-SECRET-4 | ERROR | `SECRET.*` in `emit.data:` value — published to event bus |
| SA-SECRET-5 | ERROR | `SECRET.*` in `throw.data:` or `throw.message:` — exposed in error |
| SA-SECRET-6 | ERROR | `SECRET.*` in `assert:` condition — requires comparison |
| SA-SECRET-7 | ERROR | `SECRET.*` in `switch.value:` — requires comparison |
| SA-SECRET-8 | ERROR | `SecretValue` used as operand in `if/while/break/continue.condition:` — requires boolean evaluation. `has(SECRET.name)` is allowed (map membership check, not value access) |
| SA-SECRET-9 | ERROR | `SECRET.*` in `forEach.items:` — iteration on opaque value |
| SA-SECRET-10 | ERROR | `SECRET.*` in flow-level `output:` required/optional params — contract exposure |
| SA-SECRET-11 | ERROR | `SECRET.*` in `vars:` initializer — assignment at declaration |
| SA-SECRET-12 | ERROR | `SECRET.*` in `decode()`/`parse()` CEL expression — attempts to inspect opaque secret contents |
| SA-SECRET-13 | WARN | `SECRET.*` used but flow has no `requires: { SECRET: [...] }` |
| SA-SECRET-14 | WARN | `requires: { SECRET: [...] }` lists secrets that are not used in the flow body |
| SA-SECRET-15 | ERROR | `SECRET.*` field access on wrong type — e.g., `SECRET.text_secret.username` on a `text` type |
| SA-SECRET-16 | ERROR | `SECRET.*` in `request.url:` — secret in URL (leaks to access logs, proxy logs) |
| SA-SECRET-17 | ERROR | `SECRET.*` in `exec.args:` — secret in command-line args (visible in `ps`, `/proc`) — use `exec.env:` instead |
| SA-SECRET-18 | ERROR | `SECRET.*` in `request.query:` values — query parameters are logged by proxies, CDNs, browser history, and server access logs, making secret exposure virtually certain |
| SA-SECRET-19 | ERROR | Inline secret definition in `cap:` — not supported |
| SA-SECRET-20 | ERROR | `SECRET.*` in `{{ }}` template interpolation -- leaks secret into rendered string |
| SA-SECRET-21 | WARN | `SECRET.*` in `exec.env:` values -- `/proc/PID/environ` exposure (CWE-214) |
| SA-SECRET-22 | ERROR | Function parameter receives `SECRET.*` value -- secrets MUST go directly to action `params:` |
| SA-SECRET-23 | WARN | `SECRET.*` access in unbounded loop without rate limit |
| SA-SECRET-24 | ERROR | `$declassify` audit requirement -- every use MUST be logged (CWE-778, CWE-200) |
| SA-SECRET-25 | ERROR | `hmac()` key in `$declassify` not a `SECRET.*` reference (CWE-328) |
| SA-SECRET-26 | ERROR | `SECRET.*` in `request.body/query/url` (CWE-200, CWE-598) |
| SA-SECRET-27 | ERROR | Action provider error schema includes reflected input fields (CWE-209) |

> **Query parameter prohibition.** Secret values MUST NOT appear in URL query parameters. This is enforced as a static analysis **ERROR** by SA-SECRET-18. Engines MUST also enforce this at runtime: if a resolved `SecretValue` is detected in a query parameter position during request construction, the engine MUST reject the request with `SecretInjectionError`. Query parameters are logged by proxies, CDNs, browser history, and server access logs, making secret exposure virtually certain. API keys MUST be passed via `request.headers` (e.g., `X-API-Key`), and OAuth tokens MUST use the `Authorization` header or POST body. *(CWE-598: Use of GET Request Method With Sensitive Query Strings)*

### 7.3 Runtime Protection

Even if static analysis is bypassed (e.g., action provider returns a secret value in its output), the engine provides defense-in-depth:

1. **`SecretValue` has no string conversion** — CEL `string()` on a `SecretValue` throws `SecretTypeError`. It cannot be coerced.
2. **`SecretValue` has no methods** — no `size()`, `matches()`, `contains()`. Every CEL operation on it (except field access for composite types) throws `SecretTypeError`.
3. **`SecretValue` equality always returns false** — `SECRET.a == SECRET.b` evaluates to `false` (no information leakage via comparison). When `SecretValue` equality evaluation is reached at runtime (indicating an SA rule bypass or dynamic expression), the engine MUST log a WARN-level audit event with `event_type: "secret.equality_attempted"` including the expression location and flow instance ID. This is a defense-in-depth measure — SA rules SHOULD prevent all paths to this code.
4. **`SecretValue` in string concatenation throws** — `'prefix' + SECRET.x` throws `SecretTypeError` (no accidental embedding).

### 7.4 Variable Taint System (`$exportable`, `$secret`)

> Taint metadata applies to all data elements, not just `vars:` entries.

Beyond provider-managed `SECRET.*` values, flow authors can mark local variables with taint metadata to prevent accidental leakage:

#### `$secret: true` on `typedVar`

Declares a variable as a local `SecretValue`. All SA-SECRET rules apply — the variable cannot be logged, returned, emitted, yielded, interpolated, compared, or assigned to another variable. The only valid use is passing it to action params. Implies `$exportable: false`.

#### `$exportable: false` on `typedVar`

Marks a variable as non-exportable. The value is accessible in CEL (read, compare, transform) but blocked from output boundaries: `log:`, `return:`, `emit.data:`, `yield:`, `request.url:`, `throw.message:`. Weaker than `$secret` but allows computation on the value.

#### Hierarchy

`$secret: true` implies `$exportable: false`. Setting both is redundant — SA-TAINT-3 (INFO): "Redundant `$exportable: false` on `$secret: true` variable."

#### Taint Propagation

Taint MUST propagate **automatically** through CEL expressions — if any input to a CEL expression is tainted (secret-derived), the output MUST be treated as tainted. This applies regardless of whether the `$secret` annotation is present on the result. Declared taint via `$secret` remains available for explicit marking, but automatic propagation is the primary enforcement mechanism. Engines MUST implement the following propagation rules:

- If a CEL expression references any variable with `$secret: true`, or any `SECRET.*` value (including resolved `SecretValue` handles), the result MUST inherit `$secret: true` unless the result is assigned to a position where `$declassify: true` is explicitly declared.
- If a CEL expression references any variable with `$exportable: false`, the result MUST inherit `$exportable: false`.
- Taint propagation MUST be transitive: if variable A is tainted, and variable B is derived from A, and variable C is derived from B, then C MUST be tainted — even if B lacks an explicit `$secret` annotation.
- The `$declassify: true` annotation MUST only be permitted on variables that are the direct result of a one-way transformation (hashing, HMAC, HMAC truncation). The `$declassify` annotation MUST include a `via:` field naming the transform function used (e.g., `$declassify: { via: sha256 }`). Static analysis rule SA-TAINT-8 (ERROR) MUST verify that `$declassify` is used only in conjunction with recognized safe-transform CEL functions. The normative list of recognized safe-transform functions is: `sha256()`, `sha384()`, `sha512()`, `sha3_256()`, `sha3_512()`, `hmac()`, `bcrypt()`, `argon2()`, `scrypt()`, `blake2b()`, `blake2s()`. Engine operators MAY extend this list via engine configuration for additional one-way transforms. The engine's `declassify.policy` setting controls enforcement: `DENY` (default) — any flow containing `$declassify` MUST fail SA-TAINT-8 at ERROR; `AUDIT_REQUIRED` — `$declassify` is permitted only when audit logging is active; `WARN_ONLY` — SA-TAINT-8 fires at WARN for backward compatibility during migration.

Flow authors MAY still use explicit `$secret` and `$exportable` annotations for documentation clarity, but they are not required — the engine propagates them automatically. The automatic propagation mechanism takes precedence: even if a flow author omits `$secret` on a derived value, the engine MUST still enforce taint if the value's lineage includes any tainted input.

```yaml
vars:
  auth_token:
    $kind: STRING
    $exportable: false
  # auth_header automatically inherits $exportable: false from auth_token
  auth_header:
    $kind: STRING
do:
  - set:
      auth_header: ="Bearer " + auth_token
```

SA-TAINT-5a (ERROR) enforces that values derived from `$secret: true` sources carry `$secret: true` (verified at compile time). SA-TAINT-5b (ERROR) enforces that values derived from `$exportable: false` sources carry `$exportable: false`. SA-TAINT-11 (WARN) flags action results that may carry secret-derived data without taint annotation. SA-TAINT-7 (ERROR) catches `set:` targets receiving secret-derived CEL expressions without `$secret: true`. Flow authors who intentionally declassify a value MUST use an explicit `$declassify: true` annotation on the `set:` target (see SA-TAINT-8).

> **$declassify audit requirement.** Every use of `$declassify: true` MUST be logged to the engine audit log with the following fields: the **flow ID**, **step ID**, the **secret name** being declassified, and the **target context** (the variable or injection point receiving the declassified value). The audit entry MUST also include the transformation applied and a timestamp. The audit entry MUST NOT contain the secret value or its derivative. Engines MUST disable `$declassify` by default. Operators MUST explicitly enable `$declassify` via engine configuration. When disabled (the default), any flow containing `$declassify` MUST fail static analysis with SA-TAINT-8 at severity ERROR. *(CWE-200: Exposure of Sensitive Information to an Unauthorized Actor)*
>
> **$declassify scope restriction.** `$declassify` MUST only be permitted in steps that have explicit `requires:` declarations for the secret being declassified. If a step attempts to declassify a secret that is not listed in the flow's `requires: { SECRET: [...] }`, static analysis MUST reject it with SA-TAINT-9 (ERROR). `$declassify` in `defaults:` blocks is **prohibited** — declassification is an explicit, per-step decision and MUST NOT be applied as a blanket default. SA-TAINT-13 (ERROR) enforces this prohibition. *(CWE-266: Incorrect Privilege Assignment)*
>
> **$declassify HMAC key provenance.** When `$declassify` is used with `hmac()`, the HMAC key parameter MUST be a `SecretValue` — using a plain variable or literal as the HMAC key undermines the one-way property since an attacker with key knowledge can verify guesses against the HMAC output. SA-SECRET-25 (ERROR) MUST flag `hmac()` calls in `$declassify` context where the key argument is not a `SECRET.*` reference.
>
> **$declassify hash resource limits.** The `bcrypt()`, `argon2()`, and `scrypt()` functions used in `$declassify` context MUST have a configurable per-invocation CPU time limit (default: 500ms, maximum: 5s) to prevent resource exhaustion via repeated expensive hash operations in CEL expressions. *(CWE-400)*

#### Auto-detection

The engine MUST apply content-based credential auto-detection to all action provider output values at resolution time. Detection patterns MUST include: URI with embedded userinfo (`scheme://user:pass@host`), JWT tokens (`eyJ...` prefix with two dots), connection strings containing `://...@`, bearer token patterns, and the engine-configurable API key pattern list. Detected credential values MUST be automatically annotated with `$exportable: false`. *(CWE-200)*

Auto-detected values are treated as `$exportable: false` (not `$secret: true`) to avoid breaking existing flows. The engine MUST log a WARN when auto-detection triggers.

#### Example

```yaml
vars:
  db_url:
    $kind: TEXT
    $exportable: false
    $value: =ENV.DATABASE_URL
  auth_response:
    $kind: JSON
    $secret: true
    $value: null
```

#### SA Rules

| Rule ID | Severity | Description |
|---|---|---|
| SA-TAINT-1 | ERROR | `$secret: true` variable used in output boundary (`log:`, `return:`, `emit.data:`, `yield:`, `request.url:`, `throw.message:`, `assert:`, `switch.value:`, `forEach.items:`, `throw.data:`, `mail.body:`, `mail.subject:`, `mail.to:`, `mail.cc:`, `mail.bcc:`) |
| SA-TAINT-2 | ERROR | `$exportable: false` variable used in output boundary (`log:`, `return:`, `emit.data:`, `yield:`, `request.url:`, `throw.message:`, `assert:`, `switch.value:`, `forEach.items:`, `mail.body:`, `mail.subject:`, `mail.to:`, `mail.cc:`, `mail.bcc:`) |
| SA-TAINT-3 | INFO | Redundant `$exportable: false` on `$secret: true` variable |
| SA-TAINT-4 | ERROR | Result variable receiving auth/credential output from action provider lacks `$secret: true` or `$exportable: false` taint annotation |
| SA-TAINT-5a | ERROR | Derived variable from `$secret: true` source lacks `$secret: true` |
| SA-TAINT-5b | ERROR | Derived variable from `$exportable: false` source lacks `$exportable: false` (CWE-200) |
| SA-TAINT-6 | ERROR | `$sanitized: true` without verified sanitization provenance (CWE-79) |
| SA-TAINT-7 | ERROR | `set:` target receives secret-derived CEL result without `$secret: true` |
| SA-TAINT-8 | ERROR | `$declassify: true` override of secret taint requirement (CWE-200, CWE-778) |
| SA-TAINT-9 | WARN | `$exportable: false` variable in request to non-allowlisted/dynamic URL |
| SA-TAINT-9a | ERROR | Multi-secret `$declassify` lineage incomplete — source secret(s) missing from `$declassify.sources` |
| SA-TAINT-10 | INFO | `$exportable: false` variable passed to `call.params:` |
| SA-TAINT-11 | WARN | Action result taint propagation from secret-derived input (CWE-200) |
| SA-TAINT-12 | WARN | Secret/non-exportable variable written to `GLOBAL.*` scope (CWE-200) |
| SA-TAINT-13 | ERROR | `$declassify` used inside `defaults:` block (CWE-266) |

---

## 8. Inline Secrets — Not Supported

Inline secret definitions in `cap:` (e.g., `cap: { SECRET: { name: { type: text, value: "..." } } }`) are **not supported**. Secrets MUST come from the engine's secrets provider (Vault, AWS Secrets Manager, K8s Secrets, etc.). SA-SECRET-19 (ERROR) rejects inline secret definitions.

The array form `SECRET: [name1, name2]` remains the valid way to pass provider-managed secret references to sub-flows.

---

## 9. Existence Check — `has()` Function

Since `SecretValue` does not support equality or comparison operators, the CEL `has()` macro is the way to check if a secret exists:

```yaml
- if:
    condition: =has(SECRET.api_token)
    then:
      - request:
          url: 'https://api.example.com'
          auth:
            bearer: =SECRET.api_token
    else:
      - log: 'No API token configured — skipping API call'
```

`has(SECRET.name)` returns `true` if the secret exists in the provider AND the flow has capability to access it. Returns `false` otherwise. Does NOT reveal the value.

**Strict vs. optional secrets.** Secrets listed in `requires: { SECRET: [...] }` MUST exist in the provider at flow deployment time. The engine MUST validate secret existence during flow registration and raise `SecretConfigurationError` if any declared secret is not found. For optional secrets that may be absent at runtime, use the `optional:` syntax: `requires: { SECRET: [required_name, { name: optional_name, optional: true }] }`. Optional secrets may be absent; `has()` returns `false` for absent optional secrets. For non-optional (default) secrets, `has(SECRET.name)` always returns `true` at runtime.

For field existence on composite secrets: `has(SECRET.tls_cert.ca_chain)` checks if the optional `ca_chain` field is present.

**Note:** The `has()` macro is part of standard CEL. FlowMarkup documents it in the CEL section of the spec (FLOWMARKUP-SPECIFICATION.md, §2.7) for discoverability.

> **Implementation note:** Engine implementations MUST evaluate `has(SECRET.name)` in constant time regardless of whether the secret exists, is accessible, or the name is valid. Implementation: pre-load the set of accessible secret names at flow initialization; `has()` checks membership in this pre-loaded set using a constant-time comparison (e.g., hash set with constant-time equality). Engines MUST add random jitter (uniform random delay in [0, `secrets.has.jitter_max`], default 1ms) to `has()` responses to mask any residual timing differences from hash-set implementation details, GC pauses, or cache effects. The jitter delay MUST use a CSPRNG source. This prevents timing side-channels that could be used for secret enumeration (CWE-208). In practice, the risk is low because flow authors already know which secrets they declared in `requires:`. The 10% response-time tolerance requirement is specified in [FLOWMARKUP-ENGINE.md](FLOWMARKUP-ENGINE.md) §5.4 item 55.

---

## 10. Complete Examples

### 10.1 Basic API Call with Bearer Token

```yaml
flowmarkup:
  title: Fetch User Data
  version: 1
  requires:
    SECRET: [api_token]
    REQUEST: ["api.example.com"]
  do:
    - request:
        url: 'https://api.example.com/users'
        method: GET
        auth:
          bearer: =SECRET.api_token
        result: users
    - log: "'Fetched ' + users.size() + ' users'"
```

### 10.2 Database Service with Credentials

```yaml
flowmarkup:
  title: Process Orders
  version: 1
  requires:
    SECRET: [db_creds]
    SERVICES: [database]
  do:
    - call:
        service: database
        operation: query
        params:
          username: =SECRET.db_creds.username
          password: =SECRET.db_creds.password
          query: "'SELECT * FROM orders WHERE status = ''pending'''"
        result: pending_orders
    - forEach:
        items: =pending_orders
        as: order
        do:
          - log: "'Processing order ' + order.id"
```

### 10.3 Sub-flow with Selective Secret Grants

```yaml
flowmarkup:
  title: Orchestrator
  version: 1
  requires:
    SECRET: [api_token, db_creds, master_key]
    SUBFLOWS: true
  do:
    # Grant only api_token to processor — not db_creds or master_key
    - run:
        flow: "processor.flowmarkup.yaml"
        cap:
          SECRET: [api_token]
        params:
          data: =payload

    # Grant only api_token and db_creds to billing — not master_key
    - run:
        flow: "billing/charge.flowmarkup.yaml"
        cap:
          SECRET: [api_token, db_creds]
        params:
          order_id: =order.id
```

### 10.4 OAuth2 Client Credentials Flow

```yaml
flowmarkup:
  title: OAuth2 Token Exchange
  version: 1
  requires:
    SECRET: [oauth_creds]
    REQUEST: ["auth.provider.com", "api.provider.com"]
  do:
    # Exchange client credentials for an access token
    - request:
        url: 'https://auth.provider.com/oauth/token'
        method: POST
        auth:
          basic:
            username: =SECRET.oauth_creds.client_id
            password: =SECRET.oauth_creds.client_secret
        body:
          urlencoded:
            grant_type: 'client_credentials'
            scope: 'read:data'
        result:
          name: token_response
          $exportable: false

    # Engine auto-detects JWT pattern and applies $exportable: false. Explicitly annotate for defense-in-depth.
    # Dynamically obtained credentials SHOULD be annotated $exportable: false for defense-in-depth
    - request:
        url: 'https://api.provider.com/data'
        auth:
          bearer: token_response.access_token
        result: api_data
```

### 10.5 Exec with Secret Environment Variables

```yaml
flowmarkup:
  title: Git Clone Private Repo
  version: 1
  requires:
    SECRET: [github_token]
    EXEC: [git]
  do:
    - exec:
        command: git
        args: ['clone', repo_url, '/workspace/repo']
        env:
          GIT_ASKPASS: 'echo'
          GIT_TOKEN: =SECRET.github_token     # injected as env var, not in args
        timeout: "5m"
        result: clone_output
```

### 10.6 Conditional Secret Usage

```yaml
flowmarkup:
  title: Flexible API Client
  version: 1
  description: "Uses secret auth if available, falls back to unauthenticated"
  requires:
    SECRET: [api_token]
    REQUEST: ["api.example.com"]
  do:
    - if:
        condition: =has(SECRET.api_token)
        then:
          - request:
              url: 'https://api.example.com/data'
              auth:
                bearer: =SECRET.api_token
              result: data
        else:
          - request:
              url: 'https://api.example.com/public-data'
              result: data
    - log: "'Fetched data: ' + data.size() + ' records'"
```

### 10.7 Mail with Secret SMTP Credentials

```yaml
flowmarkup:
  title: Send Alert Email
  version: 1
  requires:
    MAIL: true
    SECRET: [smtp_creds]
  do:
    - mail:
        to: 'ops@company.com'
        subject: 'Alert: System Threshold Exceeded'
        body: "'CPU usage is at ' + cpu_percent + '%'"
        smtp:
          host: 'smtp.sendgrid.net'
          port: 587
          tls: STARTTLS
          auth:
            username: =SECRET.smtp_creds.username
            password: =SECRET.smtp_creds.password
```

---

## 11. Prefer `SECRET.*` over `ENV.*` for Credentials

For credential management, prefer `SECRET.*` over `ENV.*` for stronger security guarantees:

### Using `ENV.*` (weaker — no opacity, no taint tracking)

```yaml
flowmarkup:
  title: API Client
  version: 1
  requires:
    ENV: [API_KEY, DB_USER, DB_PASS]
    REQUEST: ["api.example.com"]
    SERVICES: [database]
  do:
    - request:
        url: 'https://api.example.com'
        auth:
          bearer: =ENV.API_KEY
    - call:
        service: database
        operation: connect
        params:
          username: =ENV.DB_USER
          password: =ENV.DB_PASS
```

### Using `SECRET.*` (recommended — opaque, MUST-level redaction)

```yaml
flowmarkup:
  title: API Client
  version: 1
  requires:
    SECRET: [api_token, db_creds]
    REQUEST: ["api.example.com"]
    SERVICES: [database]
  do:
    - request:
        url: 'https://api.example.com'
        auth:
          bearer: =SECRET.api_token
    - call:
        service: database
        operation: connect
        params:
          username: =SECRET.db_creds.username
          password: =SECRET.db_creds.password
```

The engine operator configures the secrets provider to source these from the appropriate backend. The `EnvironmentSecretsProvider` (built-in) can map environment variables to `SECRET.*` names for simple deployments that lack external secret infrastructure.

---

## 12. Security Model Summary

```
┌──────────────────────────────────────────────────┐
│                  Defense in Depth                  │
│                                                    │
│  Layer 1: Capability System (deny-by-default)      │
│  ├── requires: { SECRET: [...] }                  │
│  ├── cap: { SECRET: [...] } on sub-flows          │
│  └── Three-way intersection rule (can't escalate)  │
│                                                    │
│  Layer 2: Type System (opaque SecretValue)          │
│  ├── No string conversion                          │
│  ├── No comparison operators                       │
│  ├── No concatenation                              │
│  ├── No method calls                               │
│  └── Accepted only at action boundaries             │
│                                                    │
│  Layer 3: Static Analysis (compile-time)            │
│  ├── SA-SECRET rules (see FLOWMARKUP-VALIDATION.md)│
│  ├── Variable assignment rejected                  │
│  ├── Log/return/emit/throw rejected                │
│  └── URL embedding / exec args rejected            │
│                                                    │
│  Layer 4: Runtime Redaction (defense-in-depth)      │
│  ├── Resolved values registered with redaction filter│
│  ├── Exact + base64 + URL-encoded + JSON-escaped   │
│  ├── All engine log/trace/error output filtered     │
│  └── Variable inspection shows [SECRET:name]       │
│                                                    │
│  Layer 5: Provider ACL (backend-level)              │
│  ├── Vault policies                                │
│  ├── AWS IAM policies                              │
│  ├── Kubernetes RBAC                               │
│  └── Provider-specific audit logging               │
│                                                    │
└──────────────────────────────────────────────────┘
```

### Post-Quantum Cryptography Readiness

Secret values in transit and at rest face a "harvest now, decrypt later" (HNDL) threat: adversaries may capture encrypted secret material today and decrypt it when cryptographically-relevant quantum computers become available.

**Requirements:**

1. **Transit encryption.** Engines MUST use TLS 1.2 or later for all secret provider communication. Engines SHOULD prefer TLS configurations that support hybrid post-quantum key exchange (e.g., X25519+ML-KEM-768) when available in the TLS library. *(FIPS 203: ML-KEM)* Engines MUST prefer TLS 1.3 for secret provider communication. When TLS 1.2 is used, engines MUST require cipher suites providing forward secrecy (ECDHE or DHE key exchange) and authenticated encryption (GCM or ChaCha20-Poly1305). RSA key exchange and CBC-mode cipher suites MUST be disabled for secret provider connections. *(CWE-326)*

2. **At-rest encryption.** Secret values cached or checkpointed to persistent storage MUST be encrypted using AES-256 (not AES-128). AES-128 provides only 64-bit security against Grover's algorithm, which is insufficient. *(NIST IR 8547 deprecation timeline: AES-128 deprecated after 2030.)* Secret encryption at rest SHOULD use crypto-agile abstractions that decouple the encryption interface from the underlying algorithm.

   > **Migration guidance.** Engines MUST reject AES-128 for new secret encryption operations (cache, checkpoint, envelope). When decrypting existing data encrypted with AES-128, engines SHOULD log a WARNING indicating pre-migration data that should be re-encrypted with AES-256. Engines SHOULD provide a migration utility or background process to re-encrypt AES-128 data at rest.

3. **Algorithm agility.** The secret provider SPI MUST NOT hard-code specific cryptographic algorithms for envelope encryption or key wrapping. Provider implementations MUST support algorithm identifiers in encrypted envelopes to enable migration to post-quantum algorithms without breaking backward compatibility. Current algorithms (AES-256-GCM, ChaCha20-Poly1305) remain acceptable but MUST be wrapped in abstraction layers that allow algorithm substitution without spec changes. Implementations SHOULD plan migration paths to NIST-standardized post-quantum algorithms: **ML-KEM** (Module-Lattice-Based Key Encapsulation Mechanism) for key encapsulation per **FIPS 203**, and **ML-DSA** (Module-Lattice-Based Digital Signature Algorithm) for signatures per **FIPS 204**.

4. **Long-lived secrets.** For secrets with a confidentiality lifetime exceeding 5 years (e.g., root CA keys, long-term signing keys), engines SHOULD recommend or enforce post-quantum-safe storage when the underlying vault supports it.

5. **Hash algorithms.** HMAC-based secret integrity checks MUST use SHA-256 at minimum. For new implementations, SHA-384 is RECOMMENDED to provide comfortable post-quantum collision resistance margins.

---

## 13. Design Decisions & Trade-offs

### Why opaque handles instead of resolved strings?

**Resolved strings** (like Jenkins `withCredentials`) are simpler but rely on best-effort log masking. Encoded forms, substrings, and derived values can leak. FlowMarkup is a specification — we can enforce stronger guarantees because we control the type system.

**Opaque handles** are more restrictive but make leaks *impossible by construction* rather than *unlikely by convention*. The designated injection points cover all legitimate credential patterns. If a new pattern emerges (e.g., HMAC signing), it is added as a new injection point, not by relaxing the opacity.

### Why a new `SECRET.` scope instead of extending `ENV.`?

1. **Separation of concerns** — `ENV.*` is OS environment (config, paths, locale), `SECRET.*` is credentials. Mixing them makes access control coarse-grained.
2. **Type system** — `ENV.*` values are `string`. `SECRET.*` values are `SecretValue` (opaque). Different CEL types, different semantics.
3. **Pluggable backends** — `ENV.*` is always the OS environment. `SECRET.*` is backed by any provider (Vault, AWS SM, etc.).
4. **Coexistence** — existing flows using `ENV.*` for non-secret config continue to work unchanged.

### Why deny-by-default instead of opt-in redaction?

Following the `exec` and `mail` precedent. Secrets are dangerous by nature — the safe default is no access. Flows must explicitly declare `requires: { SECRET: [...] }` and callers must explicitly grant `cap: { SECRET: [...] }`.

### Why per-secret (not per-field) capability grants?

Simplicity. `cap: { SECRET: [db_creds] }` grants access to the entire `db_creds` secret (all fields). Per-field grants (`cap: { SECRET: { db_creds: { fields: [password] } } }`) would add schema complexity for minimal security benefit — if a flow needs a credential, it typically needs all parts.


