diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e21e1338..239f0d28 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,8 +22,8 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: github/codeql-action/init@v2 + - uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@v2 - - uses: github/codeql-action/analyze@v2 + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml index ece804bf..d8e2a763 100644 --- a/.github/workflows/trivy.yaml +++ b/.github/workflows/trivy.yaml @@ -26,6 +26,6 @@ jobs: ignore-unfixed: true #severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' diff --git a/generated/routes.json b/generated/routes.json index 23824478..f0d685ed 100644 --- a/generated/routes.json +++ b/generated/routes.json @@ -85,7 +85,7 @@ }, "/getting-started/advanced-config/sandboxing": { "relPath": "/getting-started/advanced-config/sandboxing.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/getting-started/advanced-config/network-configuration": { "relPath": "/getting-started/advanced-config/network-configuration.md", @@ -105,11 +105,11 @@ }, "/api-reference/kubernetes/management-api-reference": { "relPath": "/api-reference/kubernetes/management-api-reference.md", - "lastmod": "2026-06-10T12:45:04.000Z" + "lastmod": "2026-08-13T15:31:21.000Z" }, "/api-reference/kubernetes/agent-api-reference": { "relPath": "/api-reference/kubernetes/agent-api-reference.md", - "lastmod": "2026-06-10T12:45:04.000Z" + "lastmod": "2026-08-11T20:00:52.000Z" }, "/api-reference/graphql": { "relPath": "/api-reference/graphql.md", @@ -121,11 +121,11 @@ }, "/api-reference/terraform": { "relPath": "/api-reference/terraform/index.md", - "lastmod": "2026-08-04T13:27:26.025Z" + "lastmod": "2026-08-04T15:01:58.000Z" }, "/api-reference/terraform/pulumi": { "relPath": "/api-reference/terraform/pulumi.md", - "lastmod": "2026-08-04T13:27:26.046Z" + "lastmod": "2026-08-04T15:01:58.000Z" }, "/plural-features": { "relPath": "/plural-features/index.md", @@ -137,23 +137,23 @@ }, "/plural-features/continuous-deployment/management-controller": { "relPath": "/plural-features/continuous-deployment/management-controller/index.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/plural-features/continuous-deployment/management-controller/deployment-settings": { "relPath": "/plural-features/continuous-deployment/management-controller/deployment-settings.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/plural-features/continuous-deployment/deployment-operator": { "relPath": "/plural-features/continuous-deployment/deployment-operator/index.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/plural-features/continuous-deployment/deployment-operator/agent-configuration": { "relPath": "/plural-features/continuous-deployment/deployment-operator/agent-configuration.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/plural-features/continuous-deployment/deployment-operator/custom-health": { "relPath": "/plural-features/continuous-deployment/deployment-operator/custom-health.md", - "lastmod": "2026-08-13T15:06:28.330Z" + "lastmod": "2026-08-13T15:30:45.000Z" }, "/plural-features/continuous-deployment/git-service": { "relPath": "/plural-features/continuous-deployment/git-service.md", @@ -173,11 +173,11 @@ }, "/plural-features/continuous-deployment/service-templating": { "relPath": "/plural-features/continuous-deployment/service-templating/index.md", - "lastmod": "2026-08-04T13:27:26.172Z" + "lastmod": "2026-08-29T16:19:38.379Z" }, "/plural-features/continuous-deployment/service-templating/supporting-liquid-filters": { "relPath": "/plural-features/continuous-deployment/service-templating/supporting-liquid-filters.md", - "lastmod": "2026-08-04T13:27:26.192Z" + "lastmod": "2026-08-29T16:19:38.402Z" }, "/plural-features/continuous-deployment/lua": { "relPath": "/plural-features/continuous-deployment/lua.md", @@ -221,7 +221,7 @@ }, "/plural-features/stacks-iac-management": { "relPath": "/plural-features/stacks-iac-management/index.md", - "lastmod": "2026-07-15T09:31:13.000Z" + "lastmod": "2026-08-04T15:01:58.000Z" }, "/plural-features/stacks-iac-management/customize-runners": { "relPath": "/plural-features/stacks-iac-management/customize-runners.md", @@ -229,7 +229,7 @@ }, "/plural-features/stacks-iac-management/pulumi": { "relPath": "/plural-features/stacks-iac-management/pulumi.md", - "lastmod": "2026-07-15T09:31:13.000Z" + "lastmod": "2026-08-04T15:01:58.000Z" }, "/plural-features/stacks-iac-management/pr-workflow": { "relPath": "/plural-features/stacks-iac-management/pr-workflow.md", @@ -293,7 +293,7 @@ }, "/plural-features/plural-ai/ai-agent/configure-agent": { "relPath": "/plural-features/plural-ai/ai-agent/configure-agent.md", - "lastmod": "2026-03-04T02:33:39.000Z" + "lastmod": "2026-08-29T16:16:14.000Z" }, "/plural-features/plural-ai/ai-agent/remote-browser": { "relPath": "/plural-features/plural-ai/ai-agent/remote-browser.md", @@ -345,11 +345,11 @@ }, "/plural-features/workbenches": { "relPath": "/plural-features/workbenches/index.md", - "lastmod": "2026-07-31T10:05:04.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/configuration": { "relPath": "/plural-features/workbenches/configuration.md", - "lastmod": "2026-07-31T10:05:04.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/coding-agent": { "relPath": "/plural-features/workbenches/coding-agent.md", @@ -357,28 +357,48 @@ }, "/plural-features/workbenches/tools": { "relPath": "/plural-features/workbenches/tools.md", - "lastmod": "2026-07-31T10:05:04.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/tools/datadog": { "relPath": "/plural-features/workbenches/tools/datadog.md", - "lastmod": "2026-07-31T10:05:04.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/running-jobs": { "relPath": "/plural-features/workbenches/running-jobs.md", - "lastmod": "2026-07-31T10:05:04.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/automation": { "relPath": "/plural-features/workbenches/automation.md", - "lastmod": "2026-07-30T14:01:51.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/follow-up-automation": { "relPath": "/plural-features/workbenches/follow-up-automation.md", - "lastmod": "2026-07-30T15:19:23.000Z" + "lastmod": "2026-08-06T15:20:26.000Z" }, "/plural-features/workbenches/use-cases": { "relPath": "/plural-features/workbenches/use-cases.md", "lastmod": "2026-05-27T21:33:58.000Z" }, + "/plural-features/policy-management": { + "relPath": "/plural-features/policy-management/index.md", + "lastmod": "2026-08-29T16:16:14.000Z" + }, + "/plural-features/policy-management/stack-policies": { + "relPath": "/plural-features/policy-management/stack-policies.md", + "lastmod": "2026-08-29T16:16:14.000Z" + }, + "/plural-features/policy-management/workbench-policies": { + "relPath": "/plural-features/policy-management/workbench-policies.md", + "lastmod": "2026-08-29T16:16:14.000Z" + }, + "/plural-features/policy-management/simulating-policies": { + "relPath": "/plural-features/policy-management/simulating-policies.md", + "lastmod": "2026-08-29T16:16:14.000Z" + }, + "/plural-features/policy-management/common-use-cases": { + "relPath": "/plural-features/policy-management/common-use-cases.md", + "lastmod": "2026-08-29T16:16:14.000Z" + }, "/plural-features/observability": { "relPath": "/plural-features/observability/index.md", "lastmod": "2025-05-10T04:27:39.000Z" @@ -557,7 +577,7 @@ }, "/deployments/sandboxing": { "relPath": "/getting-started/advanced-config/sandboxing.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/deployments/network-configuration": { "relPath": "/getting-started/advanced-config/network-configuration.md", @@ -569,11 +589,11 @@ }, "/deployments/operator/architecture": { "relPath": "/plural-features/continuous-deployment/deployment-operator/index.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/plural-features/continuous-deployment/deployment-operator/deployment-settings": { "relPath": "/plural-features/continuous-deployment/management-controller/deployment-settings.md", - "lastmod": "2026-06-26T10:42:27.000Z" + "lastmod": "2026-08-06T14:42:58.000Z" }, "/deployments/operator/git-service": { "relPath": "/plural-features/continuous-deployment/git-service.md", @@ -625,7 +645,7 @@ }, "/deployments/stacks": { "relPath": "/plural-features/stacks-iac-management/index.md", - "lastmod": "2026-07-15T09:31:13.000Z" + "lastmod": "2026-08-04T15:01:58.000Z" }, "/service-catalog/creation": { "relPath": "/plural-features/service-catalog/creation.md", @@ -697,10 +717,10 @@ }, "/management-api-reference": { "relPath": "/api-reference/kubernetes/management-api-reference.md", - "lastmod": "2026-06-10T12:45:04.000Z" + "lastmod": "2026-08-13T15:31:21.000Z" }, "/agent-api-reference": { "relPath": "/api-reference/kubernetes/agent-api-reference.md", - "lastmod": "2026-06-10T12:45:04.000Z" + "lastmod": "2026-08-11T20:00:52.000Z" } } \ No newline at end of file diff --git a/package.json b/package.json index 7fb3d47d..2c929f05 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,14 @@ "@react-stately/list": "3.8.1", "@react-stately/select": "3.5.1", "@react-stately/selection": "3.19.0", + "@styra/highlightjs-rego": "^0.1.2", "chroma-js": "2.4.2", "classnames": "2.3.2", "deep-freeze": "0.0.1", "fuse.js": "6.6.2", "graphql": "16.6.0", "gray-matter": "4.0.3", + "highlight.js": "11.9.0", "honorable": "0.194.0", "honorable-theme-default": "0.77.0", "htmlparser2": "9.1.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 9a3dd732..484d63b1 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -25,6 +25,7 @@ import { useRouter } from 'next/router' import { until } from '@open-draft/until' import { MarkdocContextProvider } from '@pluralsh/design-system/dist/markdoc' import { SSRProvider } from '@react-aria/ssr' +import '@src/highlight' import '@src/styles/globals.css' import styled, { ThemeProvider as StyledThemeProvider } from 'styled-components' import { SWRConfig } from 'swr' diff --git a/pages/plural-features/plural-ai/ai-agent/configure-agent.md b/pages/plural-features/plural-ai/ai-agent/configure-agent.md index 90e3131d..8bd1b3f6 100644 --- a/pages/plural-features/plural-ai/ai-agent/configure-agent.md +++ b/pages/plural-features/plural-ai/ai-agent/configure-agent.md @@ -34,6 +34,19 @@ spec: Supported runtime types are `CLAUDE`, `OPENCODE`, and `GEMINI`. +## Optional: Enable codebase memory persistence + +Agent runtimes include the `codebase-memory-mcp` server for graph-backed code search. By default, its indexes are stored only in the agent pod cache and generated `.codebase-memory/` artifacts are excluded from commits. + +Set `spec.memory: true` to enable team-shared memory persistence by default: + +```yaml +spec: + memory: true +``` + +When enabled, agents index repositories with persistent artifact export enabled, allowing `.codebase-memory/graph.db.zst` and the related `.gitattributes` update to be committed so future runs can bootstrap from the shared graph. Leave this unset or `false` if you want every run to keep its codebase-memory index local to the runtime cache. + ## Tune runtime resources `AgentRuntime` accepts a pod template so you can set CPU/memory requests and limits for the agent container. Use the `default` container name to target the main agent container. diff --git a/pages/plural-features/policy-management/common-use-cases.md b/pages/plural-features/policy-management/common-use-cases.md new file mode 100644 index 00000000..cf045505 --- /dev/null +++ b/pages/plural-features/policy-management/common-use-cases.md @@ -0,0 +1,94 @@ +--- +title: Common policy use cases +description: Patterns for governing workbench tools and infrastructure stack approvals +--- + +Policies are most useful when they encode a narrow, explainable rule around a high-impact operation. Start with guardrails that can be evaluated from explicit input fields, then expand coverage as you observe real evaluations in the simulator. + +## Workbench policies + +### Extend authorization for external tools + +Many external systems have coarse authorization models: a credential may allow access to an entire observability account, log index, or API even when a user only needs a subset. Workbench policies can add request-level controls without issuing a separate credential for every user and use case. + +Examples include: + +- Restricting production log searches to an incident-response group +- Denying queries against sensitive audit or customer-data indices +- Limiting observability queries to approved accounts, services, or time ranges +- Preventing write operations through an external API while permitting reads + +Policy input includes both the actor and tool arguments, allowing the decision to account for who is requesting the action and exactly what the agent plans to send. + +### Increase autonomy with safe-action allowlists + +Agents are most useful when routine, reversible actions can proceed without waiting for a human. Add `approve` rules for a well-defined set of safe operations while leaving everything else on the normal approval path. + +Examples include: + +- Approving restarts of stateless workloads outside protected namespaces +- Approving read-only diagnostic or observability calls +- Allowing an SRE group to update non-production resources +- Automatically posting summaries to a designated incident channel + +Prefer explicit conditions on tool name and arguments over broad actor-only approvals. A narrow allowlist keeps autonomy predictable as new tools and operations are added. + +### Guarantee operational best practices + +Workbench policies apply the same safeguards to every matching workbench, regardless of its prompt or the model running it. + +Examples include: + +- Blocking Kubernetes deletes in system namespaces +- Requiring production mutations to originate from an approved group +- Denying changes that omit required ownership, ticket, or incident metadata +- Preventing access to regulated datasets from general-purpose workbenches + +Use binding policies to attach these guardrails automatically based on workbench naming or metadata conventions. + +## Stack policies + +### Enforce change-management requirements + +Inspect the actor, stack, commit, run type, and plan to require the evidence your organization expects before infrastructure changes proceed. + +Examples include: + +- Rejecting production applies without an approved change reference +- Restricting destroy runs to a designated operations group +- Requiring sensitive stacks to follow a specific repository or branch workflow +- Blocking changes during a freeze window when the required context is present in policy input + +### Streamline approval of known-safe plans + +Stack policies can automatically approve plans that fit a constrained risk profile and leave all other plans to a human or AI reviewer. + +Examples include: + +- Approving tag-only updates +- Approving additive changes to an allowed set of resource types +- Approving non-production plans below a defined size +- Approving EKS plans only when they do not destroy clusters or node groups and do not update the control-plane version + +Model the safe case explicitly. If a plan does not satisfy every condition, return no approval and let the configured approval workflow handle it. + +### Enforce compliance at scale + +Evaluate every matching Terraform plan against controls derived from internal standards or regulatory frameworks. + +Examples include: + +- Requiring encryption and approved key-management configuration +- Denying public network exposure for protected workloads +- Enforcing required tags, retention settings, and backup policies +- Restricting resource types, providers, regions, or machine classes + +Combine reusable stack policies with binding policies to apply controls across projects and stack families. Include a clear denial message that identifies the failed requirement and the expected remediation. + +## Design recommendations + +- Keep each rule focused on one decision and provide a specific reason. +- Use `deny` for requirements that must never be bypassed. +- Use `approve` only for operations whose complete safe boundary can be expressed from the available input. +- Leave uncertain cases undecided so existing human or AI approval remains in control. +- Test boundary conditions in Rego and replay representative live inputs in the [policy simulator](/plural-features/policy-management/simulating-policies). diff --git a/pages/plural-features/policy-management/index.md b/pages/plural-features/policy-management/index.md new file mode 100644 index 00000000..b50a8ff6 --- /dev/null +++ b/pages/plural-features/policy-management/index.md @@ -0,0 +1,66 @@ +--- +title: Policy enforcement +description: Extend Plural authorization and approval workflows with policy as code +--- + +Plural policies let platform and security teams encode enterprise guardrails in [Rego](https://www.openpolicyagent.org/docs/policy-language). Policies are evaluated alongside Plural's built-in RBAC and approval controls, adding organization-specific authorization without replacing the permissions you already configured. + +Use policy enforcement to: + +- Allow or deny workbench tool calls based on the actor, tool, and arguments +- Automatically approve known-safe workbench operations +- Approve or reject stack runs from the contents of an infrastructure plan +- Apply policies consistently to matching workbenches or stacks +- Test proposed policy changes against inputs captured from live evaluations + +{% callout severity="info" %} +Policies only add constraints or automate an existing approval step. They do not grant access that the actor or workbench does not already have through Plural RBAC and tool permissions. +{% /callout %} + +## Policy types + +| Type | Rego package | Purpose | +|---|---|---| +| Workbench | `plrl.wb.admission` | Deny tool calls or automatically approve operations that require approval | +| Stack | `plrl.stack` | Approve or reject stack runs from plan, stack, commit, and actor data | +| Binding | `plrl.binding` | Select which workbenches or stacks receive another policy | + +Workbench and stack policies return decisions. Binding policies return a `bind` decision and connect those enforcement policies to matching resources. This separation lets you reuse one guardrail across many workbenches or stacks without attaching it to each resource manually. + +## Decision model + +Workbench and stack policies can add objects to the `deny` and `approve` sets: + +```rego +deny[{"msg": "explain why the operation is blocked"}] if { + # conditions +} + +approve[{"reason": "explain why the operation is safe"}] if { + # conditions +} +``` + +A denial takes precedence over an approval. When no rule produces a decision: + +- A workbench tool call continues through its normal authorization and approval path. +- A stack run continues to its configured human or AI approval path. + + +## Managing policies + +Open **Security → Policies** in the Plural Console to create and edit policies, inspect attachments, review evaluations, and run simulations. Policies are project-scoped, allowing each project to apply guardrails appropriate to its resources and teams. + +For a GitOps workflow, store policies and tests in Git and publish them with the [Plural Terraform provider](https://registry.terraform.io/providers/pluralsh/plural/latest/docs). The [Plural policy examples repository](https://github.com/pluralsh/policy-examples) demonstrates the complete workflow, including: + +- Workbench, stack, and binding policies +- Rego unit tests and CI +- Terraform resources that publish and bind policies +- A Plural `InfrastructureStack` that deploys the configuration + +## Next steps + +- [Stack policies](/plural-features/policy-management/stack-policies) +- [Workbench policies](/plural-features/policy-management/workbench-policies) +- [Simulating and testing policies](/plural-features/policy-management/simulating-policies) +- [Common use cases](/plural-features/policy-management/common-use-cases) diff --git a/pages/plural-features/policy-management/simulating-policies.md b/pages/plural-features/policy-management/simulating-policies.md new file mode 100644 index 00000000..d86285b9 --- /dev/null +++ b/pages/plural-features/policy-management/simulating-policies.md @@ -0,0 +1,139 @@ +--- +title: Simulating and testing policies +description: Validate Rego policies with live evaluation data and automated tests +--- + +Test policies at two levels: + +- Use the Plural policy simulator to evaluate an editor draft against representative or previously observed input. +- Keep Rego unit tests beside each policy in Git and run them in CI before publishing changes. + +## Simulate a policy in Plural + +Open **Security → Policies**, select a policy, and open its **Definition** tab. The simulator appears next to the Rego editor. + +![](/assets/policy-management/policy-simulator.png) + +To run a simulation: + +1. Select an item from **Past evals** to load input captured from a live policy evaluation. +2. Review or edit the JSON under **Input**. +3. Update the Rego in the editor if you want to test a proposed change. +4. Click **Run simulation** and inspect the decision and JSON under **Output**. + +Simulations run against the current editor buffer, even when it has not been saved. This lets you validate a change before replacing the active policy. + +{% callout severity="info" %} +Past evaluations are sampled for debugging and auditability, so the list is not a complete record of every policy evaluation. +{% /callout %} + +The output depends on the policy type: + +| Type | Relevant output | +|---|---| +| Workbench | `deny` and `approve` decision arrays | +| Stack | `deny`, `approve`, and `defer` | +| Binding | Boolean `bind` decision | + +For stack policies, inspect the raw output when testing automatic approval. An output without a denial is allowed by the simulator, while an `approve` entry is what causes the stack runtime to automatically approve a run. + +## Test Rego in a repository + +The [policy examples repository](https://github.com/pluralsh/policy-examples) uses this layout: + +```text +. +├── .github/workflows/test.yaml +├── policies/ +│ ├── binding/ +│ ├── stack/ +│ └── workbench/ +└── terraform/ + ├── main.tf + ├── stack.tf + ├── workbench.tf + └── policies -> ../policies +``` + +Place tests next to their policy and name them `*_test.rego`. Use the same package as the policy and evaluate its rules with a supplied input: + +```rego +package plrl.wb.admission + +test_non_sre_cannot_delete_from_kube_system if { + deny[{"msg": "deleting resources in the kube-system namespace is not allowed"}] with input as { + "actor": {"groups": ["developers"]}, + "tool_name": "delete_k8s_resource", + "tool": {"namespace": "kube-system"}, + } +} + +test_sre_update_is_approved if { + approve[{"reason": "SREs may update resources outside the kube-system namespace"}] with input as { + "actor": {"groups": ["sre"]}, + "tool_name": "update_k8s_resource", + "tool": {"namespace": "production"}, + } +} +``` + +Run formatting and tests locally with the [OPA CLI](https://www.openpolicyagent.org/docs/latest/cli/): + +```shell +opa fmt --fail policies +opa test --verbose policies +``` + +Run the same checks in CI: + +```yaml +name: Test policies + +on: + pull_request: + push: + branches: + - main + +jobs: + rego: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: open-policy-agent/setup-opa@v2 + with: + version: latest + - run: opa fmt --fail policies + - run: opa test --verbose policies +``` + +Include positive, negative, and undecided cases. For stack policies, cover destructive and replacement actions as well as missing `before` or `after` objects. For workbench policies, test relevant actor groups, tool names, and argument boundaries. + +## Publish through a Plural stack + +Manage policies as code by declaring `plural_policy` and `plural_binding_policy` resources in Terraform, then deploying that directory with an `InfrastructureStack`: + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: InfrastructureStack +metadata: + name: policy-management + namespace: infra +spec: + name: policy-management + type: TERRAFORM + approval: true + manageState: true + actor: console@plural.sh + clusterRef: + name: mgmt + namespace: infra + repositoryRef: + name: policy-management + namespace: infra + git: + ref: main + folder: terraform +``` + +When the stack checks out only the configured Git folder, keep policy files inside that folder or symlink them into it as shown in the example repository. diff --git a/pages/plural-features/policy-management/stack-policies.md b/pages/plural-features/policy-management/stack-policies.md new file mode 100644 index 00000000..572f4005 --- /dev/null +++ b/pages/plural-features/policy-management/stack-policies.md @@ -0,0 +1,149 @@ +--- +title: Stack policies +description: Enforce and automate infrastructure stack approvals with Rego +--- + +Stack policies evaluate infrastructure plans before an approval-gated stack run proceeds. They can reject a run that violates a guardrail, automatically approve a known-safe plan, or leave the run undecided so it continues to the configured human or AI approval flow. + +Stack policies use the `plrl.stack` Rego package. + +## Supported input + +Plural evaluates a stack policy with the following top-level input: + +| Field | Description | +|---|---| +| `input.plan` | A reduced Terraform plan containing `terraform_version` and `resource_changes` | +| `input.run_type` | The run operation: `plan`, `apply`, or `destroy` | +| `input.stack` | Stack metadata, including its name, project, and Git configuration | +| `input.commit` | Metadata for the commit associated with the run | +| `input.actor` | The initiating user, including `id`, `name`, `email`, and `groups` when available | + +Each entry in `input.plan.resource_changes` contains: + +| Field | Description | +|---|---| +| `address` | Full Terraform resource address | +| `type` | Terraform resource type, such as `aws_eks_cluster` | +| `name` | Resource name | +| `provider` | Short provider name | +| `change.actions` | Planned actions, such as `create`, `update`, `delete`, or `replace` | +| `change.before` | Resource state before the run | +| `change.after` | Expected resource state after the run | + +## Decisions + +A stack policy can produce: + +- `deny[{"msg": "..."}]`: reject the stack run +- `approve[{"reason": "..."}]`: approve the stack run +- `defer`: leave the decision to the next configured approval step + +If a policy produces neither a denial nor an approval, Plural also continues to the configured approval flow. This is useful for policies that only auto-approve a narrowly defined set of safe plans. + +## Full example + +The following policy denies destructive EKS changes and control-plane upgrades that skip more than one Kubernetes minor version. Plans without destructive changes or any version update are automatically approved, while single-minor upgrades continue to review: + +```rego +package plrl.stack + +eks_types := {"aws_eks_cluster", "aws_eks_node_group"} + +destructive_actions := {"delete", "replace"} + +destructive_eks_change if { + some rc in input.plan.resource_changes + eks_types[rc.type] + some action in rc.change.actions + destructive_actions[action] +} + +cluster_version_update if { + some rc in input.plan.resource_changes + rc.type == "aws_eks_cluster" + is_object(rc.change.before) + is_object(rc.change.after) + rc.change.before.version != rc.change.after.version +} + +version_number(version) := numeric_version if { + parts := split(version, ".") + major := to_number(parts[0]) + minor := to_number(parts[1]) + numeric_version := major * 1000 + minor +} + +cluster_version_skips_minor if { + some rc in input.plan.resource_changes + rc.type == "aws_eks_cluster" + is_object(rc.change.before) + is_object(rc.change.after) + before_version := version_number(rc.change.before.version) + after_version := version_number(rc.change.after.version) + after_version > before_version + 1 +} + +deny[{"msg": "destroying or replacing EKS clusters and node groups is not allowed"}] if { + destructive_eks_change +} + +deny[{"msg": "EKS control-plane upgrades cannot skip Kubernetes minor versions"}] if { + cluster_version_skips_minor +} + +approve[{"reason": "no destructive EKS cluster or node group changes and no cluster version update"}] if { + not destructive_eks_change + not cluster_version_update +} +``` + +The `deny` rules reject prohibited plans and record a specific explanation. A one-minor control-plane upgrade is not denied, but it also does not match the approval rule, so it continues to human or AI review. Plans without an EKS version change or destructive EKS action receive an explicit approval. + +## Attach a policy to stacks + +You can attach a stack policy in either of two ways: + +1. Open **Security → Policies**, select the stack policy, and use the **Attachments** tab to attach it to a stack. +2. Create a binding policy that selects stacks dynamically, then connect the binding policy to the stack policy. + +A binding policy uses `plrl.binding` and returns `bind`: + +```rego +package plrl.binding + +bind if { + startswith(input.stack.name, "cluster-") +} +``` + +Plural evaluates binding policies when matching resources change and on their configured interval. A `true` result attaches the enforcement policy; a `false` result removes an attachment previously managed by that binding. + +The same configuration can be managed with Terraform: + +```hcl +resource "plural_policy" "eks_guardrails" { + name = "eks-guardrails" + type = "STACK" + description = "Reject unsafe EKS changes and auto-approve plans without destructive or version changes." + project_id = data.plural_project.project.id + policy = file("${path.module}/policies/stack/eks_guardrails.rego") +} + +resource "plural_policy" "cluster_stacks" { + name = "cluster-stacks" + type = "BINDING" + description = "Select stacks whose names begin with cluster-." + project_id = data.plural_project.project.id + policy = file("${path.module}/policies/binding/cluster_stacks.rego") +} + +resource "plural_binding_policy" "eks_guardrails_for_clusters" { + policy_id = plural_policy.eks_guardrails.id + bind_policy_id = plural_policy.cluster_stacks.id + type = "STACK" + interval = "6h" +} +``` + +See the [complete stack example](https://github.com/pluralsh/policy-examples/tree/main/policies/stack) for its tests and deployment configuration. diff --git a/pages/plural-features/policy-management/workbench-policies.md b/pages/plural-features/policy-management/workbench-policies.md new file mode 100644 index 00000000..6b3e4360 --- /dev/null +++ b/pages/plural-features/policy-management/workbench-policies.md @@ -0,0 +1,105 @@ +--- +title: Workbench policies +description: Control and approve workbench tool calls with Rego guardrails +--- + +Workbench policies evaluate each matching tool call made by a workbench agent. They extend Plural RBAC and tool permissions with authorization rules that understand the current actor, the requested tool, and its arguments. + +Workbench policies use the `plrl.wb.admission` Rego package. + +{% callout severity="info" %} +A policy cannot make an unavailable tool accessible or grant permissions the actor does not already have. It adds guardrails to the existing workbench authorization model. +{% /callout %} + +## Supported input + +| Field | Description | +|---|---| +| `input.tool_name` | Name of the tool being called | +| `input.tool` | Arguments supplied to the tool, represented as an object | +| `input.actor` | Current user, including `id`, `name`, `email`, and a `groups` array when available | + +The shape of `input.tool` depends on the tool. For example, a Kubernetes operation can include a `namespace`, while a logging tool can include an index and query. Select a past evaluation in the [policy simulator](/plural-features/policy-management/simulating-policies) to inspect the real input for a tool before writing rules against it. + +## Decisions + +A workbench policy can produce: + +- `deny[{"msg": "..."}]`: block the tool call and return the denial message +- `approve[{"reason": "..."}]`: automatically approve a tool that supports approval and record the reason + +A denial takes precedence when multiple rules or attached policies produce decisions. If no rule produces a decision, the tool continues through its normal authorization and approval path. + +## Full example + +This policy blocks non-SRE users from deleting resources in `kube-system` and automatically approves SRE updates outside that namespace: + +```rego +package plrl.wb.admission + +actor_is_sre if { + input.actor.groups[_] == "sre" +} + +deny[{"msg": "deleting resources in the kube-system namespace is not allowed"}] if { + input.tool_name == "delete_k8s_resource" + input.tool.namespace == "kube-system" + not actor_is_sre +} + +approve[{"reason": "SREs may update resources outside the kube-system namespace"}] if { + input.tool_name == "update_k8s_resource" + input.tool.namespace != "kube-system" + actor_is_sre +} +``` + +Use exact tool names and validate the argument shape from a real evaluation. Different integrations can model similar operations with different fields. + +## Attach a policy to workbenches + +You can attach a workbench policy in either of two ways: + +1. Open **Security → Policies**, select the workbench policy, and use the **Attachments** tab to attach it to a workbench. Configure tool matches on the attachment when the policy should only evaluate selected tool names. +2. Use a binding policy to attach it automatically to workbenches matching a naming or metadata convention. + +For example, this binding policy selects workbenches whose names start with `demo-`: + +```rego +package plrl.binding + +bind if { + startswith(input.workbench.name, "demo-") +} +``` + +Plural evaluates binding policies when workbenches change and on their configured interval. A `true` result attaches the enforcement policy; a `false` result removes an attachment previously managed by that binding. + +The policy and binding can be managed with Terraform: + +```hcl +resource "plural_policy" "kubernetes_guardrails" { + name = "kubernetes-guardrails" + type = "WORKBENCH" + description = "Guard Kubernetes deletes and approve safe SRE updates." + project_id = data.plural_project.project.id + policy = file("${path.module}/policies/workbench/kubernetes_guardrails.rego") +} + +resource "plural_policy" "demo_workbenches" { + name = "demo-workbenches" + type = "BINDING" + description = "Select workbenches whose names begin with demo-." + project_id = data.plural_project.project.id + policy = file("${path.module}/policies/binding/demo_workbenches.rego") +} + +resource "plural_binding_policy" "kubernetes_guardrails_for_demos" { + policy_id = plural_policy.kubernetes_guardrails.id + bind_policy_id = plural_policy.demo_workbenches.id + type = "WORKBENCH" + interval = "6h" +} +``` + +See the [complete workbench example](https://github.com/pluralsh/policy-examples/tree/main/policies/workbench) for its tests and deployment configuration. diff --git a/public/assets/policy-management/policy-simulator.png b/public/assets/policy-management/policy-simulator.png new file mode 100644 index 00000000..f05df9aa Binary files /dev/null and b/public/assets/policy-management/policy-simulator.png differ diff --git a/src/components/GlobalStyles.tsx b/src/components/GlobalStyles.tsx index c6e06c9e..2f7704bf 100644 --- a/src/components/GlobalStyles.tsx +++ b/src/components/GlobalStyles.tsx @@ -27,6 +27,13 @@ const GlobalStyles = createGlobalStyle(({ theme }) => ({ color: 'unset', textDecoration: 'unset', }, + '.primary-content table td': { + flexDirection: 'row', + flexWrap: 'wrap', + alignContent: 'center', + alignItems: 'center', + justifyContent: 'flex-start', + }, html: { ...fillAvailable('height'), }, diff --git a/src/highlight.ts b/src/highlight.ts new file mode 100644 index 00000000..2e57495d --- /dev/null +++ b/src/highlight.ts @@ -0,0 +1,6 @@ +import rego from '@styra/highlightjs-rego' +import hljs from 'highlight.js/lib/core' + +if (!hljs.getLanguage('rego')) { + hljs.registerLanguage('rego', rego) +} diff --git a/src/routing/docs-structure.ts b/src/routing/docs-structure.ts index fb3a196f..f7708e28 100644 --- a/src/routing/docs-structure.ts +++ b/src/routing/docs-structure.ts @@ -279,6 +279,19 @@ export const docsStructure: DocSection[] = [ { path: 'use-cases', title: 'Common use cases' }, ], }, + { + path: 'policy-management', + title: 'Policy management', + sections: [ + { path: 'stack-policies', title: 'Stack policies' }, + { path: 'workbench-policies', title: 'Workbench policies' }, + { + path: 'simulating-policies', + title: 'Simulating and testing policies', + }, + { path: 'common-use-cases', title: 'Common use cases' }, + ], + }, { path: 'observability', title: 'Observability Integration', diff --git a/yarn.lock b/yarn.lock index 45c09277..47b7625e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5490,6 +5490,13 @@ __metadata: languageName: node linkType: hard +"@styra/highlightjs-rego@npm:^0.1.2": + version: 0.1.2 + resolution: "@styra/highlightjs-rego@npm:0.1.2" + checksum: f6a47f85f12a523a8104e281d26e9e44bd7d07d4886528812d4cd67991cc02a0ef00278cf717d35e12d63552014577aac7454389968f285e283416b181742646 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -14834,6 +14841,7 @@ __metadata: "@react-stately/list": 3.8.1 "@react-stately/select": 3.5.1 "@react-stately/selection": 3.19.0 + "@styra/highlightjs-rego": ^0.1.2 "@types/glob": 8.1.0 "@types/node": 20.4.2 "@types/react": 18.3.3 @@ -14859,6 +14867,7 @@ __metadata: fuse.js: 6.6.2 graphql: 16.6.0 gray-matter: 4.0.3 + highlight.js: 11.9.0 honorable: 0.194.0 honorable-theme-default: 0.77.0 htmlparser2: 9.1.0