How to use etcd (multi-machine cluster TLS/SSL security mode) in Ubuntu?


From this post, I run multi-machine cluster basic mode. Now, I will run multi-machine cluster with TLS/SSL security. I am not the security engineer. I am not friendly about TLS/SSL concepts. I studied about key and chain concept before, however it still difficult to understand it. In this documentation, there are 2 things required,  a unique key pair (member.crt, member.key) and shared cluster CA certificate (ca.crt). In this post, I address how to create these.


1. Overview the command for multi-machine cluster with TLS/SSL.


From this documentation, the commands are look like below.


etcd --name infra0 --initial-advertise-peer-urls https://172.22.0.96:2380 \

  --listen-peer-urls https://172.22.0.96:2380 \

  --listen-client-urls https://172.22.0.96:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.96:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \

  --cert-file=/path/to/infra0-client.crt --key-file=/path/to/infra0-client.key \

  --peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \

  --peer-cert-file=/path/to/infra0-peer.crt --peer-key-file=/path/to/infra0-peer.key


etcd --name infra1 --initial-advertise-peer-urls https://172.22.0.33:2380 \

  --listen-peer-urls https://172.22.0.33:2380 \

  --listen-client-urls https://172.22.0.33:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.33:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \

  --cert-file=/path/to/infra1-client.crt --key-file=/path/to/infra1-client.key \

  --peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \

  --peer-cert-file=/path/to/infra1-peer.crt --peer-key-file=/path/to/infra1-peer.key


etcd --name infra2 --initial-advertise-peer-urls https://172.22.0.133:2380 \

  --listen-peer-urls https://172.22.0.133:2380 \

  --listen-client-urls https://172.22.0.133:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.133:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \

  --cert-file=/path/to/infra2-client.crt --key-file=/path/to/infra2-client.key \

  --peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \

  --peer-cert-file=/path/to/infra2-peer.crt --peer-key-file=/path/to/infra2-peer.key 


From command line above, there is something unknown like below. I need certificate. However, I do not know how and what can I obtain these.


  --client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \

  --cert-file=/path/to/infra2-client.crt --key-file=/path/to/infra2-client.key \

  --peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \

  --peer-cert-file=/path/to/infra2-peer.crt --peer-key-file=/path/to/infra2-peer.key  


"cfssl" can do for these. In this Git link, there are introduction about "cfssl" like below.


This demonstrates using Cloudflare's cfssl to easily generate certificates for an etcd cluster. 


2. Download and install the "cfssl"


In this documentation, there are installation guide and usage


mkdir ~/bin

curl -s -L -o ~/bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64

curl -s -L -o ~/bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64

chmod +x ~/bin/{cfssl,cfssljson} 

export PATH=$PATH:~/bin 


It is so simple. I verify the version.


# cfssl version

Version: 1.2.0

Revision: dev

Runtime: go1.6


3. Create root certificate (CA).


In the command, there are 2 root certificate, ca-client.crt and ca-peer.crt. It will be shared between all hosts. Thus, I create these certificate on single host and copy into others. To create root certificate, I need 2 files, "config.json" and "csr.json". The contents is here.


# vi ca-client-config.json

{

  "signing": {

    "default": {

        "usages": [

          "signing",

          "key encipherment",

          "server auth",

          "client auth"

        ],

        "expiry": "8760h"

    }

  }

}


# vi ca-client-csr.json

{ "CN": "CA Client", "key": { "algo": "ecdsa", "size": 384 }, "names": [ { "O": "Honest Achmed's Used Certificates", "OU": "Hastily-Generated Values Divison", "L": "San Francisco", "ST": "California", "C": "US" } ] }


I create "ca-client-config.json" and "ca-client-csr.json" for ca-client certificate. I have to create more for ca-peer certificate. CN name should be different.


# vi ca-peer-config.json

{

  "signing": {

    "default": {

        "usages": [

          "signing",

          "key encipherment",

          "server auth",

          "client auth"

        ],

        "expiry": "8760h"

    }

  }

}


# vi ca-peer-csr.json

{ "CN": "CA Peer", "key": { "algo": "ecdsa", "size": 384 }, "names": [ { "O": "Honest Achmed's Used Certificates", "OU": "Hastily-Generated Values Divison", "L": "San Francisco", "ST": "California", "C": "US" } ] }


Now, I can generate with files above. Please, note that I will obtain some files which is named "ca.csr", "ca-key.pem" and "ca.pem"


