# Known Limitations

This page lists what CAPTF does not do, or does with a catch, as the code stands today. Each item says what the limit is, what follows from it, and where the detail is. A limit that has a workaround says so. For versions and what has and has not been tested, see [Compatibility](<https://captf.io/docs/operator-guide/compatibility/index.md>).

## Maturity

- **Pre-alpha.** No release is published. Every API kind is `v1alpha1`, and the module contract is `v1alpha1` and provisional: it may change before a real module has provisioned a cluster with it. See [Project status](<https://captf.io/docs/#project-status>).
- **No end-to-end run against a live management cluster has happened.** The tests are unit tests that mock the Kubernetes API and Job execution; CI runs unit tests, lint and the offline verifications. Anything that needs a real cluster, a real cloud or a real provider is untested: see [Compatibility](<https://captf.io/docs/operator-guide/compatibility/#what-is-tested>).

## Approvals and gates

- **Only the `TerraformCluster` is fully gated.** `applyPolicy: Manual` and the destructive-plan guard exist on the cluster. A machine’s apply never waits for approval. A pool’s apply is guarded in one case only: when it renders changed cluster exports. See [What is guarded](<https://captf.io/docs/concepts/approvals/limits/#what-is-guarded>).
- **A pool’s exports guard has limits.** A destructive pool apply of changed exports is **held**: the pool keeps applying with the exports of its last successful apply until the change is approved. The guard needs the record of the last applied exports, which shares the durable Secret’s budget with the rendered inputs, and `clusterctl move` re-runs a blocked plan once. Keep shared and destructive infrastructure in the cluster module, keep exports stable, and use `prevent_destroy` on what must not go. See [Machine pools](<https://captf.io/docs/concepts/approvals/destructive-guard/#machine-pools>) and [Cluster outputs reach pools and machines](<https://captf.io/docs/concepts/approvals/limits/#cluster-outputs-reach-pools-and-machines>).
- **An approval binds a plan, not the apply.** It names the plan’s hash and the apply must plan the same again; it does not promise what the provider does. See [An approval binds a plan](<https://captf.io/docs/concepts/approvals/limits/#an-approval-binds-a-plan-not-the-apply>).

## Security

> [!CAUTION]
>
> **A module image can read every Secret in its namespace and forge a plan hash**
>
> - **The module image can read the plan key.** The per-object key that makes plan hashes unforgeable by accident is mounted read-only in the module’s container for plan and approved-apply Jobs. A hostile module can read it and forge a plan hash. It protects against drift, not against the module. See [The plan key is readable by the module](<https://captf.io/docs/concepts/approvals/limits/#the-plan-key-is-readable-by-the-module>).
> - **The runner can read and write every Secret in its namespace.** RBAC cannot scope the state backend’s access. Namespaces are the only tenant boundary. See [Multi-Tenancy](<https://captf.io/docs/operator-guide/multi-tenancy/#the-runner-reads-every-secret-in-its-namespace>).

- **No encryption at rest of its own.** State, inputs and credential mirrors are Kubernetes Secrets. See [No encryption at rest](<https://captf.io/docs/concepts/secret-management/security/#no-encryption-at-rest-of-its-own>).

## State

- **OpenTofu state encryption is unsupported.** An encrypted state reads as `StateReadable=False`/`StateEncrypted`, and no backup of it is ever taken. See [Unreadable State](<https://captf.io/docs/operator-guide/runbooks/state-unreadable/#stateencrypted>).
- **The state file version must be 4.** Another version reads as `StateCorrupt`.
- **Backups are not disaster recovery.** They are in the same namespace, owned by the object, and rotated out after `--state-backups`. See [Disaster Recovery](<https://captf.io/docs/operator-guide/disaster-recovery/index.md>).
- **Owner references are repaired on the next reconcile, not at once.** The controller owns a state chunk, backup, durable inputs Secret and plan key again on the next reconcile that finds no Job running. The gaps are narrow: nothing is re-owned while the object is paused (so `clusterctl   move` is not raced), state chunks wait while a Job holds the run lease, and a restored Secret that still names an old UID can be garbage-collected before the first reconcile. See [Disaster Recovery](<https://captf.io/docs/operator-guide/disaster-recovery/index.md>).

## Jobs and leases

- **The lease grace gap.** If a `TerraformCluster`’s inputs change while it waits for machine operations, the old leases are held by a Job that will never exist, and the new Job waits out the one-minute grace. It resolves itself. See [The known gap](<https://captf.io/docs/concepts/jobs/leases/#the-known-gap>).
- **No flag caps running Jobs.** The concurrency flags cap reconciles per kind, not Jobs. The bounds are one Job per object and your quotas.
- **No quota-specific condition.** A ResourceQuota refusal shows as a reconcile error or a Job that never starts. See [Production Readiness](<https://captf.io/docs/operator-guide/production-readiness/#runner-jobs>).
- **A single manager watches one namespace or all of them.** The leader-election lease name is fixed, so per-namespace manager instances do not work; use `--watch-filter`. See [Namespace scoping](<https://captf.io/docs/operator-guide/configuration/#namespace-scoping-and---watch-filter>).

## Deletion and move

- **A paused object’s deletion waits.** A paused object, or one under a paused Cluster, never runs a destroy. `Deleting` says so. This is by design, so `clusterctl move` can delete source objects. See [Pause stops a deletion](<https://captf.io/docs/concepts/deletion/order/#pause-stops-a-deletion>).
- **A moved object in a deleted namespace looks never-applied.** Status does not move, and the `captf.io/applied` marker is on a Secret the namespace deletion removes. Its finalizer then comes off with nothing destroyed. See [The known limit](<https://captf.io/docs/concepts/deletion/namespaces/#the-known-limit>).
- **The credential source Secret does not move.** Copy it to the target yourself. See [clusterctl move](<https://captf.io/docs/operator-guide/runbooks/move/index.md>).

## Machine pools

- **The Kubernetes Cluster Autoscaler is unsupported on pools.** The controller supports **cloud-native** autoscaling in the module, driven by the pool’s min and max annotations, and writes the observed replicas back to `MachinePool.spec.replicas`. The Cluster Autoscaler’s `clusterapi` provider needs MachinePool Machines and drains nodes before it scales down, and neither exists. Its changes to `spec.replicas` are overwritten by the write-back. See [Machine Pools](<https://captf.io/docs/user-guide/machine-pools/#choose-fixed-replicas-or-autoscaling>).
- **MachinePool Machines are unsupported.** Pool instances have no `Machine` objects, so a `MachineHealthCheck` never selects them.
- **The bootstrap Secret is not watched.** A rotated token reaches the pool at its next reconcile, at the latest one membership-refresh interval later.

> [!NOTE]
>
> **See also**
>
> - [Compatibility](<https://captf.io/docs/operator-guide/compatibility/index.md>).
> - [Production Readiness](<https://captf.io/docs/operator-guide/production-readiness/index.md>).
> - [Approvals and Gates: Limits](<https://captf.io/docs/concepts/approvals/limits/index.md>).
