Development About 4 min read

Mutual TLS health client

Read a health endpoint using a validated server certificate and your own client certificate.

Sources and scopeRead before use

Synthetic fixtures and the stated local tests. No product conformance or equipment compatibility claim.

Selected local test cases passed. Read the example limits.

Verification and testing

Run the demo#

Run from the repository root. The demo uses synthetic data and does not contact equipment.

sh
go run examples/go/mtls_health.go --demo

The demo validates a built in JSON response and prints {"status":"healthy"}.

Use and limits#

Use --url, --cert and --key for an authorised endpoint. Use --ca to add a private CA to the system trust store. The client certificate and private key are PEM files that you supply locally. No certificates or private keys are included in the example archive.

The client has a 15 second overall HTTP timeout, limits headers and body size, rejects redirects and does not inherit proxy settings. It checks the server hostname and requires TLS 1.2 or later. It accepts exactly one status field with healthy or degraded.

A certificate accepted by the server establishes only the identity and permissions configured there. This is a sample health contract, not a universal equipment API. Protect private key files using your operating system's access controls.

Source#

Download the source

go
// Read one health endpoint using mutual TLS and an explicit client certificate.
package main

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

const maxBytes = 65536

type Health struct {
	Status string `json:"status"`
}

func parseHealth(data []byte) (Health, error) {
	if len(data) > maxBytes {
		return Health{}, errors.New("response exceeds 64 KiB")
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	token, err := decoder.Token()
	if err != nil || token != json.Delim('{') {
		return Health{}, errors.New("expected an object")
	}
	seen := false
	result := Health{}
	for decoder.More() {
		key, err := decoder.Token()
		if err != nil || key != "status" || seen {
			return Health{}, errors.New("unknown or duplicate field")
		}
		seen = true
		if err := decoder.Decode(&result.Status); err != nil {
			return Health{}, err
		}
	}
	if _, err := decoder.Token(); err != nil {
		return Health{}, err
	}
	if _, err := decoder.Token(); err != io.EOF {
		return Health{}, errors.New("trailing JSON data")
	}
	if !seen || (result.Status != "healthy" && result.Status != "degraded") {
		return Health{}, errors.New("expected status healthy or degraded")
	}
	return result, nil
}

func readHealth(endpoint, caFile, certFile, keyFile string) (Health, error) {
	target, err := url.Parse(endpoint)
	if err != nil || target.Scheme != "https" || target.Hostname() == "" || target.User != nil || target.Fragment != "" {
		return Health{}, errors.New("use an HTTPS URL without embedded credentials")
	}
	if certFile == "" || keyFile == "" {
		return Health{}, errors.New("client certificate and key are required")
	}
	certificate, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return Health{}, errors.New("cannot load the client certificate and key")
	}
	roots, err := x509.SystemCertPool()
	if err != nil || roots == nil {
		roots = x509.NewCertPool()
	}
	if caFile != "" {
		pem, err := os.ReadFile(caFile)
		if err != nil || !roots.AppendCertsFromPEM(pem) {
			return Health{}, errors.New("cannot load the CA bundle")
		}
	}
	transport := &http.Transport{
		Proxy: nil,
		TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots,
			Certificates: []tls.Certificate{certificate}},
		TLSHandshakeTimeout:    5 * time.Second,
		ResponseHeaderTimeout:  5 * time.Second,
		MaxResponseHeaderBytes: 16384,
		DisableCompression:     true,
	}
	defer transport.CloseIdleConnections()
	client := &http.Client{Transport: transport, Timeout: 15 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
	request, err := http.NewRequest(http.MethodGet, target.String(), nil)
	if err != nil {
		return Health{}, err
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Accept-Encoding", "identity")
	response, err := client.Do(request)
	if err != nil {
		return Health{}, errors.New("connection or certificate validation failed")
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return Health{}, fmt.Errorf("unexpected HTTP status %d", response.StatusCode)
	}
	media, _, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
	if err != nil || media != "application/json" {
		return Health{}, errors.New("expected application/json")
	}
	encoding := strings.ToLower(response.Header.Get("Content-Encoding"))
	if encoding != "" && encoding != "identity" {
		return Health{}, errors.New("compressed responses are not accepted")
	}
	data, err := io.ReadAll(io.LimitReader(response.Body, maxBytes+1))
	if err != nil {
		return Health{}, err
	}
	return parseHealth(data)
}

func main() {
	demo := flag.Bool("demo", false, "validate a built in response without networking")
	endpoint := flag.String("url", "", "exact HTTPS health endpoint")
	ca := flag.String("ca", "", "optional private CA PEM file")
	cert := flag.String("cert", "", "client certificate PEM file")
	key := flag.String("key", "", "client private key PEM file")
	flag.Parse()
	var result Health
	var err error
	if flag.NArg() != 0 || (*demo && *endpoint != "") || (!*demo && *endpoint == "") {
		fmt.Fprintln(os.Stderr, "Use --demo or --url with --cert and --key")
		os.Exit(2)
	}
	if *demo {
		result, err = parseHealth([]byte(`{"status":"healthy"}`))
	} else {
		result, err = readHealth(*endpoint, *ca, *cert, *key)
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, "Error:", err)
		os.Exit(1)
	}
	if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
		os.Exit(1)
	}
}

References#

Go HTTP client

Go TLS

All code examples · Code index