diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 208f49924..345885210 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -116,6 +116,7 @@ _CATALOG: tuple[dict[str, Any], ...] = ( { "catalog_id": "ansible:nginx-sync", "playbook_path": "infra/ansible/playbooks/nginx-sync.yml", + "check_mode_playbook_path": "infra/ansible/playbooks/nginx-sync-readonly.yml", "inventory_hosts": ["host_110", "host_188"], "domains": ["nginx", "proxy", "ollama_proxy", "tls"], "keywords": ["nginx", "proxy", "ollama", "gcp", "tls", "cert", "502", "upstream"], diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 5d8db2eda..e99b61f11 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1294,6 +1294,9 @@ def test_ansible_truth_keeps_catalog_hint_separate_from_runtime_use() -> None: assert truth["records"] == [] assert truth["not_used_reason"].startswith("no automation_operation_log row") assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:nginx-sync" + assert truth["candidate_catalog"]["candidates"][0]["check_mode_playbook_path"] == ( + "infra/ansible/playbooks/nginx-sync-readonly.yml" + ) assert truth["candidate_catalog"]["candidates"][0]["approval_required"] is False assert truth["candidate_catalog"]["decision_effect"] == "none" diff --git a/infra/ansible/playbooks/nginx-sync-readonly.yml b/infra/ansible/playbooks/nginx-sync-readonly.yml new file mode 100644 index 000000000..bd288c4c8 --- /dev/null +++ b/infra/ansible/playbooks/nginx-sync-readonly.yml @@ -0,0 +1,94 @@ +--- +# Read-only preflight for the nginx-sync catalog entry. +# This path never uses sudo and remains safe when the privileged apply executor +# is unavailable. The mutating nginx-sync.yml remains the controlled apply path. + +- name: "188 Nginx read-only evidence" + hosts: host_188 + gather_facts: false + become: false + + tasks: + - name: "Preflight | collect remote user" + ansible.builtin.command: + cmd: "id -un" + register: remote_identity + changed_when: false + check_mode: false + + - name: "Nginx | inspect public gateway configuration" + ansible.builtin.stat: + path: /etc/nginx/sites-enabled/all-sites.conf + register: public_gateway_config + failed_when: false + + - name: "Nginx | inspect internal tools HTTPS configuration" + ansible.builtin.stat: + path: /etc/nginx/sites-enabled/188-internal-tools-https.conf + register: internal_tools_config + failed_when: false + + - name: "Nginx | collect listening sockets" + ansible.builtin.command: + cmd: "ss -ltn" + register: listening_sockets + changed_when: false + failed_when: false + check_mode: false + + - name: "Evidence | emit 188 read-only summary" + ansible.builtin.debug: + msg: + remote_user: "{{ remote_identity.stdout | default('unknown') }}" + public_gateway_config_present: "{{ public_gateway_config.stat.exists | default(false) }}" + internal_tools_config_present: "{{ internal_tools_config.stat.exists | default(false) }}" + https_listener_present: "{{ ':443 ' in (listening_sockets.stdout | default('')) }}" + +- name: "110 Ollama proxy read-only evidence" + hosts: host_110 + gather_facts: false + become: false + + tasks: + - name: "Preflight | collect remote user" + ansible.builtin.command: + cmd: "id -un" + register: remote_identity + changed_when: false + check_mode: false + + - name: "Nginx | inspect managed Ollama proxy configuration" + ansible.builtin.stat: + path: /etc/nginx/sites-enabled/110-ollama-proxy.conf + register: managed_proxy_config + failed_when: false + + - name: "Nginx | inspect stale Ollama proxy configuration" + ansible.builtin.stat: + path: /etc/nginx/conf.d/ollama-gcp-proxy.conf + register: stale_proxy_config + failed_when: false + + - name: "Ollama | probe configured proxy listeners" + ansible.builtin.uri: + url: "http://127.0.0.1:{{ item }}/api/tags" + method: GET + return_content: false + status_code: [200, 502, 503, 504] + timeout: 6 + loop: + - 11435 + - 11436 + - 11437 + register: proxy_health + failed_when: false + changed_when: false + check_mode: false + + - name: "Evidence | emit 110 read-only summary" + ansible.builtin.debug: + msg: + remote_user: "{{ remote_identity.stdout | default('unknown') }}" + managed_proxy_config_present: "{{ managed_proxy_config.stat.exists | default(false) }}" + stale_proxy_config_present: "{{ stale_proxy_config.stat.exists | default(false) }}" + proxy_http_statuses: "{{ proxy_health.results | default([]) | map(attribute='status', default=-1) | list }}"