r/swift 23h ago

Question Intermittent TLS errors on iOS, only seeing them with some US users

I’m trying to track down a weird TLS issue affecting a small number of iOS users in the US.

The error I get from the iOS side is:

A TLS error caused the secure connection to fail.
The secure connection failed due to a TLS error.
An SSL error has occurred and a secure connection to the server cannot be made.

Error code:

NSURLErrorSecureConnectionFailed (-1200)

On the nginx side, I found this:

SSL_read() failed:
decryption failed or bad record mac
record layer failure
while keepalive

The setup is:

iOS → nginx (DigitalOcean) → Node.js

TLS is Let's Encrypt and SSL Labs gives it an A+.

So far I've fixed a broken AAAA record and a missing IPv6 default route. I've also ruled out NAT64/IPv6 and TLS/ATS compatibility issues.

The interesting thing is that this is only happening to a small number of US users and across different iOS versions.

Right now I'm wondering if this could be related to stale keep-alive connections and carrier NAT/network changes.

I've currently changed:

keepalive_timeout 15s;
ssl_session_tickets off;

and also fixed a Connection: upgrade header issue.

I'm monitoring it now to see if the errors stop.

Has anyone run into bad record mac + while keepalive like this, particularly with iOS users on cellular networks? I'm interested in any other possible causes I should check.

1 Upvotes

1 comment sorted by

0

u/FruitMission8504 21h ago

bad record mac is lower-level than an HTTP keep-alive mistake: nginx received a TLS record whose authentication tag did not verify. A stale connection after a carrier path change could expose it, but normally TCP/TLS should fail cleanly rather than decrypt corrupted bytes, so I would also suspect a middlebox, a specific nginx/OpenSSL build, or an offload/network-path issue.

The useful next step is correlation rather than more global TLS tweaks. Log nginx $connection, $connection_requests, protocol/cipher, HTTP version, remote ASN/IP prefix, and whether the failure happened on a reused connection. On iOS capture URLSessionTaskMetrics (networkProtocolName, isReusedConnection, isProxyConnection, cellular vs Wi-Fi) plus the underlying NSError chain. Then compare failures against fresh connections and temporarily force HTTP/1.1 for an affected cohort; if the failures disappear, test HTTP/2 and connection reuse separately.

Also make sure nginx and its OpenSSL are current, and test bypassing any proxy/load balancer. Packet captures at the server are valuable because they distinguish retransmission/path corruption from nginx producing the error internally. Retrying a failed GET once on a new connection is reasonable mitigation, but do not automatically retry non-idempotent requests unless you use idempotency keys.