Skip to content

Add TLS Support#533

Merged
stevegrubb merged 29 commits into
masterfrom
tls
Jul 7, 2026
Merged

Add TLS Support#533
stevegrubb merged 29 commits into
masterfrom
tls

Conversation

@stevegrubb

Copy link
Copy Markdown
Contributor

See individual commits

sergio-correia and others added 29 commits June 29, 2026 23:55
Add --enable-tls build option (OpenSSL >= 3.5), client-side TLS
config parsing, and TLS transport to the audisp-remote plugin.

The transport uses TLS 1.3 with X25519MLKEM768 hybrid key exchange
for post-quantum confidentiality, with classical X25519 fallback
when PQC groups are unavailable. The tls_require_pqc option enables
fail-closed PQC enforcement via an allowlist in common/common.h.

Both PSK and certificate-based authentication are supported. Server
certificate verification is gated on tls_ca_file presence, with
hostname/IP-aware SNI handling per RFC 6066. Session resumption
and 0-RTT are disabled to force fresh key exchange per connection.

Shared TLS helpers (is_pqc_group, tls_validate_key_file,
tls_load_psk) are placed in common/common.h with a log callback
to avoid code duplication with the server side.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add server-side TLS config parsing and transport to auditd for
receiving audit events over encrypted connections.

Mirrors the client-side TLS implementation with the same crypto
defaults: TLS 1.3 minimum, X25519MLKEM768 hybrid key exchange,
session resumption disabled. Adds tls_client_auth for optional
or required mutual TLS with client certificates.

PSK identity comparison uses CRYPTO_memcmp. Identity logging is
sanitized to ASCII printable range. TLS config strings are freed
during SIGHUP reconfigure to prevent leaks.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add test-tls.sh covering PSK and certificate handshakes, PQC key
exchange negotiation, and binary linkage checks. Hardened with
set -euo pipefail and dynamic port allocation.

Document all TLS config options in both man pages, including PQC
posture differences between PSK and certificate modes, certificate
chain support, and SIGHUP reload limitations.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
The blocking SSL_accept held the single-threaded libev event loop
for up to 5 seconds per connection, allowing a slow or malicious
client to stall audit event processing for all connected clients.

Replace it with a non-blocking state machine driven by ev_io and
ev_timer callbacks. Pre-handshake clients live in a separate chain
with a concurrency limit to prevent connection flooding. Per-address
counting walks both chains so a single IP cannot exhaust the global
handshake pool.

Also fixes a config pointer scope bug where tls_require_pqc
referenced an out-of-scope variable in the accept handler.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Move role-neutral TLS helper functions from common/common.h static
inlines into a dedicated autls/ internal library (libautls.la).
This keeps the OpenSSL dependency out of libaucommon consumers and
gives the TLS code a clear ownership boundary.

The library contains three source files:
- autls-psk.c: key file validation and PSK loading
- autls-io.c: deadline computation, TLS write, and TLS shutdown
- autls-profile.c: PQC group classification and cipher selection

All functions are renamed from tls_* to autls_* and constants from
TLS_*_TIMEOUT_MS to AUTLS_*_TIMEOUT_MS. Consumers (auditd,
audisp-remote, test-tls-helpers) now link libautls.la instead of
raw $(OPENSSL_LIBS).

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add __nonnull, __wur, __attr_access, and __attribute_pure__ to
the autls public API using the existing gcc-attributes.h wrappers.

This enables compile-time detection of NULL pointer misuse,
ignored return values, and buffer access violations.

autls_is_pqc_group intentionally omits __nonnull because it
accepts NULL input (returns 0).

Signed-off-by: Sergio Correia <scorreia@redhat.com>
Raise the minimum OpenSSL version from 1.1.1 to 3.0 to match the
APIs actually used (SSL_group_to_name, SSL_get_negotiated_group).

Add function probes for the TLS 1.3 APIs the implementation depends
on: SSL_CTX_set_ciphersuites, SSL_CTX_set_psk_find_session_callback,
SSL_CTX_set_psk_use_session_callback, and SSL_get_negotiated_group.
Missing required APIs now fail at configure time instead of at link
time with undefined symbol errors.

