docs(spec): peer-authentication-hardening: option D, SSH-style ed25519 key pairs
Per-host identity without a CA; only public keys move, and the trusted list can ride on the replicated cluster config. Recommended over the phased per-role-keys/PKI plan; decision still deferred. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
20ebef6d28
commit
274eddfb55
1 changed files with 61 additions and 2 deletions
|
|
@ -84,7 +84,8 @@ accident too: pointed at a production cluster, they bypass the apiserver.
|
|||
`cluster.bin`, logs or the pushed config.
|
||||
|
||||
## Open decision: secret type
|
||||
Three candidates. All are verified at join only; afterwards the
|
||||
Four candidates (D added 2026-10-12 from the user's question "or just
|
||||
rsa/ed25519?"). All are verified at join only; afterwards the
|
||||
long-lived connection is the identity (connections are never re-used
|
||||
across roles).
|
||||
|
||||
|
|
@ -113,6 +114,54 @@ Notes:
|
|||
host then still needs its own file, so it shares C's distribution
|
||||
problem without C's revocation.
|
||||
|
||||
### D. Plain key pairs, SSH style (ed25519), no CA
|
||||
Each component has its own key pair, generated on its host; the private
|
||||
key never leaves it. The controller holds a list of trusted public keys,
|
||||
each with its role (and, for a worker, its host name), like SSH's
|
||||
`authorized_keys`. Clients pin the controllers' public keys, like
|
||||
`known_hosts`.
|
||||
|
||||
- **Proof at join:** the controller sends a random challenge; the peer
|
||||
signs (challenge, TLS exporter value, role, host name) with its private
|
||||
key; the controller verifies it against the listed public key and checks
|
||||
that role and host match the entry. The controller signs the client's
|
||||
challenge the same way, so clients verify the controller.
|
||||
- **Compared with the others:**
|
||||
|
||||
| | D. Key pairs (ed25519) |
|
||||
|---|---|
|
||||
| A stolen worker key lets you be | that one host, as a worker (like C) |
|
||||
| Per-host identity | yes |
|
||||
| Revoke one host | delete its line from the trusted list |
|
||||
| Secret material that must move between hosts | **none**: only public keys move, and they are not secret |
|
||||
| Where the trusted list lives | can be in the cluster config (e.g. a `pubkey` per host in `hosts.yaml`, plus apiserver/scheduler/admin keys), so it is replicated to every controller by the existing config path and changed by `xceadmin config push` |
|
||||
| Expiry outages | none (keys don't expire; rotation is add new, remove old) |
|
||||
| Cost | a custom signed challenge (as A/B), key generation per host, collecting public keys |
|
||||
|
||||
- **Bootstrap trust:** controllers need each other's public keys and at
|
||||
least one admin public key before any config is pushed, so those sit in
|
||||
each controller's local startup config (next to `--peers`). Everything
|
||||
else comes with the cluster config, which only an `admin` key can push.
|
||||
- **Collecting public keys** (the distribution question becomes this):
|
||||
config management fetches each host's `.pub` file, or, for elastic
|
||||
hosts, a new host registers its own fresh public key once with a
|
||||
short-lived token (distribution 2, but much simpler than issuing certs:
|
||||
the controller only records a public key, it signs nothing and holds no
|
||||
CA key).
|
||||
- **Algorithm: ed25519 rather than RSA.** 32-byte keys, fast signing and
|
||||
verification, deterministic signatures (no random-number pitfalls), no
|
||||
parameters to choose. Supported by the OpenSSL 3 already linked (EVP),
|
||||
Go `crypto/ed25519`, and Python `cryptography`. RSA (3072+) or ECDSA
|
||||
P-256 only if a site's FIPS-validated crypto module lacks ed25519
|
||||
(FIPS 186-5 approves it, older modules may not), so keep the algorithm
|
||||
name in each trusted-list entry.
|
||||
- **Implementation detail:** TLS with raw public keys (RFC 7250, OpenSSL
|
||||
3.2+) could do this inside TLS, but Python's `ssl` (xceadmin) and Go's
|
||||
`crypto/tls` don't support it, so an application-level signed challenge
|
||||
in the join exchange is the portable choice. Python needs the TLS
|
||||
exporter value: `ssl` doesn't expose it, pyOpenSSL does (new xceadmin
|
||||
dependency).
|
||||
|
||||
## Open decision: key distribution
|
||||
| | 1. Operator copies files | 2. Bootstrap token, then enrolment | 3. xce-node-manager places them |
|
||||
|---|---|---|---|
|
||||
|
|
@ -123,7 +172,17 @@ Notes:
|
|||
| HA | each controller gets the same files | the issuing secret (CA key or root key) must be on every controller to enrol through any of them; it is a file on each controller, never replicated through the WAL | n/a |
|
||||
|
||||
## Recommendation (for decision)
|
||||
Phase it, with the authenticator pluggable like the apiserver auth backends:
|
||||
**Revised 2026-10-12: D (ed25519 key pairs) is the better fit.** It gives
|
||||
C's per-host identity without a CA, certificates or expiry outages; no
|
||||
secret ever moves between hosts; the trusted list rides on the existing
|
||||
replicated cluster config; elastic hosts only need a token flow that
|
||||
registers a public key. Its cost over B is small (signatures instead of
|
||||
HMAC, collecting public keys). Unless that changes, D with public keys
|
||||
collected by config management, plus token registration for elastic
|
||||
hosts, replaces the phased plan below.
|
||||
|
||||
Earlier phased plan (kept for comparison), with the authenticator
|
||||
pluggable like the apiserver auth backends:
|
||||
1. **Now: B (per-role keys) + distribution 1 (files), referenced by the
|
||||
node manager (3).** Smallest change that delivers everything in
|
||||
"Desired Outcome": apiserver-only user requests, the `admin` role,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue