diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 93ecaf59..756ee138 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -80,14 +80,10 @@ rules: - update - watch - apiGroups: - - apps - networking.k8s.io resources: - - deployments - - daemonsets - - replicasets - - statefulsets - ingresses + - networkpolicies verbs: - get - list diff --git a/molecule/default/tasks/eda_test.yml b/molecule/default/tasks/eda_test.yml index 05482c56..3827723f 100644 --- a/molecule/default/tasks/eda_test.yml +++ b/molecule/default/tasks/eda_test.yml @@ -12,7 +12,78 @@ vars: cr_file: 'eda_v1alpha1_eda.yaml' -- name: Add assertions here - assert: - that: false - fail_msg: FIXME Add real assertions for your operator +- name: Set EDA resource name from sample CR + ansible.builtin.set_fact: + eda_cr_name: "{{ lookup('file', samples_dir + '/eda_v1alpha1_eda.yaml') | from_yaml | json_query('metadata.name') }}" + +- name: Verify default-deny NetworkPolicy exists + kubernetes.core.k8s_info: + api_version: networking.k8s.io/v1 + kind: NetworkPolicy + namespace: '{{ namespace }}' + name: '{{ eda_cr_name }}-default-deny' + register: np_default_deny + until: np_default_deny.resources | length > 0 + retries: 10 + delay: 5 + +- name: Assert default-deny selects eda-operator managed pods and denies all traffic + ansible.builtin.assert: + that: + - np_default_deny.resources[0].spec.podSelector.matchExpressions | length == 1 + - np_default_deny.resources[0].spec.podSelector.matchExpressions[0].key == 'app.kubernetes.io/managed-by' + - np_default_deny.resources[0].spec.podSelector.matchExpressions[0].operator == 'In' + - np_default_deny.resources[0].spec.podSelector.matchExpressions[0].values | length == 1 + - np_default_deny.resources[0].spec.podSelector.matchExpressions[0].values[0] == deployment_type + '-operator' + - "'Ingress' in np_default_deny.resources[0].spec.policyTypes" + - "'Egress' in np_default_deny.resources[0].spec.policyTypes" + - np_default_deny.resources[0].spec.ingress is not defined or np_default_deny.resources[0].spec.ingress | length == 0 + - np_default_deny.resources[0].spec.egress is not defined or np_default_deny.resources[0].spec.egress | length == 0 + fail_msg: "default-deny NetworkPolicy does not match expected spec: {{ np_default_deny.resources }}" + +- name: Verify postgres NetworkPolicy exists + kubernetes.core.k8s_info: + api_version: networking.k8s.io/v1 + kind: NetworkPolicy + namespace: '{{ namespace }}' + name: '{{ eda_cr_name }}-postgres-{{ supported_pg_version }}' + register: np_postgres + until: np_postgres.resources | length > 0 + retries: 10 + delay: 5 + +- name: Assert postgres NetworkPolicy allows app=eda ingress for activation Job pod pg_notify connectivity + ansible.builtin.assert: + that: + - np_postgres.resources[0].spec.podSelector.matchLabels['app.kubernetes.io/component'] == 'database' + - np_postgres.resources[0].spec.ingress | length > 0 + - np_postgres.resources[0].spec.ingress[0].from | selectattr('podSelector.matchLabels.app', 'defined') | selectattr('podSelector.matchLabels.app', 'equalto', 'eda') | list | length > 0 + fail_msg: "Postgres NetworkPolicy missing app=eda ingress rule: {{ np_postgres.resources }}" + +- name: Verify per-workload NetworkPolicies exist + kubernetes.core.k8s_info: + api_version: networking.k8s.io/v1 + kind: NetworkPolicy + namespace: '{{ namespace }}' + name: '{{ eda_cr_name }}-{{ item }}' + register: np_workload + until: np_workload.resources | length > 0 + retries: 10 + delay: 5 + loop: "{{ ['api', 'default-worker', 'activation-worker', 'event-stream', 'db-management'] + ([] if ui_disabled | bool else ['ui']) }}" + +- name: Assert UI NetworkPolicy is absent when ui_disabled + kubernetes.core.k8s_info: + api_version: networking.k8s.io/v1 + kind: NetworkPolicy + namespace: '{{ namespace }}' + name: '{{ eda_cr_name }}-ui' + register: np_ui_absent + when: ui_disabled | bool + +- name: Assert UI NetworkPolicy is absent when ui_disabled + ansible.builtin.assert: + that: + - np_ui_absent.resources | length == 0 + fail_msg: "UI NetworkPolicy should be absent when ui_disabled=true" + when: ui_disabled | bool diff --git a/roles/eda/tasks/main.yml b/roles/eda/tasks/main.yml index 33149b2b..37c3f378 100644 --- a/roles/eda/tasks/main.yml +++ b/roles/eda/tasks/main.yml @@ -74,6 +74,9 @@ - ingress_type | lower == 'route' - route_tls_secret | length +- name: Apply NetworkPolicies for EDA operand pods + include_tasks: networkpolicy.yml + - name: Deploy EDA include_tasks: deploy_eda.yml diff --git a/roles/eda/tasks/networkpolicy.yml b/roles/eda/tasks/networkpolicy.yml new file mode 100644 index 00000000..49a0cd5c --- /dev/null +++ b/roles/eda/tasks/networkpolicy.yml @@ -0,0 +1,46 @@ +--- +- name: Load postgres role vars for supported_pg_version + ansible.builtin.include_vars: + file: "{{ role_path }}/../postgres/vars/main.yml" + +- name: Apply default deny NetworkPolicy for EDA operand pods + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'default-deny.networkpolicy.yaml.j2') }}" + +- name: Apply EDA API NetworkPolicy + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'eda-api.networkpolicy.yaml.j2') }}" + +- name: Apply EDA UI NetworkPolicy + kubernetes.core.k8s: + state: "{{ 'present' if not ui_disabled else 'absent' }}" + apply: true + definition: "{{ lookup('template', 'eda-ui.networkpolicy.yaml.j2') }}" + +- name: Apply EDA default-worker NetworkPolicy + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'eda-default-worker.networkpolicy.yaml.j2') }}" + +- name: Apply EDA activation-worker NetworkPolicy + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'eda-activation-worker.networkpolicy.yaml.j2') }}" + +- name: Apply EDA event-stream NetworkPolicy + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'eda-event-stream.networkpolicy.yaml.j2') }}" + +- name: Apply db-management NetworkPolicy for backup and restore pods + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'db-management.networkpolicy.yaml.j2') }}" diff --git a/roles/eda/templates/db-management.networkpolicy.yaml.j2 b/roles/eda/templates/db-management.networkpolicy.yaml.j2 new file mode 100644 index 00000000..d8f1c9c1 --- /dev/null +++ b/roles/eda/templates/db-management.networkpolicy.yaml.j2 @@ -0,0 +1,43 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-db-management' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}' + policyTypes: + - Ingress + - Egress + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Allow egress to managed PostgreSQL for pg_dump / pg_restore +{% if managed_database | bool %} + - to: + - podSelector: + matchLabels: + app.kubernetes.io/component: database + app.kubernetes.io/instance: 'postgres-{{ supported_pg_version }}-{{ ansible_operator_meta.name }}' + ports: + - protocol: TCP + port: {{ eda_postgres_port | default('5432') }} +{% else %} + # External database — destination is user-configured, allow on the port + - ports: + - protocol: TCP + port: {{ eda_postgres_port | default('5432') }} +{% endif %} diff --git a/roles/eda/templates/default-deny.networkpolicy.yaml.j2 b/roles/eda/templates/default-deny.networkpolicy.yaml.j2 new file mode 100644 index 00000000..ae120930 --- /dev/null +++ b/roles/eda/templates/default-deny.networkpolicy.yaml.j2 @@ -0,0 +1,18 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-default-deny' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} +spec: + podSelector: + matchExpressions: + - key: app.kubernetes.io/managed-by + operator: In + values: + - '{{ deployment_type }}-operator' + policyTypes: + - Ingress + - Egress diff --git a/roles/eda/templates/eda-activation-worker.networkpolicy.yaml.j2 b/roles/eda/templates/eda-activation-worker.networkpolicy.yaml.j2 new file mode 100644 index 00000000..059b8fe2 --- /dev/null +++ b/roles/eda/templates/eda-activation-worker.networkpolicy.yaml.j2 @@ -0,0 +1,33 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-activation-worker' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} + app.kubernetes.io/component: '{{ deployment_type }}-activation-worker' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-activation-worker' + policyTypes: + - Egress + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Unrestricted egress required: activation workers create and manage Job + # pods via the Kubernetes API, connect to user-defined event source + # destinations, and may call external SMS. Destinations cannot be + # enumerated at policy creation time. + - {} diff --git a/roles/eda/templates/eda-api.networkpolicy.yaml.j2 b/roles/eda/templates/eda-api.networkpolicy.yaml.j2 new file mode 100644 index 00000000..97764c31 --- /dev/null +++ b/roles/eda/templates/eda-api.networkpolicy.yaml.j2 @@ -0,0 +1,77 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-api' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} + app.kubernetes.io/component: '{{ deployment_type }}-api' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-api' + policyTypes: + - Ingress + - Egress + ingress: + # Allow traffic from the OpenShift router (Routes) and ingress controllers + - from: + - namespaceSelector: + matchLabels: + policy-group.network.openshift.io/ingress: "" + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + ports: + - protocol: TCP + port: {{ api_nginx_port }} + # Allow traffic from the gateway operator (reverse proxy) and other AAP + # component pods that call the EDA API + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/managed-by + operator: In + values: + - '{{ deployment_type }}-operator' + - 'aap-operator' + - podSelector: + matchLabels: + control-plane: controller-manager + ports: + - protocol: TCP + port: {{ api_nginx_port }} + - protocol: TCP + port: {{ websocket_port }} + # Allow traffic from activation Job pods (created at runtime by activation + # workers, not operator-managed — carry only app: eda). + # ansible_rulebook connects to daphne (websocket_port) for rulebook + # execution and to the API (api_nginx_port) for token refresh. + - from: + - podSelector: + matchLabels: + app: eda + ports: + - protocol: TCP + port: {{ websocket_port }} + - protocol: TCP + port: {{ api_nginx_port }} + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Unrestricted egress required for user-configured external services + # (external SMS such as HashiCorp Vault, fetched by daphne during websocket + # handshake; gateway JWT key fetch; postgres). Destinations are + # user-defined and cannot be enumerated at policy creation time. + - {} diff --git a/roles/eda/templates/eda-default-worker.networkpolicy.yaml.j2 b/roles/eda/templates/eda-default-worker.networkpolicy.yaml.j2 new file mode 100644 index 00000000..7f37444a --- /dev/null +++ b/roles/eda/templates/eda-default-worker.networkpolicy.yaml.j2 @@ -0,0 +1,33 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-default-worker' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} + app.kubernetes.io/component: '{{ deployment_type }}-default-worker' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-default-worker' + policyTypes: + - Egress + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Unrestricted egress required: default-worker executes user-defined tasks + # (git clones, SCM syncs, webhook calls) and may call external SMS. + # Destinations are user-configured and cannot be enumerated at policy + # creation time. + - {} diff --git a/roles/eda/templates/eda-event-stream.networkpolicy.yaml.j2 b/roles/eda/templates/eda-event-stream.networkpolicy.yaml.j2 new file mode 100644 index 00000000..f853c346 --- /dev/null +++ b/roles/eda/templates/eda-event-stream.networkpolicy.yaml.j2 @@ -0,0 +1,63 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-event-stream' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} + app.kubernetes.io/component: '{{ deployment_type }}-event-stream' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-event-stream' + policyTypes: + - Ingress + - Egress + ingress: + # Allow traffic from the OpenShift router (Routes) and ingress controllers. + # Event stream receives external webhook/event traffic via the router. + - from: + - namespaceSelector: + matchLabels: + policy-group.network.openshift.io/ingress: "" + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + ports: + - protocol: TCP + port: {{ event_stream_nginx_port }} + # Allow traffic from the gateway operator and other EDA pods + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/managed-by + operator: In + values: + - '{{ deployment_type }}-operator' + - 'aap-operator' + - podSelector: + matchLabels: + control-plane: controller-manager + ports: + - protocol: TCP + port: {{ event_stream_nginx_port }} + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Unrestricted egress required: event stream validates the X-Trusted-Proxy + # header by fetching the gateway JWT public key, connects to postgres via + # pg_notify, and may call external SMS when event stream credentials use + # an external secret manager. Destinations cannot be enumerated at policy + # creation time. + - {} diff --git a/roles/eda/templates/eda-ui.networkpolicy.yaml.j2 b/roles/eda/templates/eda-ui.networkpolicy.yaml.j2 new file mode 100644 index 00000000..035e7327 --- /dev/null +++ b/roles/eda/templates/eda-ui.networkpolicy.yaml.j2 @@ -0,0 +1,67 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-ui' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(4) | trim }} + app.kubernetes.io/component: '{{ deployment_type }}-ui' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-ui' + policyTypes: + - Ingress + - Egress + ingress: + # Allow traffic from the OpenShift router (Routes) and ingress controllers. + # The UI nginx container listens on 8080, not api_nginx_port (8000). + - from: + - namespaceSelector: + matchLabels: + policy-group.network.openshift.io/ingress: "" + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + ports: + - protocol: TCP + port: 8080 + # Allow traffic from the gateway operator (reverse proxy) + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/managed-by + operator: In + values: + - '{{ deployment_type }}-operator' + - 'aap-operator' + - podSelector: + matchLabels: + control-plane: controller-manager + ports: + - protocol: TCP + port: 8080 + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353 + # Allow egress to the EDA API pod (UI proxies API requests to the api pod) + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' + app.kubernetes.io/component: '{{ deployment_type }}-api' + ports: + - protocol: TCP + port: {{ api_nginx_port }} diff --git a/roles/postgres/tasks/main.yml b/roles/postgres/tasks/main.yml index c11f5f1b..3583a0c7 100644 --- a/roles/postgres/tasks/main.yml +++ b/roles/postgres/tasks/main.yml @@ -13,6 +13,12 @@ - name: Set variables to be used in Postgres templates import_tasks: set_variables.yml +# NetworkPolicy must be applied before the StatefulSet so deny is in place +# before any database pod starts +- name: Apply PostgreSQL NetworkPolicy + import_tasks: networkpolicy.yml + when: managed_database + # Managed Database block - name: Check PostgreSQL version to determine if an upgrade is needed import_tasks: check_postgres_version.yml diff --git a/roles/postgres/tasks/networkpolicy.yml b/roles/postgres/tasks/networkpolicy.yml new file mode 100644 index 00000000..ddb88760 --- /dev/null +++ b/roles/postgres/tasks/networkpolicy.yml @@ -0,0 +1,6 @@ +--- +- name: Apply PostgreSQL NetworkPolicy + kubernetes.core.k8s: + state: present + apply: true + definition: "{{ lookup('template', 'postgres.networkpolicy.yaml.j2') }}" diff --git a/roles/postgres/templates/postgres.networkpolicy.yaml.j2 b/roles/postgres/templates/postgres.networkpolicy.yaml.j2 new file mode 100644 index 00000000..fb0e2a8f --- /dev/null +++ b/roles/postgres/templates/postgres.networkpolicy.yaml.j2 @@ -0,0 +1,55 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: '{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}' + namespace: '{{ ansible_operator_meta.namespace }}' + labels: + app.kubernetes.io/name: 'postgres-{{ supported_pg_version }}' + app.kubernetes.io/instance: 'postgres-{{ supported_pg_version }}-{{ ansible_operator_meta.name }}' + app.kubernetes.io/component: database + app.kubernetes.io/part-of: '{{ ansible_operator_meta.name }}' + app.kubernetes.io/managed-by: '{{ deployment_type }}-operator' +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: 'postgres-{{ supported_pg_version }}' + app.kubernetes.io/instance: 'postgres-{{ supported_pg_version }}-{{ ansible_operator_meta.name }}' + app.kubernetes.io/component: database + policyTypes: + - Ingress + - Egress + ingress: + # Allow traffic from EDA operator-managed pods and the operator + # controller-manager (for DB initialization) + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/managed-by + operator: In + values: + - '{{ deployment_type }}-operator' + - 'aap-operator' + - podSelector: + matchLabels: + control-plane: controller-manager + # EDA activation Job pods (created at runtime by activation workers, + # not operator-managed — carry only app: eda, no managed-by label). + # Event stream activations connect to Postgres via pg_notify. + - podSelector: + matchLabels: + app: eda + ports: + - protocol: TCP + port: {{ eda_postgres_port | default('5432') }} + egress: + # Allow DNS resolution via OpenShift DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + ports: + - protocol: TCP + port: 5353 + - protocol: UDP + port: 5353