Summary

  • ALPN carries an ordered client offer of exact, opaque protocol identifiers and lets the server select one mutually supported value. The selection governs application data on that TLS connection; it is not a reusable statement about an origin, a fleet or another transport leg.
  • TLS termination creates a new authority boundary. A proxy can correctly speak HTTP/2 downstream and HTTP/1.1 upstream. Both observations are true, but neither may be copied onto the other connection without its own handshake and application evidence.
  • Defensible operations preserve offer, selection, failure, callback execution and post-handshake protocol confirmation per connection. They keep certificate identity and application authorization as separate decisions, and they never promote registry or configuration presence into running proof.

One correct green light, attached to the wrong system

The edge trace looked exemplary. ClientHello contained two ALPN identifiers in preference order. The server returned h2 in EncryptedExtensions, Finished authenticated the handshake transcript, and each endpoint sent the HTTP/2 connection preface. Requests arrived as HTTP/2 streams.

The upstream trace told another story. The edge acted as a new client toward the origin. Its cluster policy opened TCP, established a separate TLS connection and either omitted ALPN or obtained http/1.1. The proxy converted each downstream stream into an HTTP/1.1 request. Nothing in that design was necessarily defective.

The defect appeared in the data model. An observability pipeline joined the downstream selected value to an origin-service inventory row and stored origin_protocol=h2. It discarded the downstream connection ID, the termination event and the upstream connection ID. A connection fact became a service capability, then a fleet property.

The repair is not to distrust ALPN. It is to respect its jurisdiction. The authenticated fact is strong precisely because it is narrow: on this completed TLS connection, these endpoints selected these exact bytes and then used the corresponding application protocol.

The wire negotiates bytes, not labels

IANA assigns extension type 16 to application_layer_protocol_negotiation. RFC 7301 defines the client's payload as a list of protocol names. Each name is an opaque byte string with a one-octet length prefix. Empty and truncated elements are invalid, and each item can be at most 255 bytes.

That detail matters because interfaces tend to make the values look like forgiving text. They are not category names to trim, lowercase or approximate. The HTTP/2 identifier over TLS is exactly the two octets for h2. RFC 9113 says h2c must not be sent or selected in TLS. A private value that merely resembles a registered identifier does not inherit its meaning.

The client order expresses preference. The server chooses one value it also supports, ordinarily according to its own preference over the intersection. A trace saying “client offered h2 first” does not prove the server selected it. A trace saying “server supports h2 in configuration” does not prove the client offered it or the callback used that configuration.

No overlap is not an invitation to invent agreement. RFC 7301 requires fatal no_application_protocol when the server supports none of the offered values. RFC 9325 reinforces strict handling because accepting an unoffered protocol can enable cross-protocol confusion.

Selection is definitive—and deliberately local

Once selected, the identifier is definitive for application data on that connection. A server must not select h2 and then parse the same connection as HTTP/1.1. HTTP/2 adds a useful confirmation: after TLS negotiation, both endpoints must send their connection prefaces, including SETTINGS. An invalid preface is a connection error.

This creates an evidence ladder rather than a single flag. The ClientHello proves an offer was transmitted. EncryptedExtensions proves the server returned a selection. The authenticated Finished messages bind the handshake transcript. The application preface and parser prove the selected protocol actually began. A request and response prove useful application exchange.

Each rung answers a different question. A capture cut off before Finished cannot claim a completed negotiation. A completed TLS handshake with no HTTP/2 preface cannot claim a working HTTP/2 exchange. One successful request cannot establish future availability on another instance.

RFC 9113 explicitly warns that prior HTTP/2 support is not a strong signal for future connections because configurations, cluster instances and network conditions change. ALPN's connection scope is therefore not an operational inconvenience to be averaged away; it is the correct unit of attribution.

A terminator owns two protocol decisions

A terminating proxy is a TLS server to the downstream client and, when it originates TLS, a TLS client to the upstream service. Those are two connections with different peers, keys, certificates, policy generations, failure modes and ALPN records.

The application protocol may be preserved, upgraded, downgraded or translated. Envoy's official upstream options make the choice visible. A cluster can explicitly use the downstream protocol, independently negotiate HTTP/1.1 or HTTP/2 with the upstream, or fall back to HTTP/1.1 when upstream ALPN is absent. The existence of a “use downstream” policy proves why copying the value is never a safe default: continuity is a configured behavior that must itself run.

Tunnelling is different from termination. With a true byte tunnel, the intermediary may not be a TLS endpoint and may lack the inner selection. RFC 7639 even defines an HTTP header that can state ALPN intentions for a CONNECT tunnel. An intention or header is still not a completed inner handshake. Telemetry must say whether it observed, terminated, forwarded or merely described a connection.

A useful topology record therefore joins, but does not merge, two tuples:

  • downstream connection, client offer, edge selection, TLS result and application confirmation;
  • upstream connection, edge offer, origin selection, TLS result and application confirmation.

The translation event links them. It does not make them one protocol authority.

Configuration is a candidate, callback execution is evidence

OpenSSL exposes the operational path in unusually testable terms. A client configures a length-prefixed list. Setting its length to zero clears the list and sends no ALPN extension. A server installs a selection callback. If ClientHello contains no ALPN offer, that callback is not invoked.