# cfssl gencert -initca ca-client-csr.json | cfssljson -bare ca-client -

# cfssl gencert -initca ca-peer-csr.json | cfssljson -bare ca-peer -


I can verify this is normally good or not. 


# openssl x509 -noout -text -in ca-client.pem

# openssl x509 -noout -text -in ca-peer.pem


It's good. I can generate intermediate certification with this certificate. Therefore I need to copy these files into other hosts. I move files on each directory like below. (I create these directory on every hosts)


# mkdir ~/cert/clientm

# cp ca-client* ~/cert/client/

# ls ~/cert/client/

ca-client-key.pem ca-client.pem ca-client-config.json


# mkdir ~/cert/peer

# cp ca-peer* ~/cert/peer/

# ls ~/cert/peer/

ca-peer-key.pem  ca-peer.pem 

 

In this documentation, there are several security models. Example 2 (Red) and Example 3(Blue) are matched with the command above.


etcd --name infra0 --initial-advertise-peer-urls https://172.22.0.96:2380 \

  --listen-peer-urls https://172.22.0.96:2380 \

  --listen-client-urls https://172.22.0.96:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.96:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \

  --cert-file=/path/to/infra0-client.crt --key-file=/path/to/infra0-client.key \

  --peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \

  --peer-cert-file=/path/to/infra0-peer.crt --peer-key-file=/path/to/infra0-peer.key 


At first, In example 2, I need server to run "etcd" and I need client certificate to get response. And second, In example 3, I need peer certificate to communicate between hosts.


4. Create server, client and peer certificate.


Look at this post, there are way to generate each certificate. I create server certificate. To create, I need to "server.json" file. In this Git, there are sample JSON file. For server.json, hosts information are most important.


IP's currently in the config should be replaced/added with IP addresses of each cluster node, please note 127.0.0.1 is always required for loopback  


# cat server.json

{

  "CN": "infra0-client",

  "hosts": [

    "172.22.0.96",

    "127.0.0.1"

  ],

  "key": {

    "algo": "ecdsa",

    "size": 384

  },

  "names": [

    {

      "O": "autogenerated",

      "OU": "etcd cluster",

      "L": "the internet"

    }

  ]

}


# cat server.json

{

  "CN": "infra1-client",

  "hosts": [

    "172.22.0.33",

    "127.0.0.1"

  ],

  "key": {

    "algo": "ecdsa",

    "size": 384

  },

  "names": [

    {

      "O": "autogenerated",

      "OU": "etcd cluster",

      "L": "the internet"

    }

  ]

}


# cat server.json { "CN": "infra2-client", "hosts": [ "172.22.0.133", "127.0.0.1" ], "key": { "algo": "ecdsa", "size": 384 }, "names": [ { "O": "autogenerated", "OU": "etcd cluster", "L": "the internet" } ] }


I will generate server certificate. Please note that the command above use the name "infra-client", however, this is server certification, it is not client certificate.


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=server server.json | cfssljson -bare infra0-client

# mv infra0-client* client/


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=server server.json | cfssljson -bare infra1-client

# mv infra1-client* client/


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=server server.json | cfssljson -bare infra2-client

# mv infra2-client* client/


"-profile=server" option makes server certificate. Next, I create the peer certificate. Also I need "peer.json" file. This file is similar with "server.json". However, CommonName (CN) should be different.


# cat peer.json

{

  "CN": "infra0-peer",

  "hosts": [

    "172.22.0.96",

    "127.0.0.1"

  ],

  "key": {

    "algo": "ecdsa",

    "size": 384

  },

  "names": [

    {

      "O": "autogenerated",

      "OU": "etcd cluster",

      "L": "the internet"

    }

  ]

}


# cat peer.json

{

  "CN": "infra1-peer",

  "hosts": [

    "172.22.0.33",

    "127.0.0.1"

  ],

  "key": {

    "algo": "ecdsa",

    "size": 384

  },

  "names": [

    {

      "O": "autogenerated",

      "OU": "etcd cluster",

      "L": "the internet"

    }

  ]

}


# cat peer.json { "CN": "infra2-peer", "hosts": [ "172.22.0.133", "127.0.0.1" ], "key": { "algo": "ecdsa", "size": 384 }, "names": [ { "O": "autogenerated", "OU": "etcd cluster", "L": "the internet" } ] }


I generate peer certificate with peer.json file above. "-profile=peer" is adopted at this time.


# cfssl gencert -ca=peer/ca-peer.pem -ca-key=peer/ca-peer-key.pem -config=peer/ca-peer-config.json -profile=peer peer.json | cfssljson -bare infra0-peer

