Constitution
The constitution is the Rego policy TruStacks writes and every proposal is
judged against. It is Apache 2.0, lives in the public
trustacks-policy
repository, and is published as a signed OCI artifact. Free at all tiers.
The runner evaluates it with OPA inside your cluster, after the DevOps Engineer emits a proposal and before anything is written to your platform repo. A proposal with any deny never becomes a pull request.
The rules
Twenty-four rules in three kinds. The kind decides what a rule can do. Eight of them apply only to ECS Fargate targets (Beta), and four only to Kubernetes targets; see Which targets a rule applies to.
| Kind | Can refuse a proposal | Feeds the maturity score |
|---|---|---|
| Gate | Yes | No |
| Behavior | Yes | Yes, as a practice row |
| Posture | No, never | Yes, as a tooling row |
Gate rules
Is the artifact the right shape, in the right place?
| Rule ID | What it checks |
|---|---|
proposal.allowed_paths | Every file lives under gitops/, argo-apps/, or .github/workflows/. |
proposal.no_path_traversal | No path is absolute or contains a .. segment. |
proposal.has_workflow | The proposal includes at least one .github/workflows/*.yaml file. |
proposal.has_helm_chart | Kubernetes, Helm renderer: at least one Chart.yaml under gitops/. |
proposal.has_kustomization | Kubernetes, Kustomize renderer: at least one kustomization.yaml under gitops/. |
proposal.has_argocd_application | Kubernetes: at least one argo-apps/argo-apps-<cluster>/<service>-application.yaml. |
argocd.repoURL_is_canonical | Every ArgoCD Application’s spec.source.repoURL equals your platform repo URL. |
proposal.has_ecs_task_definition | ECS Fargate: a gitops/<application>/<target>/<service>/task-definition.json. |
proposal.has_ecs_deploy_workflow | ECS Fargate: a workflow that deploys the task definition with aws-actions/amazon-ecs-deploy-task-definition. |
Only the renderer check that matches your Application’s renderer applies, and only on a Kubernetes target. A Fargate target has no renderer.
Behavior rules
Is the artifact doing the right thing? Each one maps to a practice key in your environment profile.
| Rule ID | What it checks | Practice |
|---|---|---|
practice.workflow_has_test_step | Every CI workflow has a run: step that runs tests: pytest, npm test, yarn test, pnpm test, dotnet test, go test, or a Maven test/verify or Gradle test/check/build. | tests_in_ci |
practice.workflow_has_lint_step | Every CI workflow has a lint or static-analysis step: ruff, eslint, biome, golangci-lint, go vet, staticcheck, dotnet format, checkstyle, spotless, spotbugs, pmd, or -Dlint. | linting_in_ci |
practice.argocd_prod_requires_manual_sync | Kubernetes: when the proposal’s target cluster has tier prod, its ArgoCD Application has no spec.syncPolicy.automated, whatever the cluster is named. Runners older than 0.2.14-rc25 send no tier, so the rule falls back to argo-apps/argo-apps-prod/ (a cluster named prod). | manual_promotion_to_prod |
practice.dockerfile_runs_as_nonroot | Every Dockerfile in the proposal has a USER that is not root or 0. | non_root_containers |
practice.workflow_pins_action_versions | Every third-party uses: pins a full 40-character commit SHA. actions/* and local ./ actions are exempt. | supply_chain_pinning |
practice.ecs_task_runs_as_nonroot | ECS Fargate: every container sets user, and it is not root, 0, root:* or 0:*. (AWS Security Hub ECS.20) | non_root_containers |
practice.ecs_task_readonly_root_filesystem | ECS Fargate: every container sets readonlyRootFilesystem: true, spelled exactly that way. (ECS.5) | none yet |
practice.ecs_task_secrets_not_in_environment | ECS Fargate: no environment entry whose name or value looks like a credential, and every secrets[].valueFrom is a full Secrets Manager or SSM ARN. (Stricter than ECS.8) | none yet |
practice.ecs_task_image_is_pinned | ECS Fargate: every image has a digest or a non-floating tag (not latest, main, prod and the like, and not untagged). A no-floating-tags check, not a pin: see below. | none yet |
practice.ecs_task_configures_logging | ECS Fargate: every container sets logConfiguration.logDriver. (ECS.9) | none yet |
practice.workflow_prod_deploy_requires_environment | ECS Fargate, prod-tier target: every job that runs amazon-ecs-deploy-task-definition declares a non-empty environment:. | none, deliberately |
The test-step and lint-step rules judge CI workflows. A Fargate deploy workflow applies an artifact and has no suite to run, so it is skipped; the CI workflow beside it is still judged.
Why most Fargate rules score nothing yet. A practice key is a row on
every customer’s score. Adding one that only Fargate can check would add a
row Kubernetes customers could only self-attest, and lower their coverage
for a check that does not apply to them. So these rules refuse bad
Fargate proposals today and score nothing. ecs_task_runs_as_nonroot is
the exception: its intent is the same as the Dockerfile rule’s and it is
enforced in the artifact, so it feeds non_root_containers.
Why the prod-environment rule never scores. It proves the deploy job names a GitHub environment. Whether that environment has required reviewers is a repository setting TruStacks does not read, and on private repositories it depends on your GitHub plan. If the rule’s silence counted as “a human approves promotion to prod: verified”, the score would claim something nobody checked. For a Fargate Application, that practice can only be claimed, by your attestation.
Tags are not image identity. A commit-SHA tag can be overwritten by
anyone who can push to your registry, and ECS resolves the tag when it
deploys. ecs_task_image_is_pinned guarantees there is no floating tag. It
does not guarantee the image that runs is the one that was reviewed. Until
deploys pin a digest, that guarantee does not exist.
Which targets a rule applies to
Every rule declares the target kinds it applies to in its metadata
(target_kinds), beside the tiers it applies to (tier_scope). A rule
without target_kinds applies to every kind. For the constitution rules
below this is descriptive: each checks the kind in its own Rego. For your
own rules the gate enforces it (see
Where your rule applies).
target_kinds | Rules |
|---|---|
kubernetes | proposal.has_helm_chart, proposal.has_kustomization, proposal.has_argocd_application, practice.argocd_prod_requires_manual_sync |
ecs-fargate | proposal.has_ecs_task_definition, proposal.has_ecs_deploy_workflow, the five practice.ecs_task_* rules, practice.workflow_prod_deploy_requires_environment |
| any | everything else |
argocd.repoURL_is_canonical declares no kind, but it only inspects ArgoCD
Applications, so it never fires on a Fargate proposal. The runner tells the
policy which kind and tier a proposal targets (input.context.target). A
target with no kind is Kubernetes, so policy judges older runners exactly as
before. A policy bundle older than 1.3.0 knows nothing about Fargate and
refuses a Fargate proposal for having no ArgoCD Application: it fails
closed.
Posture rules
Do you run the tools? These never refuse a proposal. A missing scanner is a gap in your posture, not a broken artifact, so it shows up in your gap report and your score instead of blocking delivery.
| Rule ID | Category it scores |
|---|---|
posture.image_scanning_declared | image_scanning |
posture.sast_sca_declared | sast_sca |
posture.secret_scanning_declared | secret_scanning |
posture.sbom_signing_declared | sbom_signing |
The naming rule
constitution.overlay_rule_naming checks the IDs of your own overlay rules
against the rule-naming standard. It
judges overlay rule metadata, not proposals.
The Rules page (#/rules) lists every rule your runner reports, with its
description, tooling categories, and Rego source. Filter by Structural
(gate) or Behaviors.
How the score uses the rules
Two rule fields drive the maturity score:
required_tooling_categoriesputs a tooling row on the scorecard. Today that is six categories:ci_cd_platform(fromproposal.has_workflow),gitops_controller(fromproposal.has_argocd_application), and the four posture categories.practice_dimensionsputs a practice row on the scorecard: the five practice keys above. Two rules feednon_root_containers(the Dockerfile rule and the ECS task rule); the other Fargate rules feed none yet.
Gate rules with neither field are enforcement only. They never move your score.
How the policy reaches your runner
The constitution is published from the trustacks-policy repository.
| Artifact | ghcr.io/trustacks/policy/constitution |
| Current version | 1.3.0, also tagged 1 and latest |
| Signed by | https://github.com/TruStacks/trustacks-policy/.github/workflows/publish.yml@refs/tags/v<version> |
| Signing | Sigstore keyless, Rekor transparency log entry per release |
Hosted runners follow the :1 tag. Fixes in the 1.x line reach your runner
on its next start, without a runner upgrade. A 2.0 that needs a newer runner
never arrives on a tag your runner follows.
At start, the runner’s load-policy-bundle init container resolves :1 to a
digest, pulls that digest, and verifies its signature. If anything fails it
falls back to the copy of the constitution baked into the runner image and
reports why. See Policy in force.
Runner versions. Signed-policy pull for hosted runners ships in runner
0.2.14 and later (and pre-releases from 0.2.14-rc24). Earlier runners, including 0.2.13, enforce the
copy baked into the runner image and do not verify a signature at start.
Policy versions independently of the product. The constitution is on the 1.x line; runner and control-plane releases are on 0.2.x. See Versioning.
Verify the policy yourself:
cosign verify \
--certificate-identity-regexp '^https://github\.com/TruStacks/trustacks-policy/\.github/workflows/publish\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/trustacks/policy/constitution:1Your own rules
Overlay rules let you add your own reasons to refuse a proposal. Their denies
are unioned into the constitution’s (data.proposal.deny), so an overlay can
only add refusals. It has no way to cancel a constitution deny.
From runner 0.2.14 this is enforced, not just a property of how the
policy is assembled. Before it evaluates anything, the runner checks that the
overlay only adds to data.overlay. Earlier runners did not check this.
What an overlay may contain
This is what a signed overlay bundle may contain. Rules loaded from your platform repo are narrower: Rego only, tests left out. See Rules from your platform repo.
- Rego files (
*.rego, tests included, because they are loaded too) must declarepackage overlay.<name>. The package is read by OPA’s own parser, not a pattern match; a file OPA does not attribute to a package is a violation. - Data files (
*.json,*.yaml,*.yml) only underoverlay/, so they land indata.overlay. At the root, onlyopa build’sdata.jsonand therule_inventory.jsonthattrustacks rule signadds are allowed. - No symlinks. The tree must be exactly what was signed.
- Nothing else, except a root
.manifestand*.md/*.txtdocs. This is an allowlist: any other file type (a nested*.tar.gz, for example) is refused.
Rule ids must also follow the rule naming standard:
the namespaces argocd, constitution, posture, practice, proposal and
trustacks are reserved for TruStacks.
What a violation looks like
For a signed bundle, the runner gives no verdict for a proposal or gap analysis while the
overlay is unconfined. The run fails with
overlay rejected — overlay rules may only add to data.overlay: <each violation>, and the runner logs policy.overlay_rejected with the
violations. Fix the overlay, re-sign it, and run again.
Rules from your platform repo are handled differently: the run continues under the constitution alone and records why your rules were refused. See Rules from your platform repo.
trustacks rule lint and trustacks rule sign run the same check, so you
see the problem before the runner does, and sign refuses to publish an
unconfined overlay. trustacks rule new --package must start with
overlay.. See the Rule CLI and the
policy linter.
Rule metadata
Required from runner 0.2.19. Earlier runners read description,
required_tooling_categories, practice_dimensions and tier_scope,
and derive the kind.
Every overlay rule declares a rule_metadata map with one key, its rule id,
and these fields:
| Field | Required | Values |
|---|---|---|
description | Yes | One sentence a reviewer can read. |
kind | Yes | "structural" or "behavior". |
required_tooling_categories | Yes | Tooling categories the rule reads from your Stack file (data.env.tooling_categories.*), or []. |
practice_dimensions | For behavior | At least one practice key for a behavior rule. Empty, or left out, for a structural rule. |
tier_scope | Yes | ["any"], or a list of tiers from "dev", "uat" and "prod". |
target_kinds | Yes | ["any"], or a list of target kinds such as "kubernetes" and "ecs-fargate". |
subjects | No | ["proposal"] (the default) or ["proposal", "repo"]. See Your rules in the repo audit. |
"any" can’t be mixed with other values: ["any", "prod"] is refused.
A rule’s PR body (from Propose as PR) calls it one of three things. Each maps to the fields like this:
| You call it | kind | Tooling and practice fields |
|---|---|---|
| A practice rule: how the files a change writes behave | behavior | At least one practice_dimensions entry |
| A posture rule: your Stack file declares a tool | structural | The categories it reads in required_tooling_categories |
| A structural rule: the shape of a change | structural | Neither |
A declared kind is used as declared. A rule doesn’t need a practice.
id to be a behavior rule (it can’t have one: the practice namespace is
reserved).
Where your rule applies
Available from policy 1.11.0 / runner 0.2.21. Runners follow the
policy release on its own; runner 0.2.21 also carries it in its built-in
policy. Before that, tier_scope and target_kinds were descriptive only,
and a rule meant for production had to check input.context.target.tier
in its deny body.
For your rules, the gate enforces tier_scope and target_kinds. A rule’s
denies count only when the proposal’s tier is in its tier_scope and its
target kind is in its target_kinds. Outside that scope the rule is
skipped, and the PR body states the scope. You don’t need to check
input.context.target.tier in the deny body (a rule that still does keeps
working).
["any"]applies everywhere. So does a rule that doesn’t declare the field.- A missing or invalid scope means “any”. A value outside the schema
(
["staging"],[],"prod"not in a list,["any", "prod"], a target kind the constitution doesn’t know) widens the rule to every tier or kind. The rule still enforces, and the value shows as ametadata_invalidwarning. A mistake in the scope never switches a rule off. - No tier means a tier-scoped rule is skipped. When a proposal names no tier, a rule scoped to specific tiers can’t be shown to apply. A proposal with no target kind is Kubernetes, as it is for every constitution rule.
- The repo audit has no deployment target. A rule scoped to a specific
tier or target kind is skipped in the repo audit. A rule
you want in the audit uses
["any"]for both. - Constitution rules keep their own logic. Their
tier_scopeandtarget_kindsdescribe them; each checks tier and kind in its own Rego.
rule_metadata := {
"acme.prod_deploy_requires_approval": {
"description": "Production deploy jobs run in an approval environment",
"kind": "behavior",
"required_tooling_categories": [],
"practice_dimensions": ["prod_deploy_requires_approval"],
"tier_scope": ["prod"],
"target_kinds": ["any"],
},
}
rule_index := rule_metadataRefused when you write it, never switched off when it runs. Metadata problems are caught while you author a rule. Once a rule reaches a runner, a metadata problem never turns it off:
- A value outside the schema (
kind: "posture",tier_scope: ["staging"], abehaviorrule with no practice dimensions, astructuralrule that lists some), or one rule id declared in two packages, is refused bytrustacks rule lint, bytrustacks rule sign, and by the checks on every Coordinator draft. - A required field that isn’t declared is refused by
trustacks rule lintand for a new Coordinator draft. - If such a rule still reaches a runner, through
rules/in your platform repo or a signed bundle, it still loads and enforces. A missing or invalidkindfalls back to the derived kind (behaviorif it lists practice dimensions, otherwisestructural), and each problem shows as a warning (metadata_invalid, or for a missing field a message such asacme.foo: rule_metadata does not declare kind, tier_scope, target_kinds. Its kind was derived as structural, as before.). The warning is in the overlay record onpolicy_check,gap_checkandpolicy_audit, in your runner’s policy state report, and on the rule in the Rules API (warnings). Fix the metadata to clear it.
What does stop an overlay loading is a safety check, not metadata: a rule
outside data.overlay, a compile failure, the size cap, a symlink, or (for
rules/ in your platform repo) one rule id in more than one package. Then
none of the overlay loads, the constitution still applies, and the reason is
recorded on the run. See
Rules from your platform repo.
Overlay rules and your score
Overlay rules are enforced on every proposal, but they do not count toward your maturity headline. The tier, coverage and verified figures are computed from constitution rules alone. Rows that only your overlay rules ask for are listed on Maturity under Your rules, outside the score. An overlay can still lower a constitution row, never raise one. See Maturity scoring.
Rules from your platform repo
Available from runner 0.2.17. Earlier hosted runners load no overlay rules from your repo.
On the hosted service your overlay rules live in your platform repo, under
rules/. Each rule is a directory, the layout trustacks rule new creates:
rules/<rule_id>/<rule_id>.rego # package, rule_metadata, deny
rules/<rule_id>/<rule_id>_test.rego # opa testsOn every run that evaluates policy (a proposal’s policy check and its
advisory gap check, a gap analysis, a rule-impact check, and the
repo audit of an analysis), the runner
reads rules/ from your platform repo’s default branch, at the same commit
it reads environment-profile.yaml from. There are no keys, no registry and
no Helm values to set. The trust anchor is your own Git: branch protection,
pull-request review and history. That is proportionate because an overlay
can only add denies and is confined to data.overlay: the worst a bad rule
can do is block your own proposals, which anyone who can push to your
default branch could already do.
What the runner loads. Rego only. Every *.rego file under rules/
except *_test.rego: tests are for trustacks rule test and your own CI,
not for the gate. JSON and YAML data files under rules/ are not loaded, so
put lists inside the rule itself:
allowed_registries := {"ghcr.io/acme", "us-docker.pkg.dev/acme"}Every file must declare package overlay.<name>.
What the runner checks first. Before it uses any of it, rules/ must:
- stay within 256 files and 1 MiB of Rego (tests are not counted);
- contain no symlinks;
- pass the same confinement check a signed bundle gets (every package
under
overlay., nothing outsidedata.overlay); - compile together with the constitution (
opa check), under the same restricted builtins the runner evaluates with. - declare
rule_metadatavalues within the schema (from runner 0.2.19). See Rule metadata.
All or nothing. If any check fails, none of rules/ loads. The run
continues under the constitution alone, and the reason is recorded on the
run’s policy_check, gap_check or policy_audit event and in the runner log
(policy.repo_overlay_rejected). Nothing is dropped silently. This differs
from a signed bundle, where an unconfined overlay stops the run with no
verdict.
Restricted builtins. Every opa process the runner starts gets an
environment containing only PATH, and the builtins that reach the network,
read the environment, or fetch a schema $ref are removed: http.send,
net.lookup_ip_addr, opa.runtime, json.match_schema,
json.verify_schema and providers.aws.sign_req. A rule that uses one does
not compile, so it is refused at the check above. Your rule can read the
proposal and profile it is judging, and nothing else.
Recorded on every verdict. policy_check, gap_check,
gap_computed and policy_audit carry the overlay that decided them: its source (repo,
bundle or none), the commit SHA it was read from, the rule ids
loaded, and any warnings or violations. The rules appear on the
Rules page as Your overlay.
A signed bundle wins
A signed overlay bundle (policyBundle.overlayRef in the
runner chart) still works. When the runner
has verified and loaded one, the bundle is the overlay and rules/ in
the repo is ignored, with a warning. The two are never merged.
Set overlay.requireSigned: true in the runner chart to refuse the repo
source entirely and load only a signed bundle. It defaults to false.
Your rules in the repo audit
Available from runner 0.2.19. Earlier runners audit a repo with the constitution and a signed overlay bundle only.
Every analysis of a service also audits
the repo’s own delivery files: its Dockerfiles and Containerfiles, its
.github/workflows/*.yml and .yaml files, and its ECS task definitions
(up to 200 files, each up to 256 KiB). Whoever wrote them, a person or an
AI assistant, they are judged by the same rules as a proposal. It is an
audit, not a gate: it never fails the analysis. Its denials arrive on
Signals as findings observed by
trustacks-policy:repo-audit.
Only rules that declare repo in subjects are part of the audit,
constitution and overlay alike. A rule such as the proposal includes a
Helm chart means nothing for a service repo, so it stays
["proposal"]. A repo has no deployment target, so from policy 1.11.0 /
runner 0.2.21 the audit skips any rule of yours scoped to a specific tier or
target kind: give a repo-audited rule tier_scope and target_kinds of
["any"] (see Where your rule applies). To add one of your
rules:
"subjects": ["proposal", "repo"],Declare repo only when a file in the service repo is enough to judge the
rule. In the audit, input.context.subject is "repo" and there is no
deployment target, so a rule that needs input.context.target doesn’t
fire there. An overlay rule that reuses a constitution rule id is not
added.
Which overlay. The audit uses the same selection as a proposal: the
platform repo of the Application the service belongs to, read at the commit
its environment-profile.yaml was read from, with the same checks and
sandbox. A signed bundle the runner has verified still takes precedence,
and overlay.requireSigned: true still refuses the repo source. If your
rules are refused, the platform repo can’t be read, or evaluating your
rules fails, the audit still files the constitution’s findings and says
why: a problem with your rules never hides TruStacks’ own findings. A service that no Application owns yet is
audited with the constitution and any signed bundle.
The policy_audit event records the overlay it used (source, commit SHA,
rules, warnings, violations), the same shape as policy_check. A finding
from one of your rules reads Your overlay rule <rule_id> (the platform
repo’s rules/ at <commit>) denies this file in the repository itself,
or names your signed overlay bundle. A finding from the constitution
names the signed TruStacks policy rule. The runner tags each denial with
the rule set that produced it, so the label always matches its source.
Drafting and testing
- Drafting and proposing. Type
/rule new <id>in the Workspace chat and the Coordinator drafts a rule and its test cases, grounded in your environment profile and the rules already inrules/. TruStacks checks each draft under the runner’s sandbox (compiles with the constitution, confinement, the rule metadata,opa test) before you see it. Propose as PR opens a pull request on your platform repo with the rule, its test and, the first time, a CI job that runsopa testonrules/. A person merges it; TruStacks never does. Needs runner 0.2.18. See Add your own rule. - Authoring and testing.
trustacks rule new,test, andlintwork on a local checkout of your platform repo. See the Rule CLI.
What is not in the constitution
- Rules about approvals, review counts, or who may merge. Merge control belongs to your git provider’s branch protection.
- Rules about credentials, data residency, or citations.
- Waivers. There is no waiver mechanism.
- SOC 2, HIPAA, PCI, or FedRAMP rules. See Specialist Packs.
Where to go next
- Maturity scoring · how rules become a score
- Policy in force · what your runner is actually enforcing
- Proposal lifecycle · where the gate sits
- Supply-chain verification · every artifact and its signing identity