The servername callback runs first and can replace the connection's SSL_CTX; the ALPN callback then runs under the resulting context. A dashboard that inventories one global list can therefore disagree with the virtual host policy that actually selected the protocol.

The selected output must be one value from the client vector. Its storage must remain valid through the handshake. Later, SSL_get0_alpn_selected returns a borrowed, non-NUL-terminated value; callers must preserve the length and must not free the pointer. A log path that treats it as a C string can truncate, over-read or silently normalize the very bytes it is meant to prove.

One helper contains an excellent negative test. SSL_select_next_proto returns the first client item in its output even when there is no overlap, while also returning OPENSSL_NPN_NO_OVERLAP. That output is useful for older NPN behavior, but OpenSSL explicitly says an ALPN callback must ignore it on no overlap. Code that logs the output before checking the result manufactures a negotiation that never occurred.

OpenSSL also distinguishes fatal no-overlap from NOACK, where no protocol is selected—for example because none is configured for that connection. Absence, invalid input, no overlap, local no-selection policy, callback failure and successful selection need separate result codes. Flattening them into alpn=false destroys the failure boundary.

Resumption does not inherit the old selection

RFC 7301 states that ALPN is a property of the connection, not the session. When a session ticket or resumption is used, the earlier extension contents are irrelevant; the new handshake messages govern the new connection.

That rule protects operations from stale capability claims. A ticket issued when h2 was enabled does not force a later connection to select h2. A different edge instance, virtual host policy or rollout generation may offer or select something else. Metrics must bind the selected identifier to the new connection rather than copying it from ticket metadata.

TLS 1.3 early data creates a tighter constraint, not a broader inheritance. The PSK has associated 0-RTT parameters, including ALPN. A server can accept early data only if the newly selected ALPN matches the value associated with that PSK. If early data is rejected and the completed connection selects a different protocol, the application may need to construct different messages.

For that reason, RFC 9846 says the TLS implementation must not automatically resend early data unless the same ALPN is selected. “Replay the bytes after the handshake” is unsafe when the framing, semantics or allowed operations differ. The application owns the decision to rebuild, resend or abandon the work.

Protocol selection does not authenticate the service

ALPN answers which application protocol the endpoints will use. It does not answer whether the server is the intended service. Certificate path construction and service-identity matching answer that question under the applicable TLS profile.

HTTP/3 makes the separation explicit. RFC 9114 normally uses h3 in the TLS handshake over QUIC, but it separately requires the client to validate the certificate for the origin server. A server that selects h3 while presenting an unacceptable certificate is not authoritative for that origin. The selected application protocol is real; the intended service identity is not established.

Nor does identity create application permission. A correctly identified service or client can still be denied a route, tenant, method or resource. ALPN, certificate verification and authorization should appear as three named results, each with its own owner and policy generation.

This separation also prevents inverse errors. A certificate that covers many DNS names does not prove every virtual host enables the same ALPN set. A service allowed to process one route over HTTP/2 does not gain authority over another route. Shared cryptographic or protocol infrastructure does not merge application policy.

Registration is vocabulary, not deployment

The IANA ALPN registry is indispensable because it gives interoperable protocols precise identifier bytes and references. Registration answers “what does this identifier mean?” It does not answer “where is it running?”

The same boundary applies to source code, build features and configuration objects. A library can implement ALPN while a particular connection sends no extension. A server can configure h2 under one SSL_CTX while SNI selects another. An edge can negotiate h2 while an origin only accepts HTTP/1.1. An origin can support HTTP/2 on one instance and not another.

Deployment evidence begins with the running connection: the exact offer, exact selection or named failure, completed handshake, application confirmation, endpoint identities and policy generations. Inventory and registry records explain the result; they do not replace it.

Negative tests for an honest protocol ledger

Start with preference. Offer h2,http/1.1, configure the server to prefer http/1.1, and verify that telemetry records the selected bytes rather than assuming the first client item. Attempt to select a value the client never offered and confirm the handshake aborts.

Exercise absence separately. Send no ALPN extension and verify that the OpenSSL callback never runs. Configure no mutual value and observe fatal no_application_protocol. Then deliberately return NOACK under a documented permissive policy and ensure the result is not labelled no-overlap or success.

Trigger the helper trap: pass disjoint lists to SSL_select_next_proto, confirm it returns a client output alongside OPENSSL_NPN_NO_OVERLAP, and prove that the ALPN path ignores those bytes. Swap SSL_CTX in the servername callback and verify which policy generation the later ALPN callback used.

At the proxy, negotiate h2 downstream and HTTP/1.1 upstream. Preserve both connection IDs, both handshakes and the translation event. Repeat with no upstream ALPN and an explicit HTTP/1.1 fallback. No origin capability field should be derived from the downstream leg.

Resume with a policy change and verify the prior ALPN value is not reused. Offer 0-RTT under a PSK associated with one ALPN, complete the connection with another, reject early data and prove the TLS layer does not automatically resend it.

Finally, let ALPN succeed while certificate identity fails, and let both succeed while application authorization denies the operation. A reliable system will report these as distinct outcomes, not as contradictions to be reconciled by one green status.

Sources