He-Pin opened a new issue, #3237:
URL: https://github.com/apache/pekko/issues/3237
### Motivation
When an `SSLException` occurs during TLS unwrap/wrap in the GraphStage TLS
engine (`TlsGraphStage`), the code calls `writeFailureAlert()` which sends a
TLS `close_notify` alert and then gracefully completes the transport output.
This is a behavioral regression from the legacy `TLSActor`, which does not send
any data to the transport on SSL errors with `closeTransport = false` — it
simply errors the user output and stops.
In security-sensitive scenarios (handshake failures, certificate validation
failures, protocol violations), the transport connection should be aborted
immediately rather than gracefully closed with a close notification.
### Current behavior
In `TlsGraphStage.scala`, when an `SSLException` is caught during `doUnwrap`
(line 333) or `doWrap` (line 362), the code calls `failTls(ex, closeTransport =
false)`:
```scala
// line 584-595
private def failTls(ex: Throwable, closeTransport: Boolean): Unit =
if ((stateBits & StageClosedFlag) == 0) {
userInput.cancel()
transportInput.cancel()
if (closeTransport) {
stateBits |= StageClosedFlag
failStage(ex)
} else {
writeFailureAlert() // sends close_notify to transport
userOutput.failOutput(ex) // fails user output
}
}
```
`writeFailureAlert()` (line 599-610) calls `engine.closeOutbound()` then
`engine.wrap()` to generate and send a `close_notify` alert, silently
swallowing any errors:
```scala
// line 599-610
private def writeFailureAlert(): Unit =
if ((engine ne null) && !transportOutput.isCancelled)
try {
if (!engine.isOutboundDone)
engine.closeOutbound()
val result = engine.wrap(EmptyApplicationBuffer, transportOutBuffer)
lastHandshakeStatus = result.getHandshakeStatus
flushToTransport()
EmptyApplicationBuffer.clear()
} catch {
case NonFatal(_) => EmptyApplicationBuffer.clear()
}
```
After this, `completeOrFlush()` transitions to `CompletedPhase`, and
`pumpFinished()` calls `transportOutput.completeOutput()` — completing the
transport output **normally** instead of failing it. The downstream TCP
connection receives a graceful FIN rather than a connection reset.
### Expected behavior
On `SSLException` during unwrap/wrap, the stage should abort the transport
connection immediately (fail the stage with the exception) rather than sending
a `close_notify` and gracefully completing. This matches:
1. **Legacy `TLSActor` behavior**: `TLSActor.fail()` with `closeTransport =
false` does NOT send any data to the transport. It only errors the user output
and stops (TLSActor.scala line 491-501).
2. **TLS spec guidance**: RFC 5246 Section 7.2 states that fatal alert
messages MUST result in immediate connection termination. Sending a
`close_notify` after a fatal error is redundant and deviates from this guidance.
3. **Security best practice**: Abruptly terminating the connection on SSL
errors prevents any additional data exchange after a security-relevant failure
is detected.
### Code evidence
-
`stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala`:
- Line 333-339: `SSLException` caught in `doInbound` → calls `failTls(ex,
closeTransport = false)`
- Line 362-365: `SSLException` caught in `doOutbound` → calls `failTls(ex,
closeTransport = false)`
- Line 584-595: `failTls` with `closeTransport = false` → calls
`writeFailureAlert()` instead of `failStage(ex)`
- Line 599-610: `writeFailureAlert()` sends `close_notify` and silently
swallows errors
- `stream/src/main/scala/org/apache/pekko/stream/impl/io/TLSActor.scala`:
- Line 491-501: Legacy `fail()` does NOT send any alert — only errors user
output and stops
### Reproduction
1. Set up a TLS connection where the client's trust store does not contain
the server's CA certificate.
2. Use the GraphStage TLS engine (`pekko.stream.materializer.tls.engine =
graph-stage`).
3. Initiate a TLS handshake.
4. Observe that the server receives a `close_notify` alert and the TCP
connection is gracefully closed (FIN) instead of being reset.
5. The existing test `"emit an error if the TLS handshake fails certificate
checks"` in `TlsSpec.scala` (line 477) verifies error propagation to the user
side but does not verify the transport-side behavior (close_notify vs abort).
### Impact
- **Module**: `pekko-stream`, TLS GraphStage engine
- **Affected scenarios**: Any SSL error during handshake or data transfer
when using the opt-in GraphStage TLS engine — certificate validation failures,
protocol violations, corrupted records, etc.
- **Security impact**: Graceful close instead of abort on SSL errors may
mask the nature of the disconnection from peers and network observers, and
deviates from the principle of immediate termination on fatal TLS errors.
- **Behavioral regression**: The legacy `TLSActor` does not exhibit this
behavior; the `writeFailureAlert()` method is a new addition in `TlsGraphStage`
that was not part of the legacy state machine migration.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]