Skip to content

test: qualify TLS on the S3 listener, and pin a configuration hazard - #12

Merged
joey-huckabee merged 1 commit into
mainfrom
feature/s3-tls
Sep 16, 2026
Merged

joey-huckabee merged 1 commit into
mainfrom
feature/s3-tls

Conversation

@joey-huckabee

Copy link
Copy Markdown
Contributor

Summary

Closes the last gap in the client-facing security story. Until now every suite
drove the S3 API over plain HTTP, so nothing showed the image could terminate TLS
at all.

tests/s3-tls.sh     5 passed, 0 failed  (TLS)
                    2 passed, 0 failed  (the hazard)
tests/s3.sh        25 passed, 0 failed  (no regression)

New: tests/s3-tls.sh, tests/lib/tlschecks.py; S3Client gained HTTPS with a
configurable trust anchor.

The check that makes the others mean something

A client trusting only an unrelated CA is refused at the handshake. Without
that, a successful round trip proves only that bytes moved — a client trusting
everything connects to anything. A hostname the certificate does not cover is
also refused, so the certificate needs a matching subjectAltName; a common name
alone is ignored by modern clients.

Testing that honestly meant the client had to ask for the name on the
certificate
, not 127.0.0.1 with verification off. The suite resolves the
certificate's hostname to the published loopback port rather than sidestepping
the check it exists to make.

Supplying a certificate and key with no -port.https upgrades the listener:
the same port stops answering plain HTTP.

The hazard

Adding -port.https does not move TLS to a second port. It starts TLS
there and leaves the original port serving the S3 API in the clear.

With -cert.file, -key.file and -port.https=8334, the container listens on
8333, 8334 and 18333 — and 8333 really does serve the API, proven by
driving it rather than inferred from an open socket. An operator reaching for the
flag to "enable HTTPS" publishes both.

I have not added a guard for this. It is a genuine footgun and a legitimate
migration shape, and this project has a pattern for that tension — refuse by
default with an opt-out. Choosing to apply it is a decision I have recorded for
you rather than made unprompted; it is
decision 2.

Validation

  • I ran the pinned local checks (pre-commit run --all-files). Clean.
  • I reviewed logs, warnings, annotations, skipped steps, and retained
    evidence rather than relying only on green status checks. I also re-ran
    tests/s3.sh after changing the shared client, to confirm the HTTPS support
    did not disturb the plain-HTTP path.
  • I added or updated tests for behavior changes, including the negative case
    where a guard is supposed to refuse. Three of the five TLS checks are
    negative: wrong CA, wrong hostname, and plaintext on the TLS port.
  • I updated user and operator documentation where needed.
  • I recorded notable completed work in CHANGELOG.md, checked off the package
    4 item, and added the open decision.
  • I did not weaken a security or release control without documenting the
    threat, rationale, compensating control, owner, and expiry.

Security and release impact

Test and documentation only; no image or runtime change. docs/TLS.md now carries
a working TLS configuration and a warning box for the hazard.

Accepted findings: the -port.https combination, documented and left unguarded
pending your decision.

One change to the shared client worth noting: a refused TLS handshake now returns
status 0 with the reason in the body instead of raising. A handshake refusal is
a result these checks care about, not a crash — but it means callers must treat
0 as "no HTTP response", which the checks do explicitly.

Still open in package 4

The Iceberg round trip against lakekeeper-ubi, mTLS rejection behaviour,
certificate rotation, HTTPS on the internal listeners, replication and durability,
backup and restore, and the filer backend decision.

Upstream verification

Verified against SeaweedFS 4.46 in weed/command/s3.go: -cert.file with
-port.https == 0 serves the main port over TLS and does not start a plaintext
listener, while a non-zero -port.https starts TLS there and leaves the plain
port running. Also noted for later: -iam defaults to true, putting an embedded
IAM API on the same port as the S3 API.

Every other suite in this repository drives the S3 API over plain HTTP, so
nothing had shown that the image can terminate TLS at all, let alone that a
client verifying a private CA gets what it asked for.

The check that matters is the negative one. A client trusting only an unrelated
CA is refused at the handshake, which is what makes the successful round trip
mean something: a connection that succeeds for a client trusting everything says
nothing about the server's identity. A hostname the certificate does not cover
is also refused, so the certificate needs a matching subjectAltName rather than
a common name, which modern clients ignore.

To test that honestly the client has to ask for the name on the certificate
rather than for 127.0.0.1 with verification turned off, so the suite resolves
the certificate's hostname to the published loopback port instead of
sidestepping the check it exists to make.

Supplying a certificate and key with no -port.https upgrades the listener: the
same port stops answering plain HTTP.

Adding -port.https does something else entirely, and this is the finding worth
carrying forward. It does not move TLS to a second port. It starts TLS there and
leaves the original port serving the S3 API in the clear. With a certificate, a
key and -port.https=8334 the container listens on 8333, 8334 and 18333, and the
plaintext port really does serve the API -- proven by driving it, not inferred
from an open socket. An operator reaching for the flag to enable HTTPS ends up
publishing both.

Whether the entrypoint should refuse that combination is left as a decision for
a human rather than settled here. It is a real footgun, and it is also a
legitimate migration shape, and this project has a pattern for that tension:
refuse by default with an opt-out. Choosing it is not mine to do unprompted.
@joey-huckabee
joey-huckabee merged commit 555b19e into main Sep 16, 2026
@joey-huckabee
joey-huckabee deleted the feature/s3-tls branch September 16, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant