Skip to content

← All posts

Redis over TLS, and what the rediss scheme does not decide

A pasted rediss URL selects encryption without verification, because nothing in a URL says whose certificate to trust; verification stays an explicit choice.

A pasted URL looks like it settles two things: who you authenticate as, and how the transport is protected. It settles the first. The username in the userinfo half becomes the Redis 6 ACL user the driver authenticates as. The extra s in rediss:// turns encryption on and stops there - it does not decide whose certificate the client should accept, and the connection dialog will not pretend otherwise.

The failure this prevents is quiet. You paste rediss:// at a managed endpoint, the connection goes green, and you write down that the transport is verified. It is encrypted. Nothing checked a chain.

What the parser takes out of a Redis URL

The Redis provider does not accept a connection string. getCapabilities() reports supportsConnectionString: false, and RedisProvider reads discrete fields only: host, port defaulting to 6379, user, password and a logical database index. The raw string never reaches it.

The paste still works, because the decomposition happens a layer above. parseGenericURL in src/lib/connection-string-parser.ts recognises both redis:// and rediss://, splits the userinfo into user and password, and fills the host, port and database boxes on the form. What you get back is a filled dialog, not a stored URL.

That layout is why the scheme has to be translated rather than carried. Since the string is dropped, the only way rediss can mean anything downstream is as a field, so the parser returns an sslMode alongside the rest: require for rediss://, disable for redis://. Both arms are set explicitly. A paste overwrites the form rather than merging into it, so pasting a plaintext URL over a previous edit clears a require that was left behind rather than silently keeping it.

Measured through the parser and the provider together on 2026-08-23, against redis:latest --port 0 --tls-port 6390 holding a self-signed certificate:

rediss://localhost:6390 -> sslMode require | connected, PING = [{"result":"PONG"}] | 14ms
redis://localhost:6390  -> sslMode disable | FAILED: Failed to connect to Redis: Connection is closed.

Both arms are the evidence. A wired path and a documented shape look identical until the negative case fails for the right reason.

Encryption from the scheme, verification from you

require maps to { rejectUnauthorized: false }. TLS is negotiated, the traffic is encrypted, and the certificate the server presents is not checked against anything.

That is a deliberate reading of what the scheme says. rediss:// says “TLS” and stops there. It names no certificate authority, no expected host name and no trust store. Meanwhile the ordinary self-hosted deployment - a node started with --tls-port and a self-signed certificate - is exactly what a verifying mode refuses. Had the scheme been mapped to verify-full, the most common Redis over TLS setup in existence would fail on paste, and the failure would arrive as a certificate error at a moment when the person pasting believes they are still filling in a form.

So the rule is stated rather than inferred: a rediss:// URL selects require, not a verifying mode. The paste encrypts and never claims to have checked a chain. Verification is a separate decision you make in the SSL panel, and the URL alone never turns it on.

Worth being precise about the scope. This is a claim about a URL scheme. A boolean TLS parameter in a query string follows a different rule: ?ssl=true describes what a specific driver does rather than announcing a transport, so it is mapped onto the mode that matches the driver’s own behaviour, which is verify-system. Whether the scheme should be brought in line with that rule is an open question on the backlog rather than a settled one.

How the four TLS modes map onto one driver option

buildTLSOptions() turns connection.ssl into the single tls object ioredis hands to Node’s tls.connect, so the material travels under Node’s own names - the same mapping the PostgreSQL, MySQL and Couchbase adapters use.

ssl.mode tls option
absent / disable not present at all
require { rejectUnauthorized: false }
verify-system { rejectUnauthorized: true }, no ca
verify-ca / verify-full { rejectUnauthorized: true }

Three details in that table are load-bearing.

Absent means absent. ioredis negotiates TLS whenever tls is set, an empty object included, so disable cannot be expressed as tls: {}. The key is omitted entirely.

verify-system needs no PEM. It checks the chain against the trust store the runtime already carries, which is what a managed endpoint whose certificate a public root signed actually needs. Pasting a CA file is the exception, not the starting point.

verify-ca and verify-full build the same object. ioredis exposes no separate host-name check, so there is no second thing to switch on. Two names, one behaviour, and saying so is cheaper than letting someone infer a hostname assertion that is not there.

caCert, clientCert and clientKey become ca, cert and key when set, each independently, because a server can demand mutual TLS while presenting a self-signed certificate itself. An explicit ssl.rejectUnauthorized always wins over the mode.

Where the username in a pasted URL ends up

The Username field is the Redis 6 ACL user. connect() sends it as ioredis’s username, and sends nothing at all when it is empty - a plain requirepass server has no ACL user to name, and ioredis authenticates as default only when username is absent. A pasted redis://analytics:pw@host:6379/0 fills the same field, and the scheme makes no difference to which field it fills.

Measured on 2026-08-26 against redis:latest, with default left on nopass ~* &* +@all and a second user defined on >probepw ~* +@all -info:

{host, port, password}            -> ACL WHOAMI = default | INFO succeeds
{host, port, username, password}  -> ACL WHOAMI = probe   | INFO refused: NOPERM

The second arm is the interesting one, and it costs something. ioredis’s own ready check calls INFO; on NOPERM it logs “Skipping the ready check” and connects anyway, which is the behaviour a least-privilege user needs. But getHealth(), getOverview() and getPerformanceMetrics() all read INFO, so for a user without +info they raise the server’s own NOPERM sentence instead of answering with zeros that would look like measurements. POST /api/db/test-connection reports that as a degraded amber result: the connection is real, the health read is not. Grant +info, or expect those panels to stay empty for that user. The SLOWLOG and CLIENT LIST reads degrade to an empty list instead, so a restricted ACL does not break the dashboard outright.

One migration note, because it is the kind of thing that reads as a bug later. connectionFields decides what a save writes, and it omitted user for Redis until the settlement round of 2026-08-27. A connection saved before then never captured the ACL user at all, so it still authenticates as default, and there is nothing to migrate - no migration restores a credential that was never stored. Open it, type the user name, and it persists from that point on.

Hosted Redis services, and what has actually been probed

Redis Cloud, Amazon ElastiCache, Azure Cache for Redis and Upstash are names people run Redis on, and they are the reason the two fields above have to work. They are not evidence that any of them was connected to.

The provider’s committed tests replace ioredis with an in-process mock before the provider is imported. That suite does cover every ssl.mode branch and both ACL username assertions - present for a named user, absent for an empty string and for an unset field - asserted against the options object the Redis constructor actually received. It is a real check of the mapping and it is not a hosted endpoint. docs/providers/README.md records ElastiCache and Upstash under “no instance was reachable”, deliberately absent rather than assumed to work.

None of those four services is a tested target. Neither is Cluster or Sentinel: only a single standalone node is supported. What is published is the mapping from scheme to mode to driver option, measured against a local TLS-only node, and the rule that a rediss:// paste selects require rather than a verifying mode. The boundaries this product publishes about itself are collected on the security page.

If you need the chain checked, pick the mode. The URL will not pick it for you, and it should not.