fix(review): close loopback TLS auth bypass; align docs and wrong-CN test

- server gate: the --allow-unauthenticated loopback allowance now requires
  an actual plaintext connection (!gate_ctx->ssl), so a loopback TLS client
  whose cert fails the --client-cn check is refused before any SCRAM
  challenge instead of falling through the plaintext opt-in.  Keep the
  invalid-fd guard as belt-and-braces (unreachable after the policy check).
- test: rewrote test_wrong_client_cn_refused_before_auth_challenge to run
  deterministically over 127.0.0.1 with --tls + --allow-unauthenticated and
  a CA-valid wrong-CN client cert, asserting the gate refusal log and an
  unchanged module tree (no skip).
- docs: --client-cn is mandatory with --tls; dummykey sidecar is secret
  material; document all transient-fallback reasons; qualify
  --allow-unauthenticated in README and --help so it cannot read as
  permitting remote plaintext auth.
- credentials.h: drop stale restrictive-umask claim (fchmod forces exact
  0600; only create/write/fsync/link/fchmod failure degrades to ephemeral).
This commit is contained in:
2026-09-12 19:50:52 +02:00
parent 89b967f29a
commit 1b90ee2449
5 changed files with 49 additions and 40 deletions
+10 -14
View File
@@ -1154,20 +1154,16 @@ class TestDaemonTLSAuth:
@pytest.mark.ci
def test_wrong_client_cn_refused_before_auth_challenge(self):
"""A7-3/S1: over a NON-local TLS connection an auth-required module is
refused at the config gate when the CA-valid client certificate does not
match --client-cn -- before any SCRAM challenge is sent and before any
file data moves. The daemon is started WITH --allow-unauthenticated to
prove that flag never relaxes the remote auth-module transport policy
(it only opts in plaintext from a loopback peer)."""
try:
remote_ip = socket.gethostbyname(socket.gethostname())
except OSError:
pytest.skip("hostname does not resolve")
if remote_ip.startswith("127."):
pytest.skip("host resolves to loopback; no non-loopback interface")
"""A7-3/S1: with --tls AND --allow-unauthenticated, a loopback TLS peer
whose CA-valid client certificate does not match --client-cn is still
refused at the config gate -- before any SCRAM challenge is sent and
before any file data moves. The --allow-unauthenticated flag only opts
in loopback PLAINTEXT; it must never turn a wrong-CN TLS peer into an
accepted auth transport. Runs over 127.0.0.1 so it is deterministic and
never skips; the gate log line (emitted before server_auth_handshake)
plus the unchanged module tree prove the refusal preceded any challenge."""
cert_dir = os.path.join(TEST_DATA_DIR, "daemon_tls_certs_wrong")
certs = _generate_tls_certs(cert_dir, extra_san_ips=[remote_ip])
certs = _generate_tls_certs(cert_dir)
client_creds = os.path.join(TEST_DATA_DIR, "daemon_tls_wrong_client.pw")
_write_client_password_file(client_creds, "alice", ALICE_PASS)
d = DaemonManager()
@@ -1183,7 +1179,7 @@ class TestDaemonTLSAuth:
tls_flags = ["--tls",
"--cert", certs["wrong_client_cert"], "--key",
certs["wrong_client_key"], "--ca", certs["ca"]]
result, _ = run_client(SOURCE_DIR, "%s::locked" % remote_ip, port=port,
result, _ = run_client(SOURCE_DIR, "127.0.0.1::locked", port=port,
flags=tls_flags, extra_args=["--password-file", client_creds])
assert result.returncode != 0, "a wrong client CN must be refused"
assert _tree_file_count(AUTH_MODULE) == before_files, \