← S3 compatibility matrix S3 resource guide · Encryption

We never ask for your encryption key. Here’s why that’s the stronger position.

Every object is split into shards, and every shard is XChaCha20-encrypted before it leaves the gateway. Not a setting. Not a header. Not something you can forget to switch on. Because that happens on every upload without exception, there is nothing for a customer-supplied key to add — so we don’t take one. If you want to hold the key yourself, encrypt before you upload; we store the ciphertext and never look inside.

TLS in transit — always on Per-shard XChaCha20 — always on Your key stays yours — we never receive one


What this area is

S3 encryption has three layers: transit, server-managed, and customer-managed.

TLS encrypts the connection between a client and the endpoint — standard HTTPS, nothing S3-specific. SSE-S3 (x-amz-server-side-encryption: AES256) tells the server to encrypt the object at rest under a key the server manages entirely; the client never sees a key. SSE-KMS is the same idea with the key held in an external key-management service instead of the storage engine itself, adding audit trails and customer-controlled key rotation. SSE-C flips custody: the client supplies its own AES-256 key on every request header, and the server never persists it — lose the key, lose the object. AWS documents all four; a gateway can implement any subset and must say clearly which.

The four layers, at a glance

TLS
Connection encryption. Standard HTTPS.
SSE-S3
Server-managed key, at rest.
SSE-KMS
External key-management service, at rest.
SSE-C
Client-supplied key, server never persists it.


How XNS implements it

Every shard is encrypted before it leaves the gateway — independent of any SSE header.

XNS encrypts every object shard with XChaCha20 at the storage layer (Per-Shard Encryption, L2) before erasure-coding it 80 data + 40 parity across up to 120 independent hosts. This runs unconditionally — it does not depend on the client sending x-amz-server-side-encryption at all. No single host ever holds a reconstructable object, encrypted or not.

The S3-facing SSE configuration API is a real, DB-backed handler — encryption_handlers.go (187 lines) — not a stub. PutBucketEncryption / GetBucketEncryption / DeleteBucketEncryption persist a real ServerSideEncryptionConfiguration, and PutObject / CreateMultipartUpload accept the SSE-S3 header and succeed.

The boundary, stated plainly

The SSE-S3 API round-trips: configuration persists and uploads carrying the SSE-S3 header succeed. Wiring that stored configuration to change storage-path behavior is under active development — the shard layer is already always-on XChaCha20-encrypted regardless of which SSE header, if any, a client sends. Do not read a 200 on PutObject with an SSE-S3 header as evidence the header changed what happened at the storage layer; the always-on shard encryption is what's actually protecting the data today.

SSE-C is out of model by design. SSE-C asks you to put your AES-256 key in a header on every single request — every upload, every download, every part of every multipart. The key crosses the wire constantly and sits in the server's memory while it works. We don't want that responsibility and you shouldn't want to hand it over. Every shard is already encrypted on the way out, so a key you mail us adds nothing we aren't doing. Uploads carrying SSE-C headers are refused at the boundary with 501 NotImplemented rather than quietly ignored.

SSE-KMS is out of model by design — XNS has no external key-management-service dependency, and the handler rejects aws:kms at the API boundary rather than accepting a configuration it cannot honor.

