etcd supports automatic TLS as well as authentication through client certificates for both clients to server as well as peer (server to server / cluster) communication.
To get up and running, first have a CA certificate and a signed key pair for one member. It is recommended to create and sign a new key pair for every member in a cluster.
For convenience, the cfssl tool provides an easy interface to certificate generation, and we provide an example using the tool here. Alternatively, try this guide to generating self-signed key pairs.
etcd takes several certificate related configuration options, either through command-line flags or environment variables:
Client-to-server communication:
--cert-file=<path>
: Certificate used for SSL/TLS connections to etcd. When this option is set, advertise-client-urls can use the HTTPS schema.
--key-file=<path>
: Key for the certificate. Must be unencrypted.
--client-cert-auth
: When this is set etcd will check all incoming HTTPS requests for a client certificate signed by the trusted CA, requests that don't supply a valid client certificate will fail. If authentication is enabled, the certificate provides credentials for the user name given by the Common Name field.
--trusted-ca-file=<path>
: Trusted certificate authority.
--auto-tls
: Use automatically generated self-signed certificates for TLS connections with clients.
Peer (server-to-server / cluster) communication:
The peer options work the same way as the client-to-server options:
--peer-cert-file=<path>
: Certificate used for SSL/TLS connections between peers. This will be used both for listening on the peer address as well as sending requests to other peers.
--peer-key-file=<path>
: Key for the certificate. Must be unencrypted.
--peer-client-cert-auth
: When set, etcd will check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA.
--peer-trusted-ca-file=<path>
: Trusted certificate authority.
--peer-auto-tls
: Use automatically generated self-signed certificates for TLS connections between peers.
If either a client-to-server or peer certificate is supplied the key must also be set. All of these configuration options are also available through the environment variables, ETCD_CA_FILE
, ETCD_PEER_CA_FILE
and so on.
For this, have a CA certificate (ca.crt
) and signed key pair (server.crt
, server.key
) ready.
Let us configure etcd to provide simple HTTPS transport security step by step:
$ etcd --name infra0 --data-dir infra0 \
--cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls=https://127.0.0.1:2379 --listen-client-urls=https://127.0.0.1:2379
This should start up fine and it will be possible to test the configuration by speaking HTTPS to etcd:
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
The command should show that the handshake succeed. Since we use self-signed certificates with our own certificate authority, the CA must be passed to curl using the --cacert
option. Another possibility would be to add the CA certificate to the system's trusted certificates directory (usually in /etc/pki/tls/certs
or /etc/ssl/certs
).
OSX 10.9+ Users: curl 7.30.0 on OSX 10.9+ doesn't understand certificates passed in on the command line.
Instead, import the dummy ca.crt directly into the keychain or add the -k
flag to curl to ignore errors.
To test without the -k
flag, run open ./fixtures/ca/ca.crt
and follow the prompts.
Please remove this certificate after testing!
If there is a workaround, let us know.
For now we've given the etcd client the ability to verify the server identity and provide transport security. We can however also use client certificates to prevent unauthorized access to etcd.
The clients will provide their certificates to the server and the server will check whether the cert is signed by the supplied CA and decide whether to serve the request.
The same files mentioned in the first example are needed for this, as well as a key pair for the client (client.crt
, client.key
) signed by the same certificate authority.
$ etcd --name infra0 --data-dir infra0 \
--client-cert-auth --trusted-ca-file=/path/to/ca.crt --cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls https://127.0.0.1:2379 --listen-client-urls https://127.0.0.1:2379
Now try the same request as above to this server:
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
The request should be rejected by the server:
...
routines:SSL3_READ_BYTES:sslv3 alert bad certificate
...
To make it succeed, we need to give the CA signed client certificate to the server:
$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
-L https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
The output should include:
...
SSLv3, TLS handshake, CERT verify (15):
...
TLS handshake, Finished (20)
And also the response from the server:
{
"action": "set",
"node": {
"createdIndex": 12,
"key": "/foo",
"modifiedIndex": 12,
"value": "bar"
}
}
etcd supports the same model as above for peer communication, that means the communication between etcd members in a cluster.
Assuming we have our ca.crt
and two members with their own keypairs (member1.crt
& member1.key
, member2.crt
& member2.key
) signed by this CA, we launch etcd as follows:
DISCOVERY_URL=... # from https://discovery.etcd.io/new
# member1
$ etcd --name infra1 --data-dir infra1 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member1.crt --peer-key-file=/path/to/member1.key \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL}
# member2
$ etcd --name infra2 --data-dir infra2 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member2.crt --peer-key-file=/path/to/member2.key \
--initial-advertise-peer-urls=https://10.0.1.11:2380 --listen-peer-urls=https://10.0.1.11:2380 \
--discovery ${DISCOVERY_URL}
The etcd members will form a cluster and all communication between members in the cluster will be encrypted and authenticated using the client certificates. The output of etcd will show that the addresses it connects to use HTTPS.
For cases where communication encryption, but not authentication, is needed, etcd supports encrypting its messages with automatically generated self-signed certificates. This simplifies deployment because there is no need for managing certificates and keys outside of etcd.
Configure etcd to use self-signed certificates for client and peer connections with the flags --auto-tls
and --peer-auto-tls
:
DISCOVERY_URL=... # from https://discovery.etcd.io/new
# member1
$ etcd --name infra1 --data-dir infra1 \
--auto-tls --peer-auto-tls \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL}
# member2
$ etcd --name infra2 --data-dir infra2 \
--auto-tls --peer-auto-tls \
--initial-advertise-peer-urls=https://10.0.1.11:2380 --listen-peer-urls=https://10.0.1.11:2380 \
--discovery ${DISCOVERY_URL}
Self-signed certificates do not authenticate identity so curl will return an error:
curl: (60) SSL certificate problem: Invalid certificate chain
To disable certificate chain checking, invoke curl with the -k
flag:
$ curl -k https://127.0.0.1:2379/v2/keys/foo -Xput -d value=bar -v
Since v3.1.0 (except v3.2.9), discovery SRV bootstrapping authenticates ServerName
with a root domain name from --discovery-srv
flag. This is to avoid man-in-the-middle cert attacks, by requiring a certificate to have matching root domain name in its Subject Alternative Name (SAN) field. For instance, etcd --discovery-srv=etcd.local
will only authenticate peers/clients when the provided certs have root domain etcd.local
as an entry in Subject Alternative Name (SAN) field
etcd proxy terminates the TLS from its client if the connection is secure, and uses proxy's own key/cert specified in --peer-key-file
and --peer-cert-file
to communicate with etcd members.
The proxy communicates with etcd members through both the --advertise-client-urls
and --advertise-peer-urls
of a given member. It forwards client requests to etcd members’ advertised client urls, and it syncs the initial cluster configuration through etcd members’ advertised peer urls.
When client authentication is enabled for an etcd member, the administrator must ensure that the peer certificate specified in the proxy's --peer-cert-file
option is valid for that authentication. The proxy's peer certificate must also be valid for peer authentication if peer authentication is enabled.
Since v3.2.0, TLS certificates get reloaded on every client connection. This is useful when replacing expiry certs without stopping etcd servers; it can be done by overwriting old certs with new ones. Refreshing certs for every connection should not have too much overhead, but can be improved in the future, with caching layer. Example tests can be found here.
Since v3.2.0, server denies incoming peer certs with wrong IP SAN
. For instance, if peer cert contains any IP addresses in Subject Alternative Name (SAN) field, server authenticates a peer only when the remote IP address matches one of those IP addresses. This is to prevent unauthorized endpoints from joining the cluster. For example, peer B's CSR (with cfssl
) is:
{
"CN": "etcd peer",
"hosts": [
"*.example.default.svc",
"*.example.default.svc.cluster.local",
"10.138.0.27"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco"
}
]
}
when peer B's actual IP address is 10.138.0.2
, not 10.138.0.27
. When peer B tries to join the cluster, peer A will reject B with the error x509: certificate is valid for 10.138.0.27, not 10.138.0.2
, because B's remote IP address does not match the one in Subject Alternative Name (SAN) field.
Since v3.2.0, server resolves TLS DNSNames
when checking SAN
. For instance, if peer cert contains only DNS names (no IP addresses) in Subject Alternative Name (SAN) field, server authenticates a peer only when forward-lookups (dig b.com
) on those DNS names have matching IP with the remote IP address. For example, peer B's CSR (with cfssl
) is:
{
"CN": "etcd peer",
"hosts": [
"b.com"
],
when peer B's remote IP address is 10.138.0.2
. When peer B tries to join the cluster, peer A looks up the incoming host b.com
to get the list of IP addresses (e.g. dig b.com
). And rejects B if the list does not contain the IP 10.138.0.2
, with the error tls: 10.138.0.2 does not match any of DNSNames ["b.com"]
.
Since v3.2.2, server accepts connections if IP matches, without checking DNS entries. For instance, if peer cert contains IP addresses and DNS names in Subject Alternative Name (SAN) field, and the remote IP address matches one of those IP addresses, server just accepts connection without further checking the DNS names. For example, peer B's CSR (with cfssl
) is:
{
"CN": "etcd peer",
"hosts": [
"invalid.domain",
"10.138.0.2"
],
when peer B's remote IP address is 10.138.0.2
and invalid.domain
is a invalid host. When peer B tries to join the cluster, peer A successfully authenticates B, since Subject Alternative Name (SAN) field has a valid matching IP address. See issue#8206 for more detail.
Since v3.2.5, server supports reverse-lookup on wildcard DNS SAN
. For instance, if peer cert contains only DNS names (no IP addresses) in Subject Alternative Name (SAN) field, server first reverse-lookups the remote IP address to get a list of names mapping to that address (e.g. nslookup IPADDR
). Then accepts the connection if those names have a matching name with peer cert's DNS names (either by exact or wildcard match). If none is matched, server forward-lookups each DNS entry in peer cert (e.g. look up example.default.svc
when the entry is *.example.default.svc
), and accepts connection only when the host's resolved addresses have the matching IP address with the peer's remote IP address. For example, peer B's CSR (with cfssl
) is:
{
"CN": "etcd peer",
"hosts": [
"*.example.default.svc",
"*.example.default.svc.cluster.local"
],
when peer B's remote IP address is 10.138.0.2
. When peer B tries to join the cluster, peer A reverse-lookup the IP 10.138.0.2
to get the list of host names. And either exact or wildcard match the host names with peer B's cert DNS names in Subject Alternative Name (SAN) field. If none of reverse/forward lookups worked, it returns an error "tls: "10.138.0.2" does not match any of DNSNames ["*.example.default.svc","*.example.default.svc.cluster.local"]
. See issue#8268 for more detail.
v3.3.0 adds etcd --peer-cert-allowed-cn
flag to support CN(Common Name)-based auth for inter-peer connections. Kubernetes TLS bootstrapping involves generating dynamic certificates for etcd members and other system components (e.g. API server, kubelet, etc.). Maintaining different CAs for each component provides tighter access control to etcd cluster but often tedious. When --peer-cert-allowed-cn
flag is specified, node can only join with matching common name even with shared CAs. For example, each member in 3-node cluster is set up with CSRs (with cfssl
) as below:
{
"CN": "etcd.local",
"hosts": [
"m1.etcd.local",
"127.0.0.1",
"localhost"
],
{
"CN": "etcd.local",
"hosts": [
"m2.etcd.local",
"127.0.0.1",
"localhost"
],
{
"CN": "etcd.local",
"hosts": [
"m3.etcd.local",
"127.0.0.1",
"localhost"
],
Then only peers with matching common names will be authenticated if --peer-cert-allowed-cn etcd.local
is given. And nodes with different CNs in CSRs or different --peer-cert-allowed-cn
will be rejected:
$ etcd --peer-cert-allowed-cn m1.etcd.local
I | embed: rejected connection from "127.0.0.1:48044" (error "CommonName authentication failed", ServerName "m1.etcd.local")
I | embed: rejected connection from "127.0.0.1:55702" (error "remote error: tls: bad certificate", ServerName "m3.etcd.local")
Each process should be started with:
etcd --peer-cert-allowed-cn etcd.local
I | pkg/netutil: resolving m3.etcd.local:32380 to 127.0.0.1:32380
I | pkg/netutil: resolving m2.etcd.local:22380 to 127.0.0.1:22380
I | pkg/netutil: resolving m1.etcd.local:2380 to 127.0.0.1:2380
I | etcdserver: published {Name:m3 ClientURLs:[https://m3.etcd.local:32379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | etcdserver: published {Name:m1 ClientURLs:[https://m1.etcd.local:2379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | etcdserver: published {Name:m2 ClientURLs:[https://m2.etcd.local:22379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | embed: serving client requests on 127.0.0.1:32379
I | embed: serving client requests on 127.0.0.1:22379
I | embed: serving client requests on 127.0.0.1:2379
v3.2.19 and v3.3.4 fixes TLS reload when certificate SAN field only includes IP addresses but no domain names. For example, a member is set up with CSRs (with cfssl
) as below:
{
"CN": "etcd.local",
"hosts": [
"127.0.0.1"
],
In Go, server calls (*tls.Config).GetCertificate
for TLS reload if and only if server's (*tls.Config).Certificates
field is not empty, or (*tls.ClientHelloInfo).ServerName
is not empty with a valid SNI from the client. Previously, etcd always populates (*tls.Config).Certificates
on the initial client TLS handshake, as non-empty. Thus, client was always expected to supply a matching SNI in order to pass the TLS verification and to trigger (*tls.Config).GetCertificate
to reload TLS assets.
However, a certificate whose SAN field does not include any domain names but only IP addresses would request *tls.ClientHelloInfo
with an empty ServerName
field, thus failing to trigger the TLS reload on initial TLS handshake; this becomes a problem when expired certificates need to be replaced online.
Now, (*tls.Config).Certificates
is created empty on initial TLS client handshake, first to trigger (*tls.Config).GetCertificate
, and then to populate rest of the certificates on every new TLS connection, even when client SNI is empty (e.g. cert only includes IPs).
etcd --host-whitelist
flag specifies acceptable hostnames from HTTP client requests. Client origin policy protects against "DNS Rebinding" attacks to insecure etcd servers. That is, any website can simply create an authorized DNS name, and direct DNS to "localhost"
(or any other address). Then, all HTTP endpoints of etcd server listening on "localhost"
becomes accessible, thus vulnerable to DNS rebinding attacks. See CVE-2018-5702 for more detail.
Client origin policy works as follows:
- If client connection is secure via HTTPS, allow any hostnames.
- If client connection is not secure and
"HostWhitelist"
is not empty, only allow HTTP requests whose Host field is listed in whitelist.
Note that the client origin policy is enforced whether authentication is enabled or not, for tighter controls.
By default, etcd --host-whitelist
and embed.Config.HostWhitelist
are set empty to allow all hostnames. Note that when specifying hostnames, loopback addresses are not added automatically. To allow loopback interfaces, add them to whitelist manually (e.g. "localhost"
, "127.0.0.1"
, etc.).
The crypto/tls
package of golang
checks the key usage of the certificate public key before using it.
To use the certificate public key to do client auth, we need to add clientAuth
to Extended Key Usage
when creating the certificate public key.
Here is how to do it:
Add the following section to openssl.cnf:
[ ssl_client ]
...
extendedKeyUsage = clientAuth
...
When creating the cert be sure to reference it in the -extensions
flag:
$ openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
Make sure to sign the certificates with a Subject Name the member's public IP address. The etcd-ca
tool for example provides an --ip=
option for its new-cert
command.
The certificate needs to be signed for the member's FQDN in its Subject Name, use Subject Alternative Names (short IP SANs) to add the IP address. The etcd-ca
tool provides --domain=
option for its new-cert
command, and openssl can make it too.