The Why

Most of the inter-service communication is switching to, or has already switched to a secure mode. This is especially important if your objective is to achieve any compliance certification. While this is great in production, it makes testing a but difficult. Based on the availability of PKI in the company, our integration testing environment may run in a secure mode. We may be able to generate self-signed certificates for our local development environment but it requires turning off hostname verification, which isn’t a great idea.

Mountebank has rich support for HTTPS and we should be able to modify our previous remote mock setup to support communication to a dependency over https.

Generating certificates

Generating local CA

You will need to establish a local CA to start using certificates for the development environment. We will use a config file to generate certificates.

Here’s a config file for our local CA. Save it as ca.cnf to a ca-certs directory.

[ req ]
default_bits = 2048
default_md = sha256
prompt = no
encrypt_key = no
distinguished_name = ca_dn
x509_extensions = ca_extensions

[ ca_dn ]
C = US
O = Local Development CA
CN = dev-ca.example.com

[ ca_extensions ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = critical,CA:true
keyUsage = cRLSign,keyCertSign

And commands using openssl to generate the CA certificate using ca.cnf from above.

# Export a password to use for the CA key.
export CA_KEY_PWD="<some-password>"

# Generate a private key for the CA
openssl genrsa -out ./ca-cert/ca-key.pem -passout pass:$CA_KEY_PWD

# Generate the CA cert using the config file above
openssl req -new -x509 -sha256 -days 365 \
        -nodes -key ./ca-cert/ca-key.pem \
        -out ./ca-cert/ca-cert.pem \
        -config ./ca-cert/ca.cnf \
        -passin pass:$CA_KEY_PWD

Generating mountebank certificate

We will use our “Local Development CA” to obtain a certificate for our mountebank server in mb-cert directory.

The configuration for the server

[ req ]
default_bits = 2048
default_md = sha256
prompt = no
encrypt_key = no
distinguished_name = mb_dn
req_extensions = req_extensions

[ mb_dn ]
C = US
O = Local Mountebank Server
CN = mountebank.example.com

[ req_extensions ]
subjectAltName = @alt_names
subjectKeyIdentifier = hash
keyUsage = critical, digitalSignature, keyEncipherment
# This allows the certificate to be used as a server certificate
extendedKeyUsage = serverAuth 

# This section allows to define alternate names for the mountebank server
# Ideally, this will match the network aliases in your docker-compose file
# A client doing hostname validation will match its expected name to one of
# names defined here

[ alt_names ]
DNS.1 = mountebank.example.com
DNS.2 = auth.example.com
DNS.3 = telemetry.example.com

And a script to generate a server certificate

# Export a password to use for the mountebank key.
export MB_KEY_PWD="<mb-password>"

# Generate a private key for the mountebank server
openssl genrsa -out ./mb-cert/mb-key.pem -passout pass:$MB_KEY_PWD

# Create a new certificate signing request using the config above
openssl req -new -key ./mb-cert/mb-key.pem -sha256 \
        -out ./mb-cert/mb-cert.csr \
        -config ./mb-cert/mb.cnf \
        -passin pass:$MB_KEY_PWD

# As a CA, sign the certificate request and issue a new certificate
# allowing the desired extensions
openssl x509 -req -days 365 -sha256 -in ./mb-cert/mb-cert.csr \
        -CA ./ca-cert/ca-cert.pem -CAkey ./ca-cert/ca-key.pem \
        -extfile ./mb-cert/mb.cnf -extensions req_extensions \
        -set_serial 1 -out ./mb-cert/mb-cert.pem

# We need to remove newlines from the private key and certificate 
# so they can be loaded them in our imposter.
cat ./mb-cert/mb-key.pem | awk '{printf "%s\\n", $0}' > ./mb-cert/mb-key-flat.pem
cat ./mb-cert/mb-cert.pem | awk '{printf "%s\\n", $0}' > ./mb-cert/mb-cert-flat.pem

Putting it all together

Updating the docker-compose file

We will mount the mb-certs directory as a volume so mountebank can load them.

version: '3'
services:
  account_service: 
    image: example/account_service:latest
    container_name: account_service
    volumes:
      - ./logs:/app/logs
    ports:
      - "443:443"
    networks:
      account_service_network:
        ipv4_address: 172.1.1.5
  mountebank:
    build:
      context: account_service/mocks
      dockerfile: Dockerfile
    container_name: mountebank
    ports:
      - "2525:2525"
    volumes:
      - ./account_service/mocks/imposters.ejs:/mountebank/imposters.ejs
      - ./mb-certs:/mountebank/certs # Mount the certs directory
    networks:
      account_service_network:
        ipv4_address: 172.1.1.6
      aliases:
        - auth.example.com 
networks:
  account_service_network:
    driver: bridge
    ipam:
      driver: default
      config:
        -
          subnet: 172.1.1.0/24

Updating the imposter

Here’s a new imposter that uses the https protocol and uses the new certificate we just created.

{
  "port": 443,
  "protocol": "https",
  "key": "<% include /mountebank/certs/mb-key-flat.pem %>",
  "cert": "<% include /mountebank/certs/mb-cert-flat.pem %>",
  "stubs": [{
    "responses": [{ 
        "is": { 
            "statusCode": 200,
            "body": { "auth_status": "true" }
        }
    }],
    "predicates": [{
        "equals": {
            "path": "/v1/",
            "method": "POST",
            "headers": { 
                "Host": "auth.example.com",
                "Content-Type": "application/json" 
            }
        }
    }]
  }]
}

So far we have:

  1. Created a local CA and generated required certificates
  2. Updated the docker container configuration to make the certificates available to mountebank
  3. Created a new imposter that offers a server certificate with all possible remote dependency names

Wrap

It is certainly possible to mock secure endpoints of our dependencies without introducing any insecure option in our code base. Mountebank combined with the right certificates would also allow us to test dependencies that require a client certificate.