PutBucketEncryption / GetBucketEncryption / DeleteBucketEncryption
Real, persisted. Rejects aws:kms and any non-AES256 algorithm at the API boundary (errKMSNotSupported, errInvalidSSEAlgorithm, in the gateway's encryption-handler code).
PutObject / CreateMultipartUpload with SSE-S3 header
Accepted; upload succeeds. Storage-path wiring to that config value is in progress — see the boundary note.
PutObject / CreateMultipartUpload with SSE-C headers
Refused at the boundary, not silently ignored: 501 NotImplemented — “SSE-C (customer-provided keys) is not supported by this gateway. Data is encrypted natively at the storage layer.”
Client-side encryption (you encrypt, then upload)
Fully supported, and nothing to configure. We store the bytes you send and never inspect them. Your key never reaches our wire or our memory — stricter custody than SSE-C offers.
PutBucketEncryption with SSEAlgorithm=aws:kms
Rejected: 501 NotImplemented — “KMS-managed keys are not implemented by this gateway. Use AES256.”
Per-shard XChaCha20 (storage layer, L2)
Always on. Not an S3-visible operation — no header controls it, no header disables it.
TLS (transport)
Always supported; standard HTTPS termination at the gateway.


How we conform to the S3 protocol

Measured against the public ceph/s3-tests suite — and published.

Encryption results are read directly off the ceph/s3-tests per-test ledger for the baseline-53dedd8d / fullsuite-53dedd8d run, build 53dedd8d, 2026-08-11 — the same run behind the site-wide conformance numbers on the compatibility matrix. We publish the full per-test result, including what still fails, there rather than summarizing it here.

Where we deliberately differ. Two of the four AWS encryption modes are out of model, on purpose, and we'd rather say so than fudge it. SSE-C requires you to send us your key on every request; we encrypt every shard anyway, so the key would buy you nothing and cost you custody. SSE-KMS requires an external key-management service; XNS has no such dependency and won't invent one. Neither is a silent gap — both are refused cleanly with 501 NotImplemented at the API boundary, and every SSE-S3 and TLS test passes.

Full per-test evidence, including the SSE-C and SSE-KMS node-ids, is on the compatibility matrix, alongside every measured figure and its provenance.


How we compare

Same rows as the matrix — AWS S3, Ceph, MinIO, Wasabi, B2, Storj.

Competitor figures from each vendor’s official documentation, researched 2026-06-14. Reused verbatim from the S3 compatibility matrix.

FeatureXNSAWS S3Ceph RadosGWMinIOWasabiB2Storj DCS
SSE-S3 (server-managed keys)◐ round-trips; at-rest wiring under review– always end-to-end encrypted
SSE-KMS (external key management)– out of model, no external KMS dependency
SSE-C (customer-provided keys)– out of model; every shard is encrypted without your key◐ opt-in; disabled by default Apr 2026◐ Gateway-ST client keys approximate
Bucket default encryption
TLS in transit


How applications use it

Set bucket encryption, then upload with the SSE-S3 header.

boto3

# 1. Point at your gateway, not AWS
import boto3
s3 = boto3.client(
    "s3",
    endpoint_url="https://your-gateway:9000",
    aws_access_key_id="AKIA...",
    aws_secret_access_key="...",
)
# 2. Set bucket default encryption (SSE-S3, AES256) — persists, real config
s3.put_bucket_encryption(
    Bucket="my-bucket",
    ServerSideEncryptionConfiguration={
        "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
    },
)
# 3. Confirm it round-trips
cfg = s3.get_bucket_encryption(Bucket="my-bucket")
print(cfg["ServerSideEncryptionConfiguration"])
# 4. Upload with the SSE-S3 header — accepted; the shard layer is
#    already always-on XChaCha20-encrypted independent of this header
s3.put_object(
    Bucket="my-bucket",
    Key="report.pdf",
    Body=open("report.pdf", "rb"),
    ServerSideEncryption="AES256",
)
# 5. SSE-C is out of model — we never take your key. This raises NotImplemented.
#    Want to hold the key yourself? Encrypt the bytes before you upload.
# s3.put_object(Bucket="my-bucket", Key="x", Body=b"...",
#     SSECustomerAlgorithm="AES256", SSECustomerKey=key)
# botocore.exceptions.ClientError: NotImplemented —
# "SSE-C (customer-provided keys) is not supported by this gateway."

AWS CLI

aws s3api put-bucket-encryption \
  --endpoint-url https://your-gateway:9000 \
  --bucket my-bucket \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
aws s3api put-object \
  --endpoint-url https://your-gateway:9000 \
  --bucket my-bucket --key report.pdf --body ./report.pdf \
  --server-side-encryption AES256


Use cases

Where this matters in practice.

  • Data-in-transit for regulated workloads
  • Portable SSE-S3 tooling
  • Storage that's encrypted regardless of client behavior
Data-in-transit for regulated workloads

TLS termination on every S3 call, unconditionally — no configuration required, no opt-out.

Portable SSE-S3 tooling

Backup/IaC tools that call PutBucketEncryption as a matter of course keep working against the gateway without erroring out, because the config API is real, not a stub.

Storage that's encrypted regardless of client behavior

Per-shard XChaCha20 protects data even against clients that never set an SSE header at all — the common case for many S3 SDKs by default.


Applications that lean on it heavily

Real software, named.

  • Terraform — aws_s3_bucket_server_side_encryption_configuration
  • restic / rclone / kopia
Terraform — aws_s3_bucket_server_side_encryption_configuration

Applies a real PutBucketEncryption call; the resource creates and reads back cleanly against XNS today.

restic / rclone / kopia

Object storage backends that upload over plain SigV4 PUT; unaffected by SSE header choice since shard encryption is always on underneath.


FAQ

Encryption questions, answered directly.

Yes, at the storage layer, unconditionally: every object shard is encrypted with XChaCha20 before erasure-coding and distribution. This does not depend on any SSE header sent by the client.

The configuration API round-trips and the upload succeeds, but wiring that stored configuration to change storage-path behavior is under active development. The always-on per-shard XChaCha20 encryption is what is actually protecting the object today, independent of the SSE-S3 setting.

No, and that is a decision rather than a gap. SSE-C asks you to send your AES-256 key in a header on every request — every upload, every download, every multipart part — so the key crosses the wire constantly and sits in server memory while the work happens. We already encrypt every shard with XChaCha20 before it leaves the gateway, on every upload, with no header required and no way to switch it off, so your key would add nothing and cost you custody. Calls carrying SSE-C headers are refused cleanly with 501 NotImplemented rather than quietly ignored. If you want sole custody of a key, encrypt before you upload: we store the ciphertext, never inspect it, and your key never touches our infrastructure at all.

No, and it is not planned. XNS has no external key-management-service dependency by design; PutBucketEncryption rejects an aws:kms algorithm at the API boundary with 501 NotImplemented rather than accepting a configuration it cannot honor.

Yes. TLS in transit is always supported and is not conditional on any bucket or object configuration.



See the full conformance picture.

The encryption rows above are one section of the full S3 compatibility matrix — object operations, versioning, lifecycle, access control, replication, and integration all get the same treatment.


Claims on this page last verified
© Copyright - SCP, Corp | Xa Net Services and Affiliates