mTLS client auth reverse proxy

Van egy szerveralkalmazás, ami csak .htpasswd basic authentikációt tud - bár TLS-t támogat, a kliensek azonosítására szolgáló mTLS-t nem. Én pedig mindenképp azt szeretnék, mert elkapott a gépszíj, hogy ahol lehet, privát/publikus kulcsos azonosítást használjak; a secret sose hagyja el azt a gépet, ahol generáltuk.

Adja magát, hogy tegyünk elé egy reverse proxy-t, ami megoldja az azonosítást is, az alkalmazást pedig zárjuk be tűzfallal, hogy csak a reverse proxy felől érkező kéréseket fogadja.

Először nginx-et akartam, mert azt már valamennyire ismerem, aztán felmerült egy olyan igény, hogy a csatlakozó kliensektől függően ágazzunk el különböző IP-n/porton figyelő szerveralkalmazások felé. Gyakorlásképpen összedobtam egy Móricka-kódot Golang-ban, szerintem nem bonyolultabb, mint ugyanennek az nginx konfigja.

package main

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

type ClientInfo struct {
	mu          sync.Mutex
	lastRequest map[string]time.Time
}

func main() {
	serverCrt, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatalf("server: loadkeys: %s", err)
	}

	caCert, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatal(err)
	}

	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{serverCrt},
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    caCertPool,
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
			return &serverCrt, nil
		},
	}

	clientInfo := &ClientInfo{
		lastRequest: make(map[string]time.Time),
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
			cert := r.TLS.PeerCertificates[0]
			clientInfo.mu.Lock()
			lastRequest, ok := clientInfo.lastRequest[cert.Subject.CommonName]

			//log only requests unique after 10s
			if !ok || time.Since(lastRequest) > 10*time.Second {
				log.Printf("Client Cert Subject Common Name: %s\n", cert.Subject.CommonName)
				clientInfo.lastRequest[cert.Subject.CommonName] = time.Now()
			}
			clientInfo.mu.Unlock()

			var targetUrl *url.URL

			switch cert.Subject.CommonName {
			case "client04":
				targetUrl, _ = url.Parse("http://localhost:8144")
			case "client05":
				targetUrl, _ = url.Parse("http://localhost:8145")
			default:
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}

			reverseProxy := httputil.NewSingleHostReverseProxy(targetUrl)
			reverseProxy.ServeHTTP(w, r)
		}
	})

	server := &http.Server{
		Addr:      ":9140",
		TLSConfig: tlsConfig,
	}
	log.Fatal(server.ListenAndServeTLS("", ""))
}
  • A certeket egyelőre simán egy belső CA-ból generálom. Nyilván komolyabb felhasználásra igazi CA kell, vagy legalább a sajátot jobban megvédeni, intermediate certet használni stb.
  • a server.key és server.crt a reverse proxy kulcspárja, a szervert hitelesíti és a kliens felé történő HTTPS kommunikációt titkosítja.
  • kliens oldali hitelesítésként csak olyan certet fogad el, amit a kódban beimportált CA-k egyike állított ki.
  • A CN subject mezőt használom usernév gyanánt
  • A kliens certet is a szokásos módon állítjuk elő:
    • kliens gépen generáljuk privát kulcsot
    • létrehozunk egy certificate signing request-et (CSR)
    • ebből a CA-n létrehozzuk a certet
# Create CA
[ca]$ openssl genrsa -out ca.key 4096
[ca]$ openssl req -x509 -new -nodes -key ca.key -sha256 -days 1024 -out ca.crt

# Generate private key + CSR for reverse proxy
[proxy]$ openssl genrsa -out server.key 4096
[proxy]$ openssl req -new -key server.key -out server.csr

# Generate private key + CSR for client
[client]$ openssl genrsa -out client.key 4096
[client]$ openssl req -new -key client.key -out client.csr

# Sign server cert 
[ca]$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 1024 -sha256

# Sign client cert 
[ca]$ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 1024 -sha256

Kliensoldalon szükség lehet még a TLS cert és privát kulcs összefűzésére, ahol csak egy file-t tud használni a TLS client cert paraméter.

Nem vagyok a téma szakértője, csak ismerkedem, de a fentiekből már sikerült összehegeszteni egy működő tesztrendszert.

Hozzászólások

Haproxy -val megoldható a content routing bármilyen kb bármilyen TLS paraméter alapján.

Az URL parszolast és a revproxy létrehozását kihozhatnad a handleren kívülre. Ezek drága dolgoknak tűnnek nekem, de csak a partvonalrol ugatok.

de ha mar igy tweakelunk :) , en a lastRequest ido mokat is kitennem kulon fuggvenybe:

func (ci ClientInfo) NeedLog(CommonName string) bool {
  ci.mu.Lock()
  defer ci.mu.Unlock()

  lastRequest, ok := ci.lastRequest[CommonName]
  //log only requests unique after 10s
  if !ok || time.Since(lastRequest) > 10*time.Second {
   ci.lastRequest[CommonName] = time.Now()
   return true
  } 
  return false
}

aztan a kodban csak ennyi lenne: if clientInfo.NeedLog(cert.Subject.CommonName) { log.Printf("Client Cert Subject Common Name: %s\n", cert.Subject.CommonName) }

A vegtelen ciklus is vegeter egyszer, csak kelloen eros hardver kell hozza!