# 可复用:TLS 握手 + SNI,openssl s_client;输出 [OC-ASSERT](Epic 3.3 入口侧 TLS 证据)。 # # 必填:verify_tls_connect_host(建议 IP 或解析到的入口地址)、verify_tls_servername(SNI,常同 HTTP Host) # 可选:verify_tls_port(默认 443)、verify_tls_timeout_sec(默认 10)、 # verify_tls_expect_subject_substring(若设置,则 x509 subject 须包含该子串,grep -F)、 # verify_tls_expect_san_substring(若设置,则 SAN 扩展文本须包含该子串)、 # verify_tls_cafile(若设置,s_client 增加 -CAfile;路径须在执行主机存在)、 # verify_tls_insecure_skip_verify(默认 false;true 时仅审计标注「不对链做强断言」,仍解析对端呈现证书)、 # verify_tls_assertion_label(默认 tls_sni_handshake) - name: Assert tls-openssl-sni required vars ansible.builtin.assert: that: - verify_tls_connect_host is defined - (verify_tls_connect_host | trim | length) > 0 - verify_tls_servername is defined - (verify_tls_servername | trim | length) > 0 fail_msg: "verify_common tls-openssl-sni:需设置 verify_tls_connect_host 与 verify_tls_servername" - name: TLS handshake + certificate subject (openssl s_client) ansible.builtin.shell: | set -euo pipefail assertion={{ (verify_tls_assertion_label | default('tls_sni_handshake')) | quote }} host={{ verify_tls_connect_host | trim | quote }} port={{ verify_tls_port | default(443) | int }} sni={{ verify_tls_servername | trim | quote }} timeout={{ verify_tls_timeout_sec | default(10) | int }} expect={{ (verify_tls_expect_subject_substring | default('') | trim) | quote }} expect_san={{ (verify_tls_expect_san_substring | default('') | trim) | quote }} cafile={{ (verify_tls_cafile | default('') | trim) | quote }} insecure={{ ('1' if (verify_tls_insecure_skip_verify | default(false) | bool) else '0') | quote }} extra_ca="" if [ -n "$cafile" ]; then if [ ! -f "$cafile" ]; then echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=cafile result=fail reason=missing_file path=${cafile}" exit 1 fi extra_ca="-CAfile ${cafile}" fi if [ "$insecure" = "1" ]; then echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=insecure_skip_verify value=1 note=peer_cert_only_lab_or_troubleshoot" fi raw=$(echo | timeout "$timeout" openssl s_client -connect "${host}:${port}" -servername "$sni" ${extra_ca} &1 || true) echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=s_client_log excerpt" echo "$raw" | tail -25 subj=$(echo "$raw" | openssl x509 -noout -subject 2>/dev/null || true) if [ -z "$subj" ]; then echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=subject result=fail reason=no_certificate_or_handshake" exit 1 fi echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=subject value=${subj}" dates=$(echo "$raw" | openssl x509 -noout -dates 2>/dev/null || true) not_after=$(echo "$dates" | grep '^notAfter=' | cut -d= -f2- | tr -d '\r' || true) echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=cert_not_after value=${not_after:-unknown}" san=$(echo "$raw" | openssl x509 -noout -ext subjectAltName 2>/dev/null || true) if [ -z "$(echo "$san" | tr -d '[:space:]')" ]; then san=$(echo "$raw" | openssl x509 -noout -text 2>/dev/null | awk '/X509v3 Subject Alternative Name:/{getline; print; exit}' || true) fi san_log=$(echo "$san" | tr '\n\r\t' ' ' | cut -c1-240) echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=san excerpt=${san_log:-}" if [ -n "$expect" ]; then echo "$subj" | grep -Fq -- "$expect" || { echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=subject_match result=fail expected_substring=${expect}" exit 1 } echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=subject_match result=ok" fi if [ -n "$expect_san" ]; then echo "$san" | grep -Fq -- "$expect_san" || { echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=san_match result=fail expected_substring=${expect_san}" exit 1 } echo "[OC-ASSERT] assertion=${assertion} phase=tls probe=san_match result=ok" fi args: executable: /bin/bash changed_when: false