Certificate Chain

An advanced walk through for using the Replicated cert command in an app's YAML definition to generate new TLS certificates for customer use in an application.

This content is associated with a legacy version of the Replicated product. For the current Replicated product documentation, see docs.replicated.com.

Certificate Chain App

This app is an example of some of the more advanced features of Replicated.

Using the cert command in your app’s YAML definition, you can transparently and easily generate new TLS certificates for customer use in your app. For example, one of your containers may be serving an API via HTTPS for your other containers to consume. Ideally, the communication between the services running in your containers would be encrypted and authenticated. That’s where the cert command becomes useful.

All certificates generated by the cert command are not self-signed; they are signed by a unique certificate authority (CA) created by the Replicated management container. Template functions are provided for accessing this CA so that your services can add it to their trust stores and perform proper validation of the chain.

This example app consists of two containers, one which runs a tiny HTTPS server program, and another which runs a similarly-tiny HTTPS client continuously issuing requests to the server. We want the communication between these two containerized services to be both encrypted and authenticated, so we make use of TLS and the Replicated cert command.

The Go source for these two small programs are provided here for your reference.

package main

import (
  "log"
  "net/http"
)

func handler(w http.ResponseWriter, req *http.Request) {
  w.Header().Set("Content-Type", "text/plain")
  w.Write([]byte("This is an example server.\n"))
}

func main() {
  http.HandleFunc("/", handler)
  err := http.ListenAndServeTLS(":10443", "/opt/cert_chain.pem", "/opt/private_key.pem", nil)
  if err != nil {
    log.Fatal(err)
  }
}
package main

import (
  "crypto/tls"
  "crypto/x509"
  "io/ioutil"
  "log"
  "net/http"
  "time"
)

func main() {
  pem, err := ioutil.ReadFile("/opt/ca.pem")
  if err != nil {
    log.Fatalln(err)
  }

  pool := x509.NewCertPool()
  if !pool.AppendCertsFromPEM(pem) {
    log.Fatalln("Couldn't import CA")
  }

  tr := &http.Transport{
    TLSClientConfig: &tls.Config{RootCAs: pool},
  }

  client := &http.Client{Transport: tr}

  for {
    time.Sleep(time.Second * 5)
    resp, err := client.Get("https://server.replexample.int:10443/")
    if err != nil {
      log.Println(err)
      continue
    }

    if resp.StatusCode != 200 {
      body, _ := ioutil.ReadAll(resp.Body)
      log.Printf("HTTPS request failed. Response follows:\n  Header: %+v\n  Body: %s\n", resp.Header, string(body))
    }

    resp.Body.Close()
  }
}

As you can see, the client service accesses the server via a FQDN (server.replexample.int) instead of an IP address. This hostname matches the Common Name field of the certificate generated using the cert command, so validation will succeed.

This particular hostname exists only inside the client’s container. This is enabled by a field in the app’s YAML definition called extra_hosts. Any number of hostname & IP address pairings can be defined this way.

Additionally, both services require access to parts of the certificate chain that has been generated by Replicated: the client needs access to the CA at /opt/ca.pem, and the server needs access to a concatenation of the certificate & the CA at /opt/cert_chain.pem as well as the certificate’s corresponding private key at /opt/private_key.pem. These files are made available to the services via template functions in the app’s YAML definition.

The app YAML is presented below with comments describing the function of each section.

---
replicated_api_version: "1.0.0"
name: "CACertExample"

# This is what kicks off the generation of the certificate.
# Notice the arguments: we want a 2048-bit key, and CN *.replexample.int
# The output is stored in the "cert_out" identifier. There are 3 values
# returned by this command. By index, these are:
# 0: the generated private key
# 1: the generated certificate
# 2: the certificate authority used to sign the generated certificate
cmds:
- name: cert_out
  cmd: cert
  args:
  - 2048
  - "*.replexample.int"

# Here, in the config section, we write the return values of 'cert_out'
# to a series of separate config options. These are marked 'hidden'
# so that they won't be shown on the settings screen.
# Writing these values into config options allows them to be used in
# template functions (see the "components" section below).
config:
- name: HiddenCertValues
  hidden: true
  items:
  - type: file
    name: newcert_privatekey
    hidden: true
    data_cmd:
      name: cert_out
      value_at: 0
  - type: file
    name: newcert_cert
    hidden: true
    data_cmd:
      name: cert_out
      value_at: 1
  - type: file
    name: newcert_ca
    hidden: true
    data_cmd:
      name: cert_out
      value_at: 2

components:
- name: ComponentOne
  containers:
  - source: public
    image_name: repljoe/ca_cert_example_server
    version: "1"
    ports:
    - port_type: tcp
      public_port: "10443"
      private_port: "10443"
    config_files:

    # Here we write the concatenated certificate & CA together to
    # a file that will be read by the server program. The template
    # function 'ConfigOptionData' returns the raw data stored in a
    # config option.
    - filename: /opt/cert_chain.pem
      contents: |
        {{repl ConfigOptionData "newcert_cert" }}
        {{repl ConfigOptionData "newcert_ca" }}        
      file_mode: "0600"
    customer_files:

    # The private key is a simpler case: we can write the contents
    # of the config option directly to a file using an entry here in
    # the 'config_files' section.
    - name: newcert_privatekey
      filename: /opt/private_key.pem
      file_mode: "0600"
- name: ComponentTwo
  cluster: true
  tags:
  - client
  containers:
  - source: public
    image_name: repljoe/ca_cert_example_client
    version: "3"

    # In order for the certificate chain to be properly validated,
    # the client needs to connect to the server using a hostname which
    # matches the Common Name field in the certificate it (the server)
    # is using.
    # In our 'cert' command at the top of this file, we explicitly
    # requested that the CN be a wildcard: *.replexample.int
    # We use the 'extra_hosts' directive here to define a hostname which
    # will satisfy the certificate validation process.
    # The IP address of the host with the server container is returned
    # dynamically by the 'NodePrivateIPAddress' template function.
    extra_hosts:
    - hostname: server.replexample.int
      address: '{{repl NodePrivateIPAddress "ComponentOne" "repljoe/ca_cert_example_server" }}'
    customer_files:
    - name: newcert_ca
      filename: /opt/ca.pem
      file_mode: "0600"