Mutual TLS between the services¶
With mTLS on, every gRPC hop between IOLake's components runs over mutual TLS, and each side proves it holds a certificate signed by the cluster's CA:
- gateway to meta and to the OSDs;
- OSD to meta;
- meta to meta, including Raft;
- meta to the OSDs (liveness, repair, drain);
- block gateway to meta and to the OSDs;
iolakectlto the block gateway (volume,snapshot).
A peer without such a certificate is refused when it connects: a plaintext
client, a TLS client with no certificate, or one with a certificate from
another CA. The node that refused it logs a warning naming the peer's
address (gRPC: refused <address>: TLS handshake: …). A peer that
connects and never completes the handshake is disconnected 10 s after it
connected, on every gRPC port and on meta's admin port, so it holds
neither a connection nor anything else on the node. The other way round, a
node connecting to another gives up when its connect timeout is out (5 s,
less for the probes), the TLS handshake included: a peer that takes the
connection and then answers nothing, such as a frozen process, fails the
attempt rather than holding it.
Each certificate also names its role, and each call is checked
against it: an OSD's certificate can't read a user's secret key from meta,
a gateway's can't purge an OSD. Meta's admin port (Raft bootstrap and
membership) is HTTPS with the same certificates, and changes membership
only for the admin role.
TLS for clients (S3, the console, Iceberg) is a separate matter: it's terminated at the ingress or load balancer.
Without mTLS¶
Without mTLS nothing between the services is authenticated. Anyone who can reach meta's gRPC port (9100) can read every user's secret key and the key STS and console sessions are signed with; anyone who reaches an OSD (9200) can write, overwrite and delete shards; anyone who reaches meta's admin port (9102) can add a Raft member and be sent all the metadata. Each node says so in its log when it starts.
That is fine for development and for the all-in-one on a laptop, which keeps its internal ports on loopback. Anywhere else, turn mTLS on, or run on a network that only IOLake's own nodes can reach (on Kubernetes the chart's network policies, on by default; elsewhere a firewall) and keep it that way. The ports are in the installation guide.
Roles¶
A certificate names its role in a SAN URI, spiffe://objectio/<role>:
| Role | Presented by | May call |
|---|---|---|
meta |
meta nodes | everything on meta (a follower forwards each call to the leader under its own certificate, after checking the caller's); Raft; everything on the OSDs: repair, recovery, drain, placement groups, scrub, purge |
osd |
OSDs | on meta: registering, its placement groups, reporting its version, the liveness probe |
gateway |
S3 gateways | on meta: everything but registering OSDs and the block gateway's volumes, users' secret keys and the STS signing key included; on the OSDs: reading and writing shards and object metadata, deleting shards, listings, status |
block-gateway |
block gateways | on meta: its volumes and chunk maps, where to place them, reporting its version; on the OSDs: writing, reading and deleting shards |
admin |
operators: iolakectl's block commands, the Helm Raft bootstrap Job, iolake-install cluster init |
the block gateway's gRPC (volumes, snapshots); changing Raft membership on meta's admin port |
A call no role of the caller's is allowed, or one the server doesn't
know, is refused with PERMISSION_DENIED. A process that is several
components presents one certificate naming several roles: the
all-in-one's names all five. A certificate naming no role is refused
everything (but see upgrading).
Turning it on¶
Every component takes the same three settings, as flags or environment variables:
| Flag | Environment | What |
|---|---|---|
--tls-cert |
IOLAKE_TLS_CERT |
This node's certificate (PEM), naming its role. It is presented both as a server and as a client. |
--tls-key |
IOLAKE_TLS_KEY |
Its private key (PEM) |
--tls-ca |
IOLAKE_TLS_CA |
The CA (PEM) that signs every node's certificate. It is the only CA trusted. |
The three go together. iolake-aio takes them too: its own internal
hops then use mTLS, and meta's admin port inside it is HTTPS.
A server's certificate must name every address peers dial it by: the
addresses registered as --advertise-addr (OSDs), --raft-advertise
(meta) and the --meta-endpoint targets. Names and IPs both work. Give
each role its own certificate, naming only that role's addresses, so one
role's certificate can't stand in for another's, as a client or as a
server.
Enable it at install time. A cluster can't run half on TLS and half off, so turning it on, or off, later means restarting every component at once.
Issuing the certificates yourself¶
With a CA of your own (ca.pem, ca.key), one certificate per role; the
OSDs', for example:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout osd.key -subj "/CN=iolake-osd" -out osd.csr
openssl x509 -req -in osd.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-days 365 -out osd.pem -extfile <(printf '%s\n' \
"subjectAltName=URI:spiffe://objectio/osd,DNS:osd1.example.internal,DNS:osd2.example.internal,IP:127.0.0.1" \
"extendedKeyUsage=serverAuth,clientAuth")
The admin certificate needs only its URI: it is only ever a client's.
Meta's admin port¶
Under mTLS meta's admin port serves HTTPS with the meta node's certificate. A client certificate is optional when connecting, and checked for each call:
| Call | Takes |
|---|---|
GET /status, /health, /ready |
any client, with a certificate or none: they only read the node's state, and kubelet's probes present no certificate |
POST /init, /add-learner, /change-membership, and any other call |
only a client certificate of role admin; anything else is answered 403 |
curl --cacert ca.pem --cert admin.pem --key admin.key \
-X POST https://meta1:9102/add-learner -H 'content-type: application/json' \
-d '{"node_id": 2, "addr": "meta2:9100"}'
/status stays open because it holds nothing a peer can't learn
otherwise (node ids, their Raft addresses, the term and log index), and
operators and their scripts read it without an admin certificate.
The port binds the host meta's gRPC listens on (--listen), or
--admin-host. iolake-install cluster init forms the group over it
with --tls-cert, --tls-key and --tls-ca, an admin certificate
(Linux); the chart's bootstrap Job with the
admin Secret, and meta's probes ask over HTTPS (scheme: HTTPS).
Kubernetes¶
The chart requests a certificate per role from cert-manager, each with its
role's URI and the names of that role's pods and services, mounts each
component's own, and sets the variables above. The Raft bootstrap Job
gets the admin one.
tls:
enabled: true
# Either name a cert-manager CA issuer, and the chart requests the
# certificates (Secrets <release>-iolake-tls-<role>):
certManager:
issuerRef: {name: iolake-ca, kind: Issuer}
# or create the Secrets yourself (tls.crt, tls.key and ca.crt each),
# one per role, and name them:
# secrets:
# meta: my-meta-tls
# osd: my-osd-tls
# gateway: my-gateway-tls
# block-gateway: my-block-tls
# admin: my-admin-tls
A CA issuer is what you want here: ca.crt is then the CA that signs
each certificate. A minimal one, with cert-manager installed:
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: iolake-selfsigned}
spec: {selfSigned: {}}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: iolake-ca}
spec:
isCA: true
commonName: iolake-ca
secretName: iolake-ca
issuerRef: {name: iolake-selfsigned, kind: Issuer}
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: iolake-ca}
spec: {ca: {secretName: iolake-ca}}
Upgrading from a release whose certificates name no role¶
A release before roles used one certificate, naming none, for every
component. During the rolling upgrade, while the cluster's format level
is still the previous release's, a certificate naming no role is taken
for any role, so the nodes not yet upgraded keep working. Once the
upgrade is finalized (iolakectl upgrade finalize), such a certificate is
refused everything. The chart's upgrade issues the per-role certificates
and rolls each component onto its own; with certificates of your own,
reissue them with roles before finalizing. Then delete the old shared
certificate and its Secret.
Renewal¶
Components read their certificate when they start. cert-manager renews the Secrets 30 days before the certificates expire, and the chart's default lifetime is one year. After a renewal, restart the components one at a time so they pick up the new certificates:
kubectl rollout restart statefulset/<release>-iolake-meta statefulset/<release>-iolake-osd \
deployment/<release>-iolake-gateway
Picking up a renewed certificate without a restart is planned.
How it's tested¶
The end-to-end suite runs with mTLS on: every node of every test cluster gets a test CA's certificate for its role. Tests check that:
- a node refuses a plaintext client, a client with no certificate and one with another CA's certificate, each within a bound, and serves the cluster's own;
- meta and an OSD disconnect a client that connects and never starts a TLS handshake once the handshake timeout is out;
- meta refuses the STS signing key and a user's secret key to every role but a gateway's, and to a certificate naming none, and an OSD refuses placement-group calls and purges from anyone but meta;
- meta's admin port refuses plain HTTP, and
/initand membership changes from a client with no certificate or another role's, while/status,/healthand/readyanswer a client with no certificate; iolake-install cluster initforms nothing without a certificate or with another role's, and forms the group with anadminone;- the all-in-one, serving S3 on every interface, keeps every other port on loopback.
Unit tests check that every method of every service has a rule, that a
certificate naming no role is any role only before the upgrade is
finalized, and that on the admin port only reading the node's state is
open to a client without an admin certificate. Others check that a gRPC
server serves only a peer with a certificate the CA signed and hands that
certificate to the call, that a peer silent or stopped part way through
its handshake is disconnected while others are served, and that
connecting to a server that never answers the handshake fails at the
connect timeout. The rolling-upgrade test
runs plain. IOLAKE_E2E_PLAIN=1 runs the whole suite plain; the mTLS
tests then fail, since they have nothing to test.