SSL_group_to_name is probed as optional and guarded with
HAVE_SSL_GROUP_TO_NAME at call sites so the code degrades gracefully
on older OpenSSL 3.x builds.

Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add autls_validate_psk_identity() to enforce printable ASCII
(0x21-0x7E) for PSK identities, and autls_profile_ciphers() /
autls_profile_groups() to map crypto profile selections to
OpenSSL cipher and group strings.

These are the shared building blocks for Phase 0's policy surface
(tls_crypto_profile) and identity authorization. The profile
functions accept an integer profile value and return the
appropriate TLS 1.3 ciphersuite or key exchange group string,
with FIPS deferring to system crypto policy via NULL.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add the Phase 0 policy surface to both auditd and audisp-remote
config parsers: tls_auth (accepts "psk" only for tech preview)
and tls_crypto_profile (standard, fips, pqc).

The tls_require_pqc option becomes a one-directional compatibility
alias: "yes" sets tls_crypto_profile=pqc, "no" is a no-op that
does not modify the profile.

Key changes:
- Profile enum defined in auditd-config.h (not autls.h) so the
  shared config parser can use it without linking libautls
- Sanity check enforces tls_auth=psk requires tls_psk_file,
  rejecting cert-only configs under the PSK-only tech preview
- tls_client_auth default changed to TCA_NONE since PSK mode
  does not use certificate-based client authentication

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Replace hard-coded cipher suite and key exchange group defaults
with autls_profile_ciphers() and autls_profile_groups(), driven
by the new tls_crypto_profile setting.

When a profile function returns NULL (FIPS), the corresponding
SSL_CTX call is skipped entirely, deferring to system crypto
policy. The STANDARD profile preserves the existing fallback
from PQC hybrid to X25519 when ML-KEM groups are unavailable.
PQC and FIPS profiles fail closed with no fallback.

The post-handshake PQC group check now uses the profile enum
(== TLS_PROFILE_PQC) instead of the legacy tls_require_pqc
flag. A _Static_assert guards the profile constant values
between autls.h and the config headers.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Remove the silent fallback to "audit-client" as the default PSK
identity in audisp-remote. The identity is now required and
validated at TLS context init time via
autls_validate_psk_identity(), which rejects non-printable-ASCII
characters and overlength strings.

The same validation is added to the collector side in
init_tls_server_context() for the expected_psk_identity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add autls_acl_load(), autls_acl_check(), and autls_acl_free() for
parsing the collector-side client authorization file. The file
format is one entry per line: identity, status (enabled/disabled),
and optional notes.

The parser validates file permissions (root-owned, not group or
world writable, regular file, opened with O_NOFOLLOW), validates
each identity via autls_validate_psk_identity(), rejects duplicate
identities and unknown status values, and uses case-insensitive
status matching.

Identity lookup uses length-bounded comparison (strlen + memcmp)
to safely handle the non-NUL-terminated buffers from OpenSSL's
PSK callback.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add the tls_allowed_clients config option and integrate ACL-based
authorization into the collector's TLS PSK callback.

Config changes:
- Add tls_allowed_clients path field to daemon_conf
- Relax sanity_check to accept tls_psk_identity OR
  tls_allowed_clients (at least one required with PSK)
- Add parser, default, free, and stub

PSK callback authorization:
- Validate incoming identity with autls_validate_psk_identity()
  before any authorization check
- When ACL is loaded, check identity against the ACL table;
  reject unknown and disabled identities with stable reasons