# mv infra0-peer* peer/


# cfssl gencert -ca=peer/ca-peer.pem -ca-key=peer/ca-peer-key.pem -config=peer/ca-peer-config.json -profile=peer peer.json | cfssljson -bare infra1-peer

# mv infra1-peer* peer/


# cfssl gencert -ca=peer/ca-peer.pem -ca-key=peer/ca-peer-key.pem -config=peer/ca-peer-config.json -profile=peer peer.json | cfssljson -bare infra2-peer

# mv infra2-peer* peer/


I create server certificate and peer certificate. Therefore, I can run "etcd" with both certificate. However, I need client certificate to obtain response. I will use same root certificate of the server. I need "client.json" file.


# cat client.json { "CN": "infra0", "hosts": [""], "key": { "algo": "ecdsa", "size": 384 }, "names": [ { "O": "autogenerated", "OU": "etcd cluster", "L": "the internet" } ] }


# cat client.json
{
  "CN": "infra1",
  "hosts": [""],
  "key": {
    "algo": "ecdsa",
    "size": 384
  },
  "names": [
    {
      "O": "autogenerated",
      "OU": "etcd cluster",
      "L": "the internet"
    }
  ]
}

# cat client.json
{
  "CN": "infra2",
  "hosts": [""],
  "key": {
    "algo": "ecdsa",
    "size": 384
  },
  "names": [
    {
      "O": "autogenerated",
      "OU": "etcd cluster",
      "L": "the internet"
    }
  ]
}


I generate peer certificate with client.json file above. "-profile=client" is adopted at this time.


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=client client.json | cfssljson -bare infra0


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=client client.json | cfssljson -bare infra1


# cfssl gencert -ca=client/ca-client.pem -ca-key=client/ca-client-key.pem -config=client/ca-client-config.json -profile=client client.json | cfssljson -bare infra2


5. Run etcd in security mode.


I ready all certificate to run in security mode. Final command looks like below.


etcd --name infra0 --initial-advertise-peer-urls https://172.22.0.96:2380 \

  --listen-peer-urls https://172.22.0.96:2380 \

  --listen-client-urls https://172.22.0.96:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.96:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=client/ca-client.pem \

  --cert-file=client/infra0-client.pem --key-file=client/infra0-client-key.pem \

  --peer-client-cert-auth --peer-trusted-ca-file=peer/ca-peer.pem \

  --peer-cert-file=peer/infra0-peer.pem --peer-key-file=peer/infra0-peer-key.pem


etcd --name infra1 --initial-advertise-peer-urls https://172.22.0.33:2380 \

  --listen-peer-urls https://172.22.0.33:2380 \

  --listen-client-urls https://172.22.0.33:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.33:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=client/ca-client.pem \

  --cert-file=client/infra1-client.pem --key-file=client/infra1-client-key.pem \

  --peer-client-cert-auth --peer-trusted-ca-file=peer/ca-peer.pem \

  --peer-cert-file=peer/infra1-peer.pem --peer-key-file=peer/infra1-peer-key.pem


etcd --name infra2 --initial-advertise-peer-urls https://172.22.0.133:2380 \

  --listen-peer-urls https://172.22.0.133:2380 \

  --listen-client-urls https://172.22.0.133:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.133:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --client-cert-auth --trusted-ca-file=client/ca-client.pem \

  --cert-file=client/infra2-client.pem --key-file=client/infra2-client-key.pem \

  --peer-client-cert-auth --peer-trusted-ca-file=peer/ca-peer.pem \

  --peer-cert-file=peer/infra2-peer.pem --peer-key-file=peer/infra2-peer-key.pem


At this time, I do not know command line to put and get. However, there is sample curl command in this documentation (Example 2).


curl --cacert client/ca-client.pem --cert infra0.pem --key infra0-key.pem -L https://172.22.0.96:2379/v2/keys/foo -XPUT -d value=bar -v 


