Add OPA and Openshift AI
This commit is contained in:
58
roles/opa_server/defaults/main.yml
Normal file
58
roles/opa_server/defaults/main.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
# --- Deployment ---
|
||||
opa_server_namespace: opa
|
||||
opa_server_name: opa
|
||||
opa_server_image: docker.io/openpolicyagent/opa:1.21.1-static
|
||||
opa_server_replicas: 1
|
||||
opa_server_port: 8443
|
||||
opa_server_log_level: info
|
||||
# Log every decision (including the job's extra_vars) to the pod log for auditing.
|
||||
opa_server_decision_logs: true
|
||||
opa_server_resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
opa_server_wait_timeout: 300
|
||||
|
||||
# --- Policies ---
|
||||
# Every *.rego file in this directory is loaded into the server.
|
||||
opa_server_policy_dir: "{{ role_path }}/files/policies"
|
||||
# Exposed to policies as data.aap_policy_config (see files/policies/aap_policy.rego).
|
||||
opa_server_policy_config:
|
||||
allowed_ee_registries:
|
||||
- aap.toal.ca/
|
||||
- registry.redhat.io/
|
||||
forbidden_extra_vars:
|
||||
- ansible_password
|
||||
- ansible_become_password
|
||||
- ansible_ssh_pass
|
||||
- ansible_become_pass
|
||||
change_freeze: false
|
||||
change_freeze_exempt_teams: []
|
||||
|
||||
# --- Bearer token (OPA --authentication=token) ---
|
||||
# Generated on first run and stored in Vault; reused afterwards.
|
||||
opa_server_vault_mount: kv
|
||||
opa_server_vault_path: toallab/opa-server
|
||||
|
||||
# --- AAP integration ---
|
||||
opa_server_configure_aap: true
|
||||
opa_server_aap_url: "{{ aap_gateway_url }}"
|
||||
opa_server_aap_username: admin
|
||||
opa_server_aap_namespace: aap
|
||||
opa_server_aap_admin_secret: aap-admin-password
|
||||
opa_server_aap_validate_certs: true
|
||||
opa_server_aap_request_timeout: 1.5
|
||||
opa_server_aap_request_retries: 2
|
||||
# Attach a policy to AAP resources. Jobs fail closed: any OPA error or violation
|
||||
# blocks the launch, so start with aap/smoke/allow_all and a test resource.
|
||||
# - type: organizations | inventories | job_templates
|
||||
# name: Default
|
||||
# query_path: aap/policy/decision
|
||||
opa_server_aap_query_paths: []
|
||||
|
||||
# --- Debugging ---
|
||||
opa_server_no_log: true
|
||||
65
roles/opa_server/files/policies/aap_policy.rego
Normal file
65
roles/opa_server/files/policies/aap_policy.rego
Normal file
@@ -0,0 +1,65 @@
|
||||
# AAP policy-as-code baseline.
|
||||
# Set opa_query_path to aap/policy/decision on an organization, inventory, or
|
||||
# job template. Input is the serialized Job (awx/main/tasks/policy.py).
|
||||
# Tunables come from data.aap_policy_config (opa_server_policy_config).
|
||||
package aap.policy
|
||||
|
||||
import rego.v1
|
||||
|
||||
decision := {
|
||||
"allowed": count(violations) == 0,
|
||||
"violations": sort([msg | some msg in violations]),
|
||||
}
|
||||
|
||||
default_config := {
|
||||
"allowed_ee_registries": [],
|
||||
"forbidden_extra_vars": [],
|
||||
"change_freeze": false,
|
||||
"change_freeze_exempt_teams": [],
|
||||
}
|
||||
|
||||
default user_config := {}
|
||||
|
||||
user_config := data.aap_policy_config
|
||||
|
||||
config := object.union(default_config, user_config)
|
||||
|
||||
# Execution environment must come from an approved registry
|
||||
violations contains msg if {
|
||||
count(config.allowed_ee_registries) > 0
|
||||
image := input.execution_environment.image
|
||||
not approved_registry(image)
|
||||
msg := sprintf(
|
||||
"Execution environment image %q is not from an approved registry (%s)",
|
||||
[image, concat(", ", config.allowed_ee_registries)],
|
||||
)
|
||||
}
|
||||
|
||||
approved_registry(image) if {
|
||||
some prefix in config.allowed_ee_registries
|
||||
startswith(image, prefix)
|
||||
}
|
||||
|
||||
# Credentials belong in AAP credentials, not launch-time extra vars
|
||||
violations contains msg if {
|
||||
extra_vars := object.get(input, "extra_vars", {})
|
||||
is_object(extra_vars)
|
||||
some key, _ in extra_vars
|
||||
lower(key) in {lower(k) | some k in config.forbidden_extra_vars}
|
||||
msg := sprintf("Extra variable %q may not be passed at launch; use a credential", [key])
|
||||
}
|
||||
|
||||
# Change freeze: only check-mode jobs unless the launcher is exempt
|
||||
violations contains msg if {
|
||||
config.change_freeze
|
||||
input.job_type == "run"
|
||||
not freeze_exempt
|
||||
msg := "A change freeze is in effect: only check-mode jobs are allowed"
|
||||
}
|
||||
|
||||
freeze_exempt if input.created_by.is_superuser
|
||||
|
||||
freeze_exempt if {
|
||||
some team in input.created_by.teams
|
||||
team.name in config.change_freeze_exempt_teams
|
||||
}
|
||||
8
roles/opa_server/files/policies/aap_smoke.rego
Normal file
8
roles/opa_server/files/policies/aap_smoke.rego
Normal file
@@ -0,0 +1,8 @@
|
||||
# Wiring checks for AAP policy enforcement.
|
||||
# aap/smoke/allow_all - always passes (safe first attachment)
|
||||
# aap/smoke/deny_all - always blocks, proving enforcement is active
|
||||
package aap.smoke
|
||||
|
||||
allow_all := {"allowed": true, "violations": []}
|
||||
|
||||
deny_all := {"allowed": false, "violations": ["aap.smoke.deny_all: policy enforcement is active"]}
|
||||
11
roles/opa_server/files/policies/authz.rego
Normal file
11
roles/opa_server/files/policies/authz.rego
Normal file
@@ -0,0 +1,11 @@
|
||||
# Server authorization (opa run --authorization=basic).
|
||||
# Health checks are open; everything else needs the bearer token.
|
||||
package system.authz
|
||||
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if input.path == ["health"]
|
||||
|
||||
allow if input.identity == data.opa_authz.token
|
||||
87
roles/opa_server/files/tests/aap_policy_test.rego
Normal file
87
roles/opa_server/files/tests/aap_policy_test.rego
Normal file
@@ -0,0 +1,87 @@
|
||||
# Unit tests (not deployed). Run with:
|
||||
# podman run --rm -v ./roles/opa_server/files:/files:Z docker.io/openpolicyagent/opa:1.21.1-static \
|
||||
# test /files/policies /files/tests -v
|
||||
package aap.policy_test
|
||||
|
||||
import rego.v1
|
||||
|
||||
import data.aap.policy
|
||||
|
||||
cfg := {
|
||||
"allowed_ee_registries": ["aap.toal.ca/", "registry.redhat.io/"],
|
||||
"forbidden_extra_vars": ["ansible_password"],
|
||||
"change_freeze": false,
|
||||
"change_freeze_exempt_teams": ["Platform"],
|
||||
}
|
||||
|
||||
job := {
|
||||
"job_type": "run",
|
||||
"execution_environment": {"image": "aap.toal.ca/toallab/ee-demo:latest"},
|
||||
"extra_vars": {"target": "web"},
|
||||
"created_by": {"username": "dev", "is_superuser": false, "teams": [{"id": 1, "name": "Dev"}]},
|
||||
}
|
||||
|
||||
test_compliant_job_allowed if {
|
||||
d := policy.decision with input as job with data.aap_policy_config as cfg
|
||||
d.allowed
|
||||
d.violations == []
|
||||
}
|
||||
|
||||
test_unapproved_registry_denied if {
|
||||
j := object.union(job, {"execution_environment": {"image": "quay.io/scottharwell/cloud-ee:latest"}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
not d.allowed
|
||||
count(d.violations) == 1
|
||||
}
|
||||
|
||||
test_forbidden_extra_var_denied_case_insensitive if {
|
||||
j := object.union(job, {"extra_vars": {"Ansible_Password": "x"}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_null_extra_vars_and_ee_allowed if {
|
||||
j := object.union(job, {"extra_vars": null, "execution_environment": null})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_blocks_run if {
|
||||
d := policy.decision with input as job with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_freeze_allows_check if {
|
||||
j := object.union(job, {"job_type": "check"})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_exempt_team if {
|
||||
j := object.union(job, {"created_by": {"is_superuser": false, "teams": [{"name": "Platform"}]}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_scheduled_job_without_creator if {
|
||||
j := object.union(job, {"created_by": null})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_no_config_allows_everything if {
|
||||
j := object.union(job, {"execution_environment": {"image": "quay.io/anything:latest"}})
|
||||
d := policy.decision with input as j
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_authz_health_open if {
|
||||
data.system.authz.allow with input as {"path": ["health"]}
|
||||
}
|
||||
|
||||
test_authz_requires_token if {
|
||||
not data.system.authz.allow with input as {"path": ["v1", "data"], "identity": "bad"}
|
||||
with data.opa_authz.token as "good"
|
||||
data.system.authz.allow with input as {"path": ["v1", "data"], "identity": "good"}
|
||||
with data.opa_authz.token as "good"
|
||||
}
|
||||
103
roles/opa_server/meta/argument_specs.yml
Normal file
103
roles/opa_server/meta/argument_specs.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Deploy an OPA server for AAP policy enforcement
|
||||
description:
|
||||
- Deploys Open Policy Agent with TLS (service-ca) and bearer-token authentication, loading
|
||||
the Rego policies in opa_server_policy_dir and opa_server_policy_config as
|
||||
data.aap_policy_config.
|
||||
- Configures AAP's PolicyAsCode settings to use it and optionally attaches opa_query_path
|
||||
to organizations, inventories, or job templates. AAP fails closed once a path is attached.
|
||||
- The bearer token is generated on first run and stored in Vault at opa_server_vault_path.
|
||||
options:
|
||||
opa_server_namespace:
|
||||
description: Namespace for the OPA server.
|
||||
type: str
|
||||
default: opa
|
||||
opa_server_name:
|
||||
description: Name for the Deployment, Service, and derived resources.
|
||||
type: str
|
||||
default: opa
|
||||
opa_server_image:
|
||||
description: OPA container image (pin an exact version).
|
||||
type: str
|
||||
default: docker.io/openpolicyagent/opa:1.21.1-static
|
||||
opa_server_replicas:
|
||||
description: Number of OPA replicas.
|
||||
type: int
|
||||
default: 1
|
||||
opa_server_port:
|
||||
description: HTTPS port OPA listens on and the Service exposes.
|
||||
type: int
|
||||
default: 8443
|
||||
opa_server_log_level:
|
||||
description: OPA log level.
|
||||
type: str
|
||||
choices: [debug, info, warn, error]
|
||||
default: info
|
||||
opa_server_decision_logs:
|
||||
description: Log every decision (including job extra_vars) to the pod log.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_resources:
|
||||
description: Container resource requests and limits.
|
||||
type: dict
|
||||
opa_server_wait_timeout:
|
||||
description: Seconds to wait for the OPA Deployment to become available.
|
||||
type: int
|
||||
default: 300
|
||||
opa_server_policy_dir:
|
||||
description: Directory whose *.rego files are loaded into OPA.
|
||||
type: path
|
||||
opa_server_policy_config:
|
||||
description: Policy tunables exposed as data.aap_policy_config.
|
||||
type: dict
|
||||
opa_server_vault_mount:
|
||||
description: Vault KV2 mount for the bearer token.
|
||||
type: str
|
||||
default: kv
|
||||
opa_server_vault_path:
|
||||
description: Vault KV2 path for the bearer token.
|
||||
type: str
|
||||
default: toallab/opa-server
|
||||
opa_server_configure_aap:
|
||||
description: Configure AAP's PolicyAsCode settings to use this server.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_aap_url:
|
||||
description: AAP gateway URL.
|
||||
type: str
|
||||
opa_server_aap_username:
|
||||
description: AAP admin username.
|
||||
type: str
|
||||
default: admin
|
||||
opa_server_aap_namespace:
|
||||
description: Namespace of the AAP instance (holds the admin password secret).
|
||||
type: str
|
||||
default: aap
|
||||
opa_server_aap_admin_secret:
|
||||
description: Secret holding the AAP admin password (key password).
|
||||
type: str
|
||||
default: aap-admin-password
|
||||
opa_server_aap_validate_certs:
|
||||
description: Validate the AAP gateway certificate.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_aap_request_timeout:
|
||||
description: OPA_REQUEST_TIMEOUT in seconds.
|
||||
type: float
|
||||
default: 1.5
|
||||
opa_server_aap_request_retries:
|
||||
description: OPA_REQUEST_RETRIES.
|
||||
type: int
|
||||
default: 2
|
||||
opa_server_aap_query_paths:
|
||||
description: Resources to attach a policy to (type organizations|inventories|job_templates,
|
||||
name, query_path).
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
opa_server_no_log:
|
||||
description: Set to false to expose secret values in task output.
|
||||
type: bool
|
||||
default: true
|
||||
17
roles/opa_server/meta/main.yml
Normal file
17
roles/opa_server/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Deploy an Open Policy Agent server on OpenShift for AAP policy enforcement
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- opa
|
||||
- policy
|
||||
- aap
|
||||
|
||||
dependencies: []
|
||||
95
roles/opa_server/tasks/aap.yml
Normal file
95
roles/opa_server/tasks/aap.yml
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
# Point the AAP controller at OPA (PolicyAsCode settings) and optionally attach
|
||||
# query paths to organizations, inventories, or job templates.
|
||||
|
||||
- name: Read AAP admin password
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ opa_server_aap_namespace }}"
|
||||
name: "{{ opa_server_aap_admin_secret }}"
|
||||
register: __opa_server_aap_admin
|
||||
no_log: true
|
||||
|
||||
- name: Read the service CA bundle
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: ConfigMap
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: openshift-service-ca.crt
|
||||
register: __opa_server_service_ca
|
||||
|
||||
- name: Resolve AAP API connection
|
||||
ansible.builtin.set_fact:
|
||||
__opa_server_aap_api: "{{ opa_server_aap_url | regex_replace('/$', '') }}/api/controller/v2"
|
||||
__opa_server_aap_password: "{{ __opa_server_aap_admin.resources[0].data.password | b64decode }}"
|
||||
__opa_server_aap_settings:
|
||||
OPA_HOST: "{{ __opa_server_host }}"
|
||||
OPA_PORT: "{{ opa_server_port | int }}"
|
||||
OPA_SSL: true
|
||||
OPA_AUTH_TYPE: Token
|
||||
OPA_AUTH_CA_CERT: "{{ __opa_server_service_ca.resources[0].data['service-ca.crt'] }}"
|
||||
OPA_REQUEST_TIMEOUT: "{{ opa_server_aap_request_timeout | float }}"
|
||||
OPA_REQUEST_RETRIES: "{{ opa_server_aap_request_retries | int }}"
|
||||
no_log: true
|
||||
|
||||
- name: Read AAP policy-as-code settings
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/settings/policyascode/"
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
register: __opa_server_aap_current
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
# The token reads back as $encrypted$, so it is only resent when unset or rotated
|
||||
- name: Configure AAP policy-as-code settings
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/settings/policyascode/"
|
||||
method: PATCH
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
body_format: json
|
||||
body: "{{ __opa_server_aap_settings | combine({'OPA_AUTH_TOKEN': __opa_server_token}) }}"
|
||||
changed_when: true
|
||||
when: >-
|
||||
(__opa_server_aap_current.json | dict2items
|
||||
| selectattr('key', 'in', __opa_server_aap_settings.keys() | list)
|
||||
| items2dict) != __opa_server_aap_settings
|
||||
or (__opa_server_aap_current.json.OPA_AUTH_TOKEN | default('')) | length == 0
|
||||
or __opa_server_token_generated | bool
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Look up resources to attach policies to
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/{{ item.type }}/?name={{ item.name | urlencode }}"
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
register: __opa_server_aap_targets
|
||||
failed_when: >-
|
||||
__opa_server_aap_targets.status != 200 or __opa_server_aap_targets.json.count != 1
|
||||
loop: "{{ opa_server_aap_query_paths }}"
|
||||
loop_control:
|
||||
label: "{{ item.type }}/{{ item.name }}"
|
||||
|
||||
- name: Attach OPA query paths
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/{{ item.item.type }}/{{ item.json.results[0].id }}/"
|
||||
method: PATCH
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
body_format: json
|
||||
body:
|
||||
opa_query_path: "{{ item.item.query_path }}"
|
||||
changed_when: true
|
||||
when: item.json.results[0].opa_query_path | default('') != item.item.query_path
|
||||
loop: "{{ __opa_server_aap_targets.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.type }}/{{ item.item.name }} -> {{ item.item.query_path }}"
|
||||
172
roles/opa_server/tasks/deploy.yml
Normal file
172
roles/opa_server/tasks/deploy.yml
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
- name: Create OPA namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ opa_server_namespace }}"
|
||||
|
||||
- name: Create OPA policy ConfigMap
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
apply: true
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}-policies"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
data: >-
|
||||
{{ __opa_server_policies
|
||||
| combine({'aap_policy_config.json': {'aap_policy_config': opa_server_policy_config} | to_json}) }}
|
||||
|
||||
- name: Create OPA authorization token Secret
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}-authz"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
type: Opaque
|
||||
stringData:
|
||||
opa_authz.json: "{{ {'opa_authz': {'token': __opa_server_token}} | to_json }}"
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Create OPA Service with a service-ca serving certificate
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
annotations:
|
||||
service.beta.openshift.io/serving-cert-secret-name: "{{ __opa_server_tls_secret }}"
|
||||
spec:
|
||||
selector:
|
||||
app: "{{ opa_server_name }}"
|
||||
ports:
|
||||
- name: https
|
||||
port: "{{ opa_server_port | int }}"
|
||||
targetPort: https
|
||||
|
||||
- name: Wait for OPA serving certificate
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: "{{ __opa_server_tls_secret }}"
|
||||
register: __opa_server_cert
|
||||
until: __opa_server_cert.resources | length > 0
|
||||
retries: 30
|
||||
delay: 10
|
||||
no_log: true
|
||||
|
||||
- name: Create OPA Deployment
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
labels:
|
||||
app: "{{ opa_server_name }}"
|
||||
spec:
|
||||
replicas: "{{ opa_server_replicas | int }}"
|
||||
selector:
|
||||
matchLabels:
|
||||
app: "{{ opa_server_name }}"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: "{{ opa_server_name }}"
|
||||
annotations:
|
||||
# Roll pods when policies, config, or the token change
|
||||
checksum/policies: "{{ (__opa_server_policies | to_json ~ opa_server_policy_config | to_json) | hash('sha256') }}"
|
||||
checksum/authz: "{{ __opa_server_token | hash('sha256') }}"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: opa
|
||||
image: "{{ opa_server_image }}"
|
||||
args: >-
|
||||
{{ [
|
||||
'run', '--server',
|
||||
'--addr=0.0.0.0:' ~ opa_server_port,
|
||||
'--tls-cert-file=/certs/tls.crt',
|
||||
'--tls-private-key-file=/certs/tls.key',
|
||||
'--authentication=token',
|
||||
'--authorization=basic',
|
||||
'--ignore=.*',
|
||||
'--log-format=json',
|
||||
'--log-level=' ~ opa_server_log_level,
|
||||
]
|
||||
+ (['--set=decision_logs.console=true'] if opa_server_decision_logs | bool else [])
|
||||
+ ['/policies', '/authz'] }}
|
||||
ports:
|
||||
- name: https
|
||||
containerPort: "{{ opa_server_port | int }}"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: https
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: https
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources: "{{ opa_server_resources }}"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
volumeMounts:
|
||||
- name: policies
|
||||
mountPath: /policies
|
||||
readOnly: true
|
||||
- name: authz
|
||||
mountPath: /authz
|
||||
readOnly: true
|
||||
- name: certs
|
||||
mountPath: /certs
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: policies
|
||||
configMap:
|
||||
name: "{{ opa_server_name }}-policies"
|
||||
- name: authz
|
||||
secret:
|
||||
secretName: "{{ opa_server_name }}-authz"
|
||||
- name: certs
|
||||
secret:
|
||||
secretName: "{{ __opa_server_tls_secret }}"
|
||||
|
||||
- name: Wait for OPA to be available
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: "{{ opa_server_name }}"
|
||||
register: __opa_server_deploy
|
||||
until: >-
|
||||
__opa_server_deploy.resources | length > 0 and
|
||||
(__opa_server_deploy.resources[0].status.updatedReplicas | default(0)) == opa_server_replicas | int and
|
||||
(__opa_server_deploy.resources[0].status.availableReplicas | default(0)) == opa_server_replicas | int
|
||||
retries: "{{ __opa_server_wait_retries }}"
|
||||
delay: 10
|
||||
32
roles/opa_server/tasks/main.yml
Normal file
32
roles/opa_server/tasks/main.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Deploy an Open Policy Agent server for AAP policy enforcement (policy as code)
|
||||
# and point AAP's controller at it.
|
||||
#
|
||||
# OPA serves TLS with a service-ca certificate and requires a bearer token
|
||||
# (--authentication=token, system.authz policy). AAP connects in-cluster with
|
||||
# OPA_SSL=true, OPA_AUTH_TYPE=Token and the service CA bundle.
|
||||
#
|
||||
# AAP fails closed: once a resource has an opa_query_path, any OPA error or
|
||||
# violation blocks its jobs. Nothing is attached unless opa_server_aap_query_paths
|
||||
# lists it.
|
||||
#
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
- name: Resolve OPA bearer token from Vault
|
||||
ansible.builtin.include_tasks: vault_token.yml
|
||||
|
||||
- name: Deploy OPA server
|
||||
ansible.builtin.include_tasks: deploy.yml
|
||||
|
||||
- name: Configure AAP policy enforcement
|
||||
ansible.builtin.include_tasks: aap.yml
|
||||
when: opa_server_configure_aap | bool
|
||||
|
||||
- name: Display OPA deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OPA server ready!"
|
||||
- " Endpoint : https://{{ __opa_server_host }}:{{ opa_server_port }} (in-cluster)"
|
||||
- " Policies : {{ __opa_server_policies.keys() | list | join(', ') }}"
|
||||
- " Query paths : aap/policy/decision, aap/smoke/allow_all, aap/smoke/deny_all"
|
||||
- " Token : Vault {{ opa_server_vault_mount }}/{{ opa_server_vault_path }}"
|
||||
28
roles/opa_server/tasks/vault_token.yml
Normal file
28
roles/opa_server/tasks/vault_token.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
# Read the OPA bearer token from Vault; generate and store it if missing.
|
||||
# Sets __opa_server_token and __opa_server_token_generated.
|
||||
|
||||
- name: Read OPA token from Vault
|
||||
community.hashi_vault.vault_kv2_get:
|
||||
path: "{{ opa_server_vault_path }}"
|
||||
engine_mount_point: "{{ opa_server_vault_mount }}"
|
||||
register: __opa_server_vault_read
|
||||
failed_when: false
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Resolve OPA token (generate if missing)
|
||||
ansible.builtin.set_fact:
|
||||
__opa_server_token_generated: "{{ ((__opa_server_vault_read.secret | default({})).token | default('')) | length == 0 }}"
|
||||
__opa_server_token: >-
|
||||
{{ (__opa_server_vault_read.secret | default({})).token
|
||||
| default(lookup('community.general.random_string', length=48, special=false)) }}
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Write OPA token to Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: "{{ opa_server_vault_path }}"
|
||||
engine_mount_point: "{{ opa_server_vault_mount }}"
|
||||
data:
|
||||
token: "{{ __opa_server_token }}"
|
||||
when: __opa_server_token_generated | bool
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
9
roles/opa_server/vars/main.yml
Normal file
9
roles/opa_server/vars/main.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__opa_server_wait_retries: "{{ (opa_server_wait_timeout / 10) | int }}"
|
||||
__opa_server_host: "{{ opa_server_name }}.{{ opa_server_namespace }}.svc"
|
||||
__opa_server_tls_secret: "{{ opa_server_name }}-tls"
|
||||
__opa_server_policy_files: "{{ query('ansible.builtin.fileglob', opa_server_policy_dir ~ '/*.rego') | sort }}"
|
||||
__opa_server_policies: >-
|
||||
{{ dict(__opa_server_policy_files | map('basename')
|
||||
| zip(query('ansible.builtin.file', *__opa_server_policy_files))) }}
|
||||
Reference in New Issue
Block a user