- When no ACL, fall back to expected_psk_identity comparison
- Store accepted identity in SSL ex-data (strndup'd) for
  transfer to ev_tcp after handshake

Identity lifecycle:
- Ex-data indices registered without free callbacks (manual
  cleanup on all paths to prevent double-free)
- Identity transferred from ex-data to client->accepted_identity
  after PQC check passes (so PQC failures go through abort path)
- abort_handshake retrieves ex-data before SSL_free
- release_client frees accepted_identity
- uninit frees the ACL table

Single-PSK constraint enforced at init: at most one enabled
identity when tls_psk_file is set.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Replace the simple addr/port/res audit records with structured
TLS establishment records containing:

  op=tls-accept res=<success|no> role=collector addr=<> port=<>
  id=<identity> profile=<standard|fips|pqc> tls_version=<>
  cipher=<> group=<> auth=psk reason=<stable reason>

Add emit_tls_audit_record() helper with a 512-byte buffer and
critical fields (op, res) placed first to survive truncation.
Identity is truncated to 128 chars via %.128s.

The abort_handshake() path now retrieves the failure reason from
SSL ex-data (set by the PSK callback) and falls back to the op
parameter when ex-data is NULL. This wires up the stable failure
reasons: unknown-identity, disabled-identity, invalid-identity,
handshake-timeout, unsupported-profile, openssl-error.

Also adds a LOG_WARNING when strndup fails for the accepted
identity so operators know the connection lacks attribution.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add tls_auth, tls_crypto_profile, and tls_allowed_clients to the
TLS field copy block in auditd_tcp_listen_reconfigure(). The path
string ownership is transferred from nconf to oconf (matching the
pattern for all other TLS string fields), while the live ACL table
and TLS context remain untouched -- TLS changes require a restart.

Update the NOTES section in auditd.conf(5) to list all TLS options
that require restart.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Document the Phase 0 TLS PSK tech preview configuration surface:
- tls_auth (psk only for tech preview)
- tls_crypto_profile (standard, fips, pqc)
- tls_allowed_clients (ACL file format, permissions, single-PSK
  constraint)
- tls_require_pqc updated as compatibility alias
- tls_client_auth default updated to none (PSK mode)

Sample configs reorganized to lead with PSK options and profile
selection, with certificate mode options grouped separately and
marked as not supported in this tech preview.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Extend test-tls-helpers with six new test functions covering:

- autls_validate_psk_identity: empty, NUL bytes, control chars,
  space, DEL, high bytes (0x80-0xFF rejected), max/overlength,
  valid edge cases including boundary chars 0x21 and 0x7E
- autls_profile_ciphers: standard/pqc return defaults, fips NULL
- autls_profile_groups: standard/pqc/fips return correct strings
- autls_acl_load: valid file, empty file, duplicates, invalid
  status, missing status, case-insensitive status, group-writable
  file rejected
- autls_acl_check: enabled/disabled/unknown returns, empty
  identity, prefix mismatch (length-bounded comparison)
- autls_authorize_psk_identity: composed validation+ACL path,
  catches if(rc) vs if(rc==1) bug class

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add tests to auditd_config_alloc_test for the new TLS config
parsers, covering:

- tls_auth_parser: accepts "psk", rejects unknown values
- tls_crypto_profile_parser: accepts standard/fips/pqc, rejects
  unknown
- tls_require_pqc compatibility alias: "yes" sets both
  tls_require_pqc and tls_crypto_profile to PQC, "no" is a no-op
  for the profile, and explicit tls_crypto_profile after "yes"
  wins (last-writer-wins sequential parsing)

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
The client_ack TLS path called autls_ssl_write() which blocks the
single-threaded event loop for up to 100ms per client via poll().
With multiple stalled clients, this freezes the loop.

Replace with a non-blocking SSL_write that queues unsent data in a
per-client pending_ack buffer and arms EV_WRITE. The pending write
is drained on the next event loop iteration before reading new data.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Pre-compute and store identity_len in autls_acl_entry at load time.
This avoids re-computing strlen on every autls_acl_check call and
provides a cheap early-out on length mismatch before the constant-time
comparison.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Test the server-side sanity_check TLS validation: cert/key pairing,
PSK/cert mutual exclusion, tls_auth=psk requires PSK file, credential
requirement, PSK identity/ACL requirement, tls_require_pqc consistency,
and valid minimal PSK config.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
- Eliminate TOCTOU in private key loading: add autls_load_key_file()
  using open(O_NOFOLLOW) + fstat + BIO, replacing the separate
  autls_validate_key_file + SSL_CTX_use_PrivateKey_file sequence
- Use validate-before-free pattern in remote-config.c TLS parsers
  to avoid dangling pointer on validation failure
- Use replace_string() in auditd-config.c TLS parsers for atomic
  strdup-before-free
- Add logging to client PSK callback error paths
- Send non-blocking close_notify in release_client to avoid
  stalling the event loop
- Move TLS resource cleanup after client goodbye loop in shutdown
- Add missing oconf->transport update in TLS reconfigure branch
- Distinguish I/O errors from empty file in autls_load_psk via
  ferror() check

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Add libautls.la to auditd_DEPENDENCIES so the link step has an
explicit dependency on the convenience library, not just implicit
SUBDIRS ordering.

Move AC_SUBST for OPENSSL_CFLAGS/OPENSSL_LIBS outside the TLS
conditional so autls/Makefile always gets proper substitution even
when configured without --enable-tls.

Preserve user-supplied OPENSSL_LIBS instead of unconditionally
overwriting with -lssl -lcrypto.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
SIGCHLD is registered with sa_flags=0, so delivery interrupts
blocking syscalls inside SSL_read/SSL_write. OpenSSL surfaces
the resulting EINTR as SSL_ERROR_SYSCALL, which the TLS I/O
paths treat as fatal — triggering a full reconnection cycle.

Add SA_RESTART so SIGCHLD-interrupted syscalls are transparently
restarted by the kernel. Also save and restore errno in the
handler — waitpid sets errno to ECHILD when no children remain,
which can clobber errno between completed syscalls and their
error checks (e.g., in ar_read where errno == 0 gates the
reconnection path).

The other signal handlers (SIGHUP, SIGUSR1/2) intentionally
lack SA_RESTART because they need to interrupt select() to
trigger state changes.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
Set SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER on the client TLS context
to match the server-side setting. This is defensive — the current
autls_ssl_write() does not move the buffer pointer between retries,
but the flag ensures correctness if the write path is refactored.

Assisted-by: Claude Opus 4.6
Signed-off-by: Sergio Correia <scorreia@redhat.com>
The TLS sanity_check unit test created configs with clear_config(), but clear_config() leaves space_left and admin_space_left at zero. The shared sanity_check() rejects that generic disk-space invariant before the test can exercise the TLS-specific constraints, so the supposedly valid TLS case aborted in make check.

Add a small test helper that starts from clear_config() and satisfies the non-TLS disk-space invariant. Use it for the TLS sanity_check cases so those assertions cover the intended TLS credential/profile checks.
The Phase 0 TLS profile names exposed standard, fips, and pqc. The fips name implied that auditd was asserting FIPS enforcement even though the implementation intentionally deferred cipher and group selection to OpenSSL and the system crypto policy. The standard name also did not describe the compatibility-oriented default clearly.

Rename the user-facing profile values to compatible, system, and pqc, and rename the matching internal constants. The system profile preserves the existing delegation behavior by leaving cipher suites and key exchange groups unset so OpenSSL and system crypto-policy choose them. The documentation now tells FIPS deployments to configure the host for FIPS mode and use the system profile.

Updated the parser tests, autls profile helper tests, sample configs, and man pages for the new names.
Phase 0 TLS was still relying on the TLS-specific DAEMON_ACCEPT diagnostics for crypto session visibility. The sender did not retain CAP_AUDIT_WRITE after dropping privileges, and neither side emitted CRYPTO_SESSION/CRYPTO_KEY_USER records in the field order used by other userspace crypto audit producers.

Add shared autls formatters for the CRYPTO_SESSION and CRYPTO_KEY_USER bodies, use audit_log_user_message() from audisp-remote, and retain CAP_AUDIT_WRITE for TLS transport. The collector emits internal CRYPTO_SESSION records with send_audit_event() only after successful TLS handshake and identity authorization, formats the exe field through libaudit's name/value encoder using the configured auditd install path, then closes just the new connection if those records cannot be created. Failed collector handshakes deliberately remain DAEMON_ACCEPT diagnostics only, with comments documenting the audit-flood concern from unauthenticated peers.

Tests cover the shared crypto audit body formatting and truncation behavior.
SIGHUP reconfiguration updated tls_allowed_clients in the active config before the replacement ACL had been parsed and validated. A bad reload could therefore leave auditd reporting the new ACL path while continuing to use the previous in-memory ACL, and removing tls_allowed_clients could clear the only active PSK authorization source without rebuilding the live TLS context.

Load replacement ACLs into a temporary table and only publish the new path and table together after validation succeeds. Failed reloads and single-PSK ACL cardinality errors now keep the old live ACL, and ACL removal is rejected when the live PSK listener has no tls_psk_identity fallback. The new path string is freed when it is not published so the existing reconfigure ownership model does not leak it.

Add listener reconfiguration coverage to format_event_test for invalid ACL replacement, ACL-only removal on a live PSK listener, and successful ACL removal when a fallback identity is active.
@stevegrubb stevegrubb merged commit 3fb4cd5 into master Jul 7, 2026
4 checks passed
@stevegrubb stevegrubb deleted the tls branch July 7, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants