id: cerbos-policy-drift-and-authorization-gate
namespace: company.team
description: |
Audits standalone Cerbos Policy Decision Point (PDP) instances on a schedule
by probing engine health and executing canary authorization checks across
critical role-action pairs, detecting unauthorized privilege escalations or
policy drift regressions, and dispatching actionable Slack security alerts.
triggers:
- id: periodic_policy_audit
type: io.kestra.plugin.core.trigger.Schedule
description: "Periodically executes canary authorization assertions against
Cerbos PDP. Shipped disabled by default; enable once cerbos_host and
cerbos_port point to a running Cerbos container."
cron: "*/15 * * * *"
disabled: true
inputs:
cerbos_host: "localhost"
cerbos_port: 3592
canary_principal_id: "canary_standard_user"
canary_role: "user"
canary_resource_kind: "document"
canary_resource_id: "doc_canary_001"
auto_reload_store: "false"
dry_run: "true"
inputs:
- id: cerbos_host
type: STRING
displayName: "Cerbos Host"
defaults: "localhost"
description: "Hostname or IP address of the Cerbos Policy Decision Point (PDP) server."
- id: cerbos_port
type: INT
displayName: "Cerbos Port"
defaults: 3592
description: "HTTP REST API port exposed by the Cerbos PDP server (default: 3592)."
- id: canary_principal_id
type: STRING
displayName: "Canary Principal ID"
defaults: "canary_standard_user"
description: "Synthetic principal identifier used to evaluate role-based policy
assertions."
- id: canary_role
type: STRING
displayName: "Canary Role"
defaults: "user"
description: "Role assigned to the canary principal to assert expected
permission boundaries."
- id: canary_resource_kind
type: STRING
displayName: "Canary Resource Kind"
defaults: "document"
description: "Resource kind against which the canary authorization checks are asserted."
- id: canary_resource_id
type: STRING
displayName: "Canary Resource ID"
defaults: "doc_canary_001"
description: "Resource instance identifier used during canary evaluation."
- id: auto_reload_store
type: STRING
displayName: "Auto Reload Store"
defaults: "false"
description: "When set to 'true' and dry_run is 'false', automatically triggers
a policy schema reload via POST /api/reload when drift is flagged."
- id: dry_run
type: STRING
displayName: "Dry Run Mode"
defaults: "true"
description: "When set to 'true', suppresses automated remediation requests and
logs dry-run audit records instead."
tasks:
- id: probe_cerbos_health
type: io.kestra.plugin.core.http.Request
description: "Queries Cerbos GET /api/health endpoint to verify PDP engine
readiness and subsystem operational state."
uri: "http://{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}/api/health"
method: GET
headers:
Accept: "application/json"
options:
allowFailed: true
- id: probe_canary_authorization
type: io.kestra.plugin.core.http.Request
description: "Executes synthetic authorization check via POST
/api/check/resources asserting standard user read and delete permissions."
uri: "http://{{ inputs.cerbos_host }}:{{ inputs.cerbos_port
}}/api/check/resources"
method: POST
headers:
Content-Type: "application/json"
Accept: "application/json"
body: |
{
"requestId": "kestra-canary-{{ execution.id }}",
"principal": {
"id": "{{ inputs.canary_principal_id }}",
"roles": ["{{ inputs.canary_role }}"]
},
"resources": [
{
"resource": {
"kind": "{{ inputs.canary_resource_kind }}",
"id": "{{ inputs.canary_resource_id }}"
},
"actions": ["view", "delete"]
}
]
}
options:
allowFailed: true
- id: classify_authorization_verdict
type: io.kestra.plugin.core.flow.Switch
description: "Evaluates health status and canary action effects to classify
authorization state."
value: >-
{% if fromJson(outputs.probe_cerbos_health.body).status !=
'CERBOS_STATUS_OK' %} PDP_UNHEALTHY {% elif
(fromJson(outputs.probe_canary_authorization.body).results[0].actions.delete
?? '') == 'EFFECT_ALLOW' %} PRIVILEGE_ESCALATION_DETECTED {% elif
(fromJson(outputs.probe_canary_authorization.body).results[0].actions.view
?? '') == 'EFFECT_DENY' %} POLICY_REGRESSION_DETECTED {% else %} HEALTHY
{% endif %}
cases:
PRIVILEGE_ESCALATION_DETECTED:
- id: alert_privilege_escalation
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Dispatches critical security alert to Slack when unprivileged role
unexpectedly receives delete permissions."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#security-alerts",
"text": ":rotating_light: *Cerbos Policy Drift: Unauthorized Privilege Escalation Detected*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rotating_light: *Critical Authorization Drift Detected*\n*Host:* `{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}`\n*Principal:* `{{ inputs.canary_principal_id }}` (Role: `{{ inputs.canary_role }}`)\n*Resource:* `{{ inputs.canary_resource_kind }}:{{ inputs.canary_resource_id }}`\n*Breach:* Action `delete` evaluated to `EFFECT_ALLOW` for standard user role! Policy regression allows unauthorized mutations."
}
}
]
}
- id: evaluate_remediation_on_escalation
type: io.kestra.plugin.core.flow.If
description: "Evaluates safety gate before reloading policy store following
privilege escalation detection."
condition: "{{ inputs.auto_reload_store == 'true' and inputs.dry_run == 'false'
}}"
then:
- id: reload_policy_store_escalation
type: io.kestra.plugin.core.http.Request
description: "Sends POST /api/reload to force Cerbos to reload policies from
disk or remote Git storage."
uri: "http://{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}/api/reload"
method: POST
- id: log_reload_escalation
type: io.kestra.plugin.core.log.Log
message: "Triggered automated policy store reload on Cerbos PDP following
privilege escalation detection."
else:
- id: log_dry_run_escalation
type: io.kestra.plugin.core.log.Log
message: "Dry-run active or auto_reload_store disabled. Suppressed automated
policy reload."
POLICY_REGRESSION_DETECTED:
- id: alert_policy_regression
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Dispatches warning alert to Slack when standard user view
permissions are denied unexpectedly."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#api-alerts",
"text": ":warning: *Cerbos Policy Drift: Legitimate Access Denied*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":warning: *Cerbos Access Regression Detected*\n*Host:* `{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}`\n*Principal:* `{{ inputs.canary_principal_id }}` (Role: `{{ inputs.canary_role }}`)\n*Resource:* `{{ inputs.canary_resource_kind }}:{{ inputs.canary_resource_id }}`\n*Issue:* Action `view` evaluated to `EFFECT_DENY`. Microservices will face unauthorized 403 access rejections."
}
}
]
}
- id: evaluate_remediation_on_regression
type: io.kestra.plugin.core.flow.If
description: "Evaluates safety gate before reloading policy store following
access regression."
condition: "{{ inputs.auto_reload_store == 'true' and inputs.dry_run == 'false'
}}"
then:
- id: reload_policy_store_regression
type: io.kestra.plugin.core.http.Request
description: "Sends POST /api/reload to reload Cerbos policies from configured
storage."
uri: "http://{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}/api/reload"
method: POST
- id: log_reload_regression
type: io.kestra.plugin.core.log.Log
message: "Triggered automated policy store reload on Cerbos PDP following access
regression."
else:
- id: log_dry_run_regression
type: io.kestra.plugin.core.log.Log
message: "Dry-run active or auto_reload_store disabled. Suppressed automated
policy reload."
PDP_UNHEALTHY:
- id: alert_pdp_unhealthy
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Dispatches critical alert when Cerbos PDP engine reports degraded
or failed health."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#platform-alerts",
"text": ":x: *Cerbos Policy Decision Point Degraded*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":x: *Cerbos Health Check Failed*\n*Host:* `{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}`\n*Status:* `{{ fromJson(outputs.probe_cerbos_health.body).status ?? 'UNKNOWN' }}`\nThe PDP is unable to evaluate authorization decisions. Downstream microservices may reject calls."
}
}
]
}
HEALTHY:
- id: log_healthy_authorization
type: io.kestra.plugin.core.log.Log
message: "Cerbos PDP authorization verified healthy on {{ inputs.cerbos_host
}}:{{ inputs.cerbos_port }}. Canary permissions: view=ALLOW,
delete=DENY."
- id: log_audit_complete
type: io.kestra.plugin.core.log.Log
description: "Persists final execution metadata and authorization verdict for
compliance audit trails."
message: "Cerbos authorization canary audit completed for host {{
inputs.cerbos_host }}:{{ inputs.cerbos_port }}."
errors:
- id: alert_cerbos_unreachable
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Dispatches out-of-band notification if Cerbos PDP server cannot be
reached."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "#platform-alerts",
"text": ":rotating_light: *Cerbos PDP Server Unreachable*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rotating_light: *Cerbos Connectivity Outage*\nFailed to connect to Cerbos REST API at `http://{{ inputs.cerbos_host }}:{{ inputs.cerbos_port }}`. Verify container runtime status, networking, and security groups."
}
}
]
}