# curl --cacert client/ca-client.pem --cert infra0.pem --key infra0-key.pem -L https://172.22.0.96:2379/v2/keys/foo -XPUT -d value=bar -v * Trying 172.22.0.96... * Connected to 172.22.0.96 (172.22.0.96) port 2379 (#0) * found 1 certificates in client/ca-client.pem * found 592 certificates in /etc/ssl/certs * ALPN, offering http/1.1 * SSL connection using TLS1.2 / ECDHE_ECDSA_AES_128_GCM_SHA256 * server certificate verification OK * server certificate status verification SKIPPED * common name: infra0-client (matched) * server certificate expiration date OK * server certificate activation date OK * certificate public key: EC * certificate version: #3 * subject: L=the internet,O=autogenerated,OU=etcd cluster,CN=infra0-client * start date: Fri, 19 Oct 2018 16:34:00 GMT * expire date: Sat, 19 Oct 2019 16:34:00 GMT * issuer: C=US,ST=California,L=San Francisco,O=Honest Achmed's Used Certificates,OU=Hastily-Generated Values Divison,CN=CA Client * compression: NULL * ALPN, server did not agree to a protocol > PUT /v2/keys/foo HTTP/1.1 > Host: 172.22.0.96:2379 > User-Agent: curl/7.47.0 > Accept: */* > Content-Length: 9 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 9 out of 9 bytes < HTTP/1.1 201 Created < Content-Type: application/json < X-Etcd-Cluster-Id: e3631873cee60c62 < X-Etcd-Index: 17 < X-Raft-Index: 26 < X-Raft-Term: 47 < Date: Fri, 19 Oct 2018 17:33:13 GMT < Content-Length: 90 < {"action":"set","node":{"key":"/foo","value":"bar","modifiedIndex":17,"createdIndex":17}}


# curl --cacert client/ca-client.pem --cert infra0.pem --key infra0-key.pem -L https://172.22.0.96:2379/v2/keys/foo

{"action":"get","node":{"key":"/foo","value":"bar","modifiedIndex":17,"createdIndex":17}}


I can see the result {"action":"set","node":{"key":"/foo","value":"bar","modifiedIndex":17,"createdIndex":17}}. It's works now.


6. Run automatic certificate mode


So far, I do so many step to run in security mode. It's is not simple. Because of this, etcd offers automatic certificate mode. In this mode, etcd create certificate automatically. "--auto-tls" and "--peer-auto-tls" are replaced instead of part for certificate.


etcd --name infra0 --initial-advertise-peer-urls https://172.22.0.96:2380 \

  --listen-peer-urls https://172.22.0.96:2380 \

  --listen-client-urls https://172.22.0.96:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.96:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --auto-tls \

  --peer-auto-tls


etcd --name infra1 --initial-advertise-peer-urls https://172.22.0.33:2380 \

  --listen-peer-urls https://172.22.0.33:2380 \

  --listen-client-urls https://172.22.0.33:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.33:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --auto-tls \

  --peer-auto-tls


etcd --name infra2 --initial-advertise-peer-urls https://172.22.0.133:2380 \

  --listen-peer-urls https://172.22.0.133:2380 \

  --listen-client-urls https://172.22.0.133:2379,https://127.0.0.1:2379 \

  --advertise-client-urls https://172.22.0.133:2379 \

  --initial-cluster-token etcd-cluster-1 \

  --initial-cluster infra0=https://172.22.0.96:2380,infra1=https://172.22.0.33:2380,infra2=https://172.22.0.133:2380 \

  --initial-cluster-state new \

  --auto-tls \

  --peer-auto-tls


In Example 4 of this documentation, "curl -k https://127.0.0.1:2379/v2/keys/foo -Xput -d value=bar -v" command is used. (This site also useful)


# curl -k -X "PUT" https://127.0.0.1:2379/v2/keys/seoul -d value='seoul'

{"action":"set","node":{"key":"/seoul","value":"seoul","modifiedIndex":18,"createdIndex":18}}


# curl -k https://127.0.0.1:2379/v2/keys/seoul

{"action":"get","node":{"key":"/seoul","value":"seoul","modifiedIndex":18,"createdIndex":18}}


# curl -k -X "DELETE" https://127.0.0.1:2379/v2/keys/seoul


Now, I can use with simple model.


Reference


[ 1 ] http://createnetech.tistory.com/16

[ 2 ] http://createnetech.tistory.com/14

[ 3 ] http://createnetech.tistory.com/12

[ 4 ] https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/clustering.md

[ 5 ] https://coreos.com/etcd/docs/latest/op-guide/security.html

[ 6 ] https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/security.md

[ 7 ] https://coreos.com/os/docs/latest/generate-self-signed-certificates.html

[ 8 ] https://github.com/etcd-io/etcd/tree/master/hack/tls-setup

[ 9 ] https://coreos.com/etcd/docs/latest/demo.html

[ 10 ] https://www.mkyong.com/web/curl-put-request-examples/

+ Recent posts