out_forward: align secure forward handshake and chunk ack with protocol spec - #12037
Conversation
The Forward protocol (v1 and v1.5) defines the shared_key handshake as mutual authentication: the server proves it holds the same shared_key by returning sha512_hex(shared_key_salt + server_hostname + nonce + shared_key) in the PONG message. The client validated only the PONG type and auth_result, so any server could pass the handshake without knowing the shared_key. Validate the full PONG shape (exactly 5 fields with the expected types), keep the HELO nonce across the PONG read, compute the expected server digest and reject the connection on mismatch. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
username/password authorization is part of the secure forward handshake which is only attempted when a shared_key is configured. Credentials set without shared_key or empty_shared_key were silently unused, letting users believe authentication was enabled when none would happen. Fail at configuration time instead, matching the fail-close behavior of in_forward for security.users. Also propagate config_set_properties() failures in both the simple and HA setup paths, which previously ignored its return value. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
The Forward protocol (v1 and v1.5) defines the 'chunk' option value as a Base64 representation of a 128 bits unique id. out_forward sent a 32-character lowercase hex string instead. Encode the first 16 bytes of the payload SHA512 checksum as Base64 (24 characters). Receivers echo the token byte-for-byte in 'ack', so this stays interoperable while matching the specified encoding. Rename the flush context field from checksum_hex to chunk_token and initialize the formatter-local token buffers. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
When retain_metadata_in_forward_mode is disabled and the transcode to the older Forward representation fails, the formatter returned success with an empty options buffer. Return -1 instead and make the flush callback retry when formatting fails. Also log the 'chunk' debug message from the produced token only: when require_ack_response is disabled the output buffer is never written, so formatter-only paths could print uninitialized stack data. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
Extend the mock secure forward server to complete the handshake and compute the PONG digest from the captured PING salt. Cover a valid PONG (data flows), a wrong server digest, a missing digest field and a wrong digest type (all rejected with no data sent), username/password without shared_key failing at startup, and update the ack chunk assertion to the Base64 token format. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
Teach the python forward receiver the shared_key handshake (HELO, PING digest validation, PONG with a real or intentionally corrupted server digest) and add an out_forward scenario that verifies the end-to-end secure handshake delivers records with a spec-compliant PING digest and Base64 chunk token, and that a wrong server digest is rejected without delivering data. Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
📝 WalkthroughWalkthroughThis PR reworks the secure forward handshake in out_forward to use a generalized shared-key digest incorporating hostname and nonce, tightens PONG validation, adds config checks, switches the ACK "chunk" token from hex to Base64, and adds runtime/integration test coverage. ChangesSecure forward handshake and chunk token changes
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Client
participant Server
Client->>Server: HELO
Server-->>Client: HELO options (nonce)
Client->>Server: PING (hostname, nonce, digest)
Server->>Server: verify client digest via secure_forward_ping
Server-->>Client: PONG (server_hostname, server digest)
Client->>Client: compute expected digest via secure_forward_hash_key_digest
Client->>Client: compare expected vs received digest, accept/reject
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
tests/integration/src/server/forward_server.py (1)
316-326: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winServer never rejects on invalid client digest.
client_digest_validis computed and recorded (Line 318) but the PONG always sendsauth_result=True(Line 326), regardless of that value. This means there's no test coverage for the server-side rejection path (client presents wrong shared key). Consider wiringauth_result/reasontoclient_digest_validso future tests can exercise that scenario too.♻️ Proposed enhancement
expected_client_digest = _sha512_hex(shared_key_salt, client_hostname, nonce, shared_key) + client_digest_valid = client_digest == expected_client_digest data_storage["handshakes"].append({ "client_hostname": client_hostname, - "client_digest_valid": client_digest == expected_client_digest, + "client_digest_valid": client_digest_valid, }) server_digest = _sha512_hex(shared_key_salt, self_hostname, nonce, shared_key) if server_options["corrupt_pong_digest"]: first = "1" if server_digest[0] == "0" else "0" server_digest = first + server_digest[1:] - conn.sendall(_pack_obj(["PONG", True, "", self_hostname, server_digest])) + conn.sendall(_pack_obj([ + "PONG", + client_digest_valid, + "" if client_digest_valid else "shared key mismatch", + self_hostname, + server_digest, + ]))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/src/server/forward_server.py` around lines 316 - 326, The forward_server handshake currently records client_digest_valid in data_storage but still always sends PONG with auth_result set to true, so the invalid-client-digest path is never exercised. Update the handshake logic in the server connection flow to derive the PONG auth_result and reason from client_digest_valid, using the existing client_digest_valid/server_digest logic in forward_server.py so invalid shared-key clients are rejected and can be covered by tests.tests/integration/scenarios/out_forward/tests/test_out_forward_secure_001.py (1)
23-58: 🚀 Performance & Scalability | 🔵 TrivialDuplicated msgpack packing helpers across test-support files.
_pack_uint/_pack_str/_pack_objhere largely re-implement logic already present intests/integration/src/server/forward_server.py. Could be consolidated into a shared test utility module to avoid drift between the two encoders.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/scenarios/out_forward/tests/test_out_forward_secure_001.py` around lines 23 - 58, The msgpack packing helpers in this test file duplicate the encoder logic already implemented in forward_server.py, so consolidate them into a shared test utility module and have both places import the same _pack_uint, _pack_str, and _pack_obj behavior. Update the test helper to reference the shared encoder symbols instead of maintaining a separate copy, keeping the existing packing API intact while removing the duplicate implementation.plugins/out_forward/forward_format.c (1)
457-466: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
append_optionsreturn value is ignored.
append_optionscan return-1(hash/base64 failure) before it callsflb_mp_map_header_end, leaving the msgpack map header unterminated. Both call sites here ignore the return and still emitout_bufwithreturn 0, so a malformed payload could be sent. The new transcode-failure handling above sets a good precedent; consider propagatingappend_optionsfailures the same way.♻️ Proposed handling
entries = flb_mp_count(transcoded_buffer, transcoded_length); - append_options(ctx, fc, event_type, &mp_pck, entries, - transcoded_buffer, - transcoded_length, - NULL, chunk); - - free(transcoded_buffer); + result = append_options(ctx, fc, event_type, &mp_pck, entries, + transcoded_buffer, + transcoded_length, + NULL, chunk); + free(transcoded_buffer); + if (result != 0) { + msgpack_sbuffer_destroy(&mp_sbuf); + return -1; + } } else { - append_options(ctx, fc, event_type, &mp_pck, entries, (char *) data, bytes, NULL, chunk); + if (append_options(ctx, fc, event_type, &mp_pck, entries, + (char *) data, bytes, NULL, chunk) != 0) { + msgpack_sbuffer_destroy(&mp_sbuf); + return -1; + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/out_forward/forward_format.c` around lines 457 - 466, The append_options call in forward_format.c is not checked, so failures from hash/base64 encoding can still leave the msgpack map incomplete and the function incorrectly returns success. Update both call sites in the transcode and non-transcode paths to capture the return from append_options and propagate a failure the same way the transcode-failure handling does, ensuring the out_buf payload is only emitted after a successful append_options and completed msgpack header.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@plugins/out_forward/forward_format.c`:
- Around line 457-466: The append_options call in forward_format.c is not
checked, so failures from hash/base64 encoding can still leave the msgpack map
incomplete and the function incorrectly returns success. Update both call sites
in the transcode and non-transcode paths to capture the return from
append_options and propagate a failure the same way the transcode-failure
handling does, ensuring the out_buf payload is only emitted after a successful
append_options and completed msgpack header.
In
`@tests/integration/scenarios/out_forward/tests/test_out_forward_secure_001.py`:
- Around line 23-58: The msgpack packing helpers in this test file duplicate the
encoder logic already implemented in forward_server.py, so consolidate them into
a shared test utility module and have both places import the same _pack_uint,
_pack_str, and _pack_obj behavior. Update the test helper to reference the
shared encoder symbols instead of maintaining a separate copy, keeping the
existing packing API intact while removing the duplicate implementation.
In `@tests/integration/src/server/forward_server.py`:
- Around line 316-326: The forward_server handshake currently records
client_digest_valid in data_storage but still always sends PONG with auth_result
set to true, so the invalid-client-digest path is never exercised. Update the
handshake logic in the server connection flow to derive the PONG auth_result and
reason from client_digest_valid, using the existing
client_digest_valid/server_digest logic in forward_server.py so invalid
shared-key clients are rejected and can be covered by tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e238ed8f-303e-4bdc-b52a-735b7c576d1d
📒 Files selected for processing (7)
plugins/out_forward/forward.cplugins/out_forward/forward.hplugins/out_forward/forward_format.ctests/integration/scenarios/out_forward/config/out_forward_secure_sender.yamltests/integration/scenarios/out_forward/tests/test_out_forward_secure_001.pytests/integration/src/server/forward_server.pytests/runtime/out_forward.c
This PR aligns out_forward with the Fluentd Forward protocol specification (v1 / v1.5), based on a review of the local Forward implementation against the spec documents.
A few details of the out_forward implementation diverged from what the spec describes: the client did not check the server_hostname and shared_key_hexdigest fields that servers return in the PONG message, username/password settings were silently unused when no shared_key was configured, the chunk ack token was sent as a hex string although the spec defines it as a Base64 representation of a 128-bit unique id, and the formatter had two small robustness bugs (a failed metadata transcode still returned success, and a debug log could print an uninitialized buffer).
Fluent Bit is licensed under Apache 2.0, by submitting this pull request I understand that this code will be released under the terms of that license.
Summary by CodeRabbit
New Features
Bug Fixes
Tests