281 lines
14 KiB
YAML
Executable File
281 lines
14 KiB
YAML
Executable File
---
|
|
- name: Validate Luanti WASM role without a cluster
|
|
hosts: localhost
|
|
connection: local
|
|
gather_facts: false
|
|
vars:
|
|
# Non-routable documentation fixtures only; never pulled or deployed.
|
|
luanti_wasm_web_image: registry.invalid/fixture/luanti-web:test-1
|
|
luanti_wasm_proxy_image: "registry.invalid/fixture/lwproxy@sha256:{{ 'a' * 64 }}"
|
|
luanti_wasm_game_cidrs: [192.0.2.15/32, '2001:db8::15/128']
|
|
tasks:
|
|
- name: Load defaults and validate fixture settings
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: validate.yml
|
|
public: true
|
|
|
|
- name: Render the actual role templates
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: render.yml
|
|
|
|
- name: Check private stateless defaults and service isolation
|
|
ansible.builtin.assert:
|
|
that:
|
|
- not luanti_wasm_ingress_enabled
|
|
- _luanti_wasm_manifests.deployment.apiVersion == 'apps/v1'
|
|
- _luanti_wasm_manifests.deployment.spec.replicas == 1
|
|
- _luanti_wasm_manifests.deployment.spec.template.spec.containers | length == 2
|
|
- _luanti_wasm_manifests.service.spec.type == 'ClusterIP'
|
|
- _luanti_wasm_manifests.service.spec.ports | map(attribute='port') | list == [8080]
|
|
- _luanti_wasm_manifests.service.spec.ports[0].targetPort == 'http'
|
|
- _luanti_wasm_manifests.service.spec.selector == _luanti_wasm_manifests.deployment.spec.template.metadata.labels
|
|
- "'hostNetwork' not in _luanti_wasm_manifests.deployment.spec.template.spec"
|
|
- "'spec' not in _luanti_wasm_manifests.ingress"
|
|
- _luanti_wasm_manifests.deployment.spec.template.spec.volumes | length == 2
|
|
- _luanti_wasm_manifests.deployment.spec.template.spec.volumes | selectattr('persistentVolumeClaim', 'defined') | list | length == 0
|
|
|
|
- name: Check pod security, bounded memory temporary volume, and probes
|
|
ansible.builtin.assert:
|
|
that:
|
|
- not pod.automountServiceAccountToken
|
|
- pod.securityContext.runAsNonRoot
|
|
- pod.securityContext.seccompProfile.type == 'RuntimeDefault'
|
|
- pod.securityContext.fsGroup == 101
|
|
- pod.volumes[1].emptyDir == {'medium':'Memory', 'sizeLimit':'64Mi'}
|
|
- pod.containers[0].securityContext.runAsUser == 101
|
|
- pod.containers[1].securityContext.runAsUser == 65532
|
|
- pod.containers[0].readinessProbe.httpGet == {'path':'/', 'port':'http'}
|
|
- pod.containers[0].livenessProbe.httpGet.port == 'http'
|
|
- pod.containers[1].readinessProbe.tcpSocket.port == 'proxy'
|
|
- pod.containers[1].livenessProbe.tcpSocket.port == 'proxy'
|
|
- pod.containers[0].image == luanti_wasm_web_image
|
|
- pod.containers[1].image == luanti_wasm_proxy_image
|
|
- pod.containers[0].volumeMounts[0].mountPath == '/etc/nginx/nginx.conf'
|
|
- pod.containers[1].volumeMounts[0].mountPath == '/etc/lwproxy.toml'
|
|
- pod.containers[1].volumeMounts[0].subPath == 'lwproxy.toml'
|
|
vars:
|
|
pod: "{{ _luanti_wasm_manifests.deployment.spec.template.spec }}"
|
|
|
|
- name: Check both containers are hardened and resource bounded
|
|
ansible.builtin.assert:
|
|
that:
|
|
- item.securityContext.runAsNonRoot
|
|
- item.securityContext.readOnlyRootFilesystem
|
|
- not item.securityContext.allowPrivilegeEscalation
|
|
- item.securityContext.capabilities.drop == ['ALL']
|
|
- item.resources.requests == {'cpu':'100m', 'memory':'64Mi'}
|
|
- item.resources.limits.memory == '256Mi'
|
|
- item.volumeMounts[0].readOnly
|
|
- item.startupProbe.failureThreshold == 30
|
|
- item.ports | selectattr('hostPort', 'defined') | list | length == 0
|
|
loop: "{{ _luanti_wasm_manifests.deployment.spec.template.spec.containers }}"
|
|
|
|
- name: Check WASM headers, release cache contract, and localhost routing
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'add_header Cross-Origin-Opener-Policy same-origin always;' in nginx"
|
|
- "'add_header Cross-Origin-Embedder-Policy require-corp always;' in nginx"
|
|
- "'add_header X-Content-Type-Options nosniff always;' in nginx"
|
|
- "'~^/[0-9a-f]{12}/' in nginx"
|
|
- "'public, max-age=31536000, immutable' in nginx"
|
|
- "'default \"no-cache\";' in nginx"
|
|
- "'proxy_pass http://127.0.0.1:7777;' in nginx"
|
|
- "'proxy_set_header Upgrade $http_upgrade;' in nginx"
|
|
- "'proxy_read_timeout 3600s;' in nginx"
|
|
- "'proxy_buffering off;' in nginx"
|
|
- "'bind_address = \"0.0.0.0:7777\"' in proxy"
|
|
- "'allowed_hosts = [\"content.luanti.org\", \"servers.luanti.org\"]' in proxy"
|
|
- _luanti_wasm_manifests.configmap.metadata.name == 'luanti-wasm-config'
|
|
- _luanti_wasm_manifests.deployment.spec.template.spec.volumes[0].configMap.name == _luanti_wasm_manifests.configmap.metadata.name
|
|
vars:
|
|
nginx: "{{ _luanti_wasm_manifests.configmap.data['nginx.conf'] }}"
|
|
proxy: "{{ _luanti_wasm_manifests.configmap.data['lwproxy.toml'] }}"
|
|
|
|
- name: Check mandatory policy and absence of wildcard egress
|
|
ansible.builtin.assert:
|
|
that:
|
|
- policy.policyTypes == ['Ingress', 'Egress']
|
|
- policy.podSelector.matchLabels == _luanti_wasm_manifests.service.spec.selector
|
|
- policy.ingress == [{'ports':[{'port':8080, 'protocol':'TCP'}]}]
|
|
- policy.egress | length == 2
|
|
- policy.egress[0].to == [{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'kube-system'}}, 'podSelector':{'matchLabels':{'k8s-app':'kube-dns'}}}]
|
|
- policy.egress[0].ports == [{'port':53, 'protocol':'UDP'}, {'port':53, 'protocol':'TCP'}]
|
|
- policy.egress[1].to | map(attribute='ipBlock.cidr') | list == luanti_wasm_game_cidrs
|
|
- policy.egress[1].ports == [{'port':30000, 'protocol':'UDP'}]
|
|
vars:
|
|
policy: "{{ _luanti_wasm_manifests.networkpolicy.spec }}"
|
|
|
|
- name: Preserve initial checksum
|
|
ansible.builtin.set_fact:
|
|
_initial_checksum: "{{ _luanti_wasm_manifests.deployment.spec.template.metadata.annotations['checksum/config'] }}"
|
|
|
|
- name: Re-render identical settings
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: render.yml
|
|
|
|
- name: Check deterministic SHA256 checksum
|
|
ansible.builtin.assert:
|
|
that:
|
|
- _initial_checksum is match('^[a-f0-9]{64}$')
|
|
- _initial_checksum == _luanti_wasm_manifests.deployment.spec.template.metadata.annotations['checksum/config']
|
|
|
|
- name: Configure optional egress and ingress restrictions
|
|
ansible.builtin.set_fact:
|
|
luanti_wasm_https_cidrs: [198.51.100.0/24, '2001:db8:1::/64']
|
|
luanti_wasm_ingress_peers:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: ingress-nginx
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/component: controller
|
|
- ipBlock:
|
|
cidr: 203.0.113.0/24
|
|
except: [203.0.113.1/32]
|
|
luanti_wasm_http_allowed_hosts: [content.luanti.org]
|
|
luanti_wasm_websocket_timeout_seconds: 7200
|
|
luanti_wasm_replicas: 2
|
|
|
|
- name: Validate optional settings
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: validate.yml
|
|
|
|
- name: Render optional settings
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: render.yml
|
|
|
|
- name: Check optional rules never widen ports or open arbitrary egress
|
|
ansible.builtin.assert:
|
|
that:
|
|
- _luanti_wasm_manifests.deployment.spec.replicas == 2
|
|
- _luanti_wasm_manifests.networkpolicy.spec.ingress[0]['from'] == luanti_wasm_ingress_peers
|
|
- _luanti_wasm_manifests.networkpolicy.spec.ingress[0].ports == [{'port':8080, 'protocol':'TCP'}]
|
|
- _luanti_wasm_manifests.networkpolicy.spec.egress | length == 3
|
|
- _luanti_wasm_manifests.networkpolicy.spec.egress[2].ports == [{'port':443, 'protocol':'TCP'}]
|
|
- _luanti_wasm_manifests.networkpolicy.spec.egress[2].to | map(attribute='ipBlock.cidr') | list == luanti_wasm_https_cidrs
|
|
- _initial_checksum != _luanti_wasm_manifests.deployment.spec.template.metadata.annotations['checksum/config']
|
|
- "'proxy_read_timeout 7200s;' in _luanti_wasm_manifests.configmap.data['nginx.conf']"
|
|
|
|
- name: Accept latest tags for both runtime images
|
|
ansible.builtin.set_fact:
|
|
luanti_wasm_web_image: registry.invalid/fixture/luanti-web:latest
|
|
luanti_wasm_proxy_image: registry.invalid/fixture/lwproxy:latest
|
|
|
|
- name: Validate latest-tagged runtime images
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: validate.yml
|
|
|
|
- name: Restore pinned test image references
|
|
ansible.builtin.set_fact:
|
|
luanti_wasm_web_image: registry.invalid/fixture/luanti-web:test-1
|
|
luanti_wasm_proxy_image: "registry.invalid/fixture/lwproxy@sha256:{{ 'a' * 64 }}"
|
|
|
|
- name: Supply fixture eom inputs
|
|
ansible.builtin.set_fact:
|
|
eom_luanti_wasm_web_image: registry.invalid/fixture/eom-web:release-1
|
|
eom_luanti_wasm_proxy_image: registry.invalid/fixture/eom-proxy:release-1
|
|
eom_luanti_wasm_game_cidrs: [192.0.2.15/32]
|
|
|
|
- name: Load real eom playbook variables
|
|
ansible.builtin.set_fact:
|
|
# Rendering an entire YAML file quotes its embedded list expressions;
|
|
# restore those two list types, as native playbook evaluation does.
|
|
"{{ item.key }}": "{{ (item.value | from_yaml) if item.key in ['luanti_wasm_game_cidrs', 'luanti_wasm_https_cidrs'] else item.value }}"
|
|
loop: "{{ (lookup('ansible.builtin.template', '../playbooks/deploy-eom.yml') | from_yaml)[0].vars | dict2items }}"
|
|
|
|
- name: Validate eom deployment settings without deploying
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: validate.yml
|
|
|
|
- name: Render eom deployment settings
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: render.yml
|
|
|
|
- name: Check TLS ingress and eom integration
|
|
ansible.builtin.assert:
|
|
that:
|
|
- _luanti_wasm_manifests.ingress.apiVersion == 'networking.k8s.io/v1'
|
|
- _luanti_wasm_manifests.ingress.spec.ingressClassName == 'nginx'
|
|
- _luanti_wasm_manifests.ingress.spec.rules[0].host == 'luanti.eom.dev'
|
|
- _luanti_wasm_manifests.ingress.spec.tls == [{'hosts':['luanti.eom.dev'], 'secretName':'luanti-wasm-tls'}]
|
|
- _luanti_wasm_manifests.ingress.spec.rules[0].http.paths[0].backend.service.port.number == 8080
|
|
- _luanti_wasm_manifests.ingress.metadata.annotations['cert-manager.io/cluster-issuer'] == 'ca-issuer'
|
|
- _luanti_wasm_manifests.ingress.metadata.annotations['nginx.ingress.kubernetes.io/proxy-read-timeout'] == '3600'
|
|
- _luanti_wasm_manifests.deployment.spec.template.spec.containers[0].image == eom_luanti_wasm_web_image
|
|
|
|
- name: Exercise external TLS and configurable annotations
|
|
ansible.builtin.set_fact:
|
|
luanti_wasm_cluster_issuer: ""
|
|
luanti_wasm_ingress_annotations:
|
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "7200"
|
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "7200"
|
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
|
|
- name: Render external TLS configuration
|
|
ansible.builtin.include_role:
|
|
name: ansible-role-luanti-wasm
|
|
tasks_from: render.yml
|
|
|
|
- name: Check cert-manager is optional but TLS is not
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'cert-manager.io/cluster-issuer' not in _luanti_wasm_manifests.ingress.metadata.annotations"
|
|
- _luanti_wasm_manifests.ingress.spec.tls | length == 1
|
|
- _luanti_wasm_manifests.ingress.metadata.annotations['nginx.ingress.kubernetes.io/proxy-read-timeout'] == '7200'
|
|
|
|
- name: Capture valid baseline for rejection tests
|
|
ansible.builtin.set_fact:
|
|
_luanti_baseline: "{{ vars | dict2items | selectattr('key', 'match', '^luanti_wasm_') | items2dict }}"
|
|
|
|
- name: Reject invalid settings at role validation, never at Kubernetes
|
|
ansible.builtin.include_tasks: invalid.yml
|
|
loop:
|
|
- {luanti_wasm_web_image: ""}
|
|
- {luanti_wasm_proxy_image: ""}
|
|
- {luanti_wasm_web_image: 'registry.invalid/team/web'}
|
|
- {luanti_wasm_proxy_image: 'registry.invalid/team/proxy@sha256:abc'}
|
|
- {luanti_wasm_web_image: 'https://registry.invalid/team/web:1'}
|
|
- {luanti_wasm_game_cidrs: []}
|
|
- {luanti_wasm_game_cidrs: ['0.0.0.0/0']}
|
|
- {luanti_wasm_https_cidrs: ['::/0']}
|
|
- {luanti_wasm_game_cidrs: ['192.0.2.999/32']}
|
|
- {luanti_wasm_game_cidrs: ['192.0.2.1/24']}
|
|
- {luanti_wasm_game_cidrs: ['2001:db8:::/64']}
|
|
- {luanti_wasm_game_cidrs: ['2001:db8::/129']}
|
|
- {luanti_wasm_game_cidrs: ['luanti.eom.dev/32']}
|
|
- {luanti_wasm_game_cidrs: '192.0.2.0/24'}
|
|
- {luanti_wasm_ingress_peers: [{}]}
|
|
- {luanti_wasm_ingress_peers: ['not-an-object']}
|
|
- {luanti_wasm_ingress_peers: [{namespaceSelector: 'not-an-object'}]}
|
|
- {luanti_wasm_ingress_peers: [{ports: [7777]}]}
|
|
- {luanti_wasm_ingress_peers: [{ipBlock: {cidr: '0.0.0.0/0'}}]}
|
|
- {luanti_wasm_ingress_peers: [{ipBlock: {cidr: '192.0.2.0/24', except: ['198.51.100.0/24']}}]}
|
|
- {luanti_wasm_tls_enabled: false}
|
|
- {luanti_wasm_tls_secret_name: ""}
|
|
- {luanti_wasm_hostname: 'https://luanti.eom.dev/'}
|
|
- {luanti_wasm_hostname: '*.eom.dev'}
|
|
- {luanti_wasm_name: 'Invalid_Name'}
|
|
- {luanti_wasm_name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}
|
|
- {luanti_wasm_namespace: 'bad.namespace'}
|
|
- {luanti_wasm_replicas: 0}
|
|
- {luanti_wasm_replicas: '2'}
|
|
- {luanti_wasm_replicas: true}
|
|
- {luanti_wasm_game_port: 65536}
|
|
- {luanti_wasm_websocket_timeout_seconds: '3600; bad'}
|
|
- {luanti_wasm_ingress_enabled: 'true'}
|
|
- {luanti_wasm_image_pull_policy: Sometimes}
|
|
- {luanti_wasm_ingress_annotations: {example.org/key: false}}
|
|
- {luanti_wasm_http_allowed_hosts: ['*.luanti.org']}
|
|
- {luanti_wasm_web_resources: {requests: {cpu: 100m}}}
|
|
- {luanti_wasm_tmp_size_limit: unbounded}
|
|
loop_control:
|
|
loop_var: invalid_settings
|