Policy enforcement
Requires an enterprise license with the policy_enforcement feature.
A policy is an organisation rule checked before an environment is created. It can refuse.
AF-EE-010 Organization policy no-unmasked-goldens refuses this environment:the golden gv_20260826120000_a1b2c3d4 was published without a verificationattestation. Next: Ask an organization administrator to review no-unmasked-goldens, or bring the repository into compliance.Writing the policy down
Section titled “Writing the policy down”A policy is a YAML document, and the engine reads it from the path in
AF_ORG_POLICY_FILE:
export AF_ORG_POLICY_FILE=/etc/antifailure/policy.yaml# Every key is a restriction. There is no key that grants anything.required_masked_columns: - "*.email" - "customers.card_number"denied_hosts: - api.stripe.comallowed_modes: - block - capture - mocksynth_requires_approval: trueallowed_providers: - neonallowed_regions: - westeuroperequired_masked_columns is table.column with * allowed in either part.
Write the schema too, as public.users.email, when you mean one schema in
particular; a pattern without one names that table in whichever schema holds
it.
The rule is checked against the database’s own catalogue, so it means every
column it names. "*.email" is satisfied when every email column in the
database is masked, and a plan that masks users.email and leaves
contacts.email readable is refused by name. A pattern that matches no column
at all counts as unsatisfied too.
denied_hosts refuses a host named in any mode other than block. A repository
may still write a block rule for one, so that it can document what it
deliberately refuses.
The engine prints which rules are in force at startup, on standard error:
af: organization policy: egress deny list (1 hosts)af: organization policy: required masking (*.email, customers.card_number)A file you named that cannot be read, cannot be parsed, or carries a key this build does not know stops the engine with the reason.
Setting nothing registers nothing and prints nothing, which is the ordinary case for an installation with no organization policy.
Approvals live in the control plane and this file does not carry them, so
synth_requires_approval refuses every synth rule when the engine reads its
policy from a file.
Where it runs
Section titled “Where it runs”Most of the policy is checked before anything is created, not after.
required_masked_columns is the exception: it is checked during a golden refresh
instead, after the engine has read the database’s catalogue and worked out which
columns its rules will rewrite, and before the first row is rewritten. A refusal
there means the golden is never published, and an unverified golden cannot be
branched, so no environment can hold data the policy refused.
One consequence to plan for: a golden published before you tightened the policy
is not re-examined. Refresh the golden after a policy change, with
af golden refresh, and the new rule decides whether it may be published.
The extension points it uses are in the community edition, in
engine/pkg/extension. That is deliberate: the sockets are MIT so that anybody
can write a hook, and the enterprise edition supplies one implementation of
them.
Hooks can only refuse
Section titled “Hooks can only refuse”A hook returns a refusal or nothing. It cannot permit something the engine would otherwise refuse.
Writing one
Section titled “Writing one”type PolicyHook interface { // Returns an error to refuse. Nil permits nothing; it declines to object. Check(ctx context.Context, req EnvironmentRequest) error}
type MaskingHook interface { // Asked during a golden refresh, with the columns a plan will rewrite and // the whole catalogue it read them from. CheckMasking(ctx context.Context, req MaskingRequest) error}A hook may implement either or both. MaskingRequest carries two column lists
and a hook needs both.
Register it with the engine’s extension registry. The community build registers nothing, so each check iterates an empty slice and returns nil.