Skip to content

Commit

Permalink
Fix #33: Added support for cert based auth for apps
Browse files Browse the repository at this point in the history
  • Loading branch information
akclace committed Sep 15, 2024
1 parent dd09dcb commit 7b34168
Show file tree
Hide file tree
Showing 20 changed files with 510 additions and 27 deletions.
50 changes: 46 additions & 4 deletions internal/server/app_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package server
import (
"cmp"
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -410,6 +411,7 @@ func (s *Server) authenticateAndServeApp(w http.ResponseWriter, r *http.Request,
appAuth = types.AppAuthnSystem
}

appAuthString := string(appAuth)
if appAuth == types.AppAuthnNone {
// No authentication required
} else if appAuth == types.AppAuthnSystem {
Expand All @@ -420,15 +422,26 @@ func (s *Server) authenticateAndServeApp(w http.ResponseWriter, r *http.Request,
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
} else if appAuthString == "cert" || strings.HasPrefix(appAuthString, "cert_") {
// Use client certificate authentication
if s.config.Https.DisableClientCerts {
http.Error(w, "Client certificates are disabled in clace.config, update https.disable_client_certs", http.StatusInternalServerError)
return
}
err = s.verifyClientCerts(r, appAuthString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
} else {
authString := string(appAuth)
if !s.ssoAuth.ValidateProviderName(authString) {
http.Error(w, "Unsupported authentication provider: "+authString, http.StatusInternalServerError)
// Use SSO auth
if !s.ssoAuth.ValidateProviderName(appAuthString) {
http.Error(w, "Unsupported authentication provider: "+appAuthString, http.StatusInternalServerError)
return
}

// Redirect to the auth provider if not logged in
loggedIn, err := s.ssoAuth.CheckAuth(w, r, authString, true)
loggedIn, err := s.ssoAuth.CheckAuth(w, r, appAuthString, true)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
Expand All @@ -441,6 +454,35 @@ func (s *Server) authenticateAndServeApp(w http.ResponseWriter, r *http.Request,
app.ServeHTTP(w, r)
}

// verifyClientCerts verifies the client certificate, whether it is signed by one
// of the root CAs in the authName config
func (s *Server) verifyClientCerts(r *http.Request, authName string) error {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
return fmt.Errorf("client certificate required")
}

requestCert := r.TLS.PeerCertificates[0]
clientConfig, ok := s.config.ClientAuth[authName]
if !ok {
return fmt.Errorf("client auth config not found for %s", authName)
}

opts := x509.VerifyOptions{
Roots: clientConfig.RootCAs,
Intermediates: x509.NewCertPool(),
}

for _, cert := range r.TLS.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}

if _, err := requestCert.Verify(opts); err != nil {
return fmt.Errorf("client certificate verification failed: %w", err)
}

return nil
}

func (s *Server) MatchApp(hostHeader, matchPath string) (types.AppInfo, error) {
s.Trace().Msgf("MatchApp %s %s", hostHeader, matchPath)
apps, err := s.apps.GetAllApps()
Expand Down
43 changes: 39 additions & 4 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package server
import (
"context"
"crypto/tls"
"crypto/x509"
"embed"
"errors"
"fmt"
Expand Down Expand Up @@ -354,7 +355,12 @@ func (s *Server) Start() error {
}

if s.config.Https.Port >= 0 {
s.httpsServer = s.setupHTTPSServer()
var err error
s.httpsServer, err = s.setupHTTPSServer()
if err != nil {
return err
}

}

generatedPass, err := s.setupAdminAccount()
Expand Down Expand Up @@ -415,8 +421,7 @@ func (s *Server) Start() error {
return nil
}

func (s *Server) setupHTTPSServer() *http.Server {

func (s *Server) setupHTTPSServer() (*http.Server, error) {
var tlsConfig *tls.Config
if s.config.Https.ServiceEmail != "" {
// Certmagic is enabled
Expand Down Expand Up @@ -489,14 +494,44 @@ func (s *Server) setupHTTPSServer() *http.Server {
}
}

if !s.config.Https.DisableClientCerts {
// Request client certificates, verification is done in the handler
tlsConfig.ClientAuth = tls.RequestClientCert
for name, clientCertConfig := range s.config.ClientAuth {
rootCAs, err := loadRootCAs(clientCertConfig.CACertFile)
if err != nil {
return nil, fmt.Errorf("error loading root CAs pem file %s for %s: %w", clientCertConfig.CACertFile, name, err)
}
s.config.ClientAuth[name] = types.ClientCertConfig{
CACertFile: clientCertConfig.CACertFile,
RootCAs: rootCAs,
}
}
}

server := &http.Server{
WriteTimeout: 180 * time.Second,
ReadTimeout: 180 * time.Second,
IdleTimeout: 30 * time.Second,
Handler: s.handler.router,
TLSConfig: tlsConfig,
}
return server
return server, nil
}

func loadRootCAs(rootCertFile string) (*x509.CertPool, error) {
rootPEM, err := os.ReadFile(rootCertFile)
if err != nil {
return nil, err
}

roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(rootPEM)
if !ok {
return nil, fmt.Errorf("failed to parse root certificate %s", rootCertFile)
}

return roots, nil
}

// Stop stops the Clace Server
Expand Down
4 changes: 4 additions & 0 deletions internal/server/sso_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ func (s *SSOAuth) ValidateAuthType(authType string) bool {
case string(types.AppAuthnDefault), string(types.AppAuthnSystem), string(types.AppAuthnNone):
return true
default:
if authType == "cert" || strings.HasPrefix(authType, "cert_") {
_, ok := s.config.ClientAuth[authType]
return ok
}
return s.ValidateProviderName(authType)
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/system/clace.default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ service_email = "" # email address for registering w
use_staging = true # use Let's Encrypt staging server
cert_location = "$CL_HOME/config/certificates" # where to look for existing certificate files
storage_location = "$CL_HOME/run/certmagic" # where to cache dynamically created certificates
disable_client_certs = false # disable client certs for HTTPS

[security]
admin_over_tcp = false # enable admin API's over TCP (HTTP/HTTPS). Admin is over UDS only by default
Expand Down
46 changes: 27 additions & 19 deletions internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package types

import (
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -49,18 +50,19 @@ type GlobalConfig struct {
// ServerConfig is the configuration for the Clace Server
type ServerConfig struct {
GlobalConfig
Http HttpConfig `toml:"http"`
Https HttpsConfig `toml:"https"`
Security SecurityConfig `toml:"security"`
Metadata MetadataConfig `toml:"metadata"`
Log LogConfig `toml:"logging"`
System SystemConfig `toml:"system"`
GitAuth map[string]GitAuthEntry `toml:"git_auth"`
Plugins map[string]PluginSettings `toml:"plugin"`
Auth map[string]AuthConfig `toml:"auth"`
Secret map[string]SecretConfig `toml:"secret"`
ProfileMode string `toml:"profile_mode"`
AppConfig AppConfig `toml:"app_config"`
Http HttpConfig `toml:"http"`
Https HttpsConfig `toml:"https"`
Security SecurityConfig `toml:"security"`
Metadata MetadataConfig `toml:"metadata"`
Log LogConfig `toml:"logging"`
System SystemConfig `toml:"system"`
GitAuth map[string]GitAuthEntry `toml:"git_auth"`
Plugins map[string]PluginSettings `toml:"plugin"`
Auth map[string]AuthConfig `toml:"auth"`
ClientAuth map[string]ClientCertConfig `toml:"client_auth"`
Secret map[string]SecretConfig `toml:"secret"`
ProfileMode string `toml:"profile_mode"`
AppConfig AppConfig `toml:"app_config"`
}

type PluginSettings map[string]any
Expand Down Expand Up @@ -117,13 +119,14 @@ type HttpConfig struct {

// HttpsConfig is the configuration for the HTTPs server
type HttpsConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
EnableCertLookup bool `toml:"enable_cert_lookup"`
ServiceEmail string `toml:"service_email"`
UseStaging bool `toml:"use_staging"`
StorageLocation string `toml:"storage_location"`
CertLocation string `toml:"cert_location"`
Host string `toml:"host"`
Port int `toml:"port"`
EnableCertLookup bool `toml:"enable_cert_lookup"`
ServiceEmail string `toml:"service_email"`
UseStaging bool `toml:"use_staging"`
StorageLocation string `toml:"storage_location"`
CertLocation string `toml:"cert_location"`
DisableClientCerts bool `toml:"disable_client_certs"`
}

// SecurityConfig is the configuration for Inter process communication
Expand Down Expand Up @@ -179,6 +182,11 @@ type AuthConfig struct {
Scopes []string `toml:"scopes"` // oauth scopes
}

type ClientCertConfig struct {
CACertFile string `toml:"ca_cert_file"`
RootCAs *x509.CertPool `toml:"-"`
}

// ClientConfig is the configuration for the Clace Client
type ClientConfig struct {
GlobalConfig
Expand Down
42 changes: 42 additions & 0 deletions tests/certs/cers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash
# Based on https://github.com/haoel/mTLS/blob/main/key.sh
# run twice, first mv certs testcerts1 and then mv certs testcerts2

set -e

pushd `dirname $0` > /dev/null
SCRIPTPATH=`pwd -P`
popd > /dev/null
SCRIPTFILE=`basename $0`

mkdir -p ${SCRIPTPATH}/certs

cd ${SCRIPTPATH}/certs

DAYS=3650

# generate a self-signed rootCA file that would be used to sign both the server and client cert.
# Alternatively, we can use different CA files to sign the server and client, but for our use case, we would use a single CA.
openssl req -newkey rsa:2048 \
-new -nodes -x509 \
-days ${DAYS} \
-out ca.crt \
-keyout ca.key \
-subj "/C=SO/ST=Earth/L=Mountain/O=IntTest/OU=IntCloud/CN=localhost"

function generate_client() {
CLIENT=$1
O=$2
OU=$3
openssl genrsa -out ${CLIENT}.key 2048
openssl req -new -key ${CLIENT}.key -out ${CLIENT}.csr \
-subj "/C=SO/ST=Earth/L=Mountain/O=$O/OU=$OU/CN=localhost"
openssl x509 -req -in ${CLIENT}.csr \
-extfile <(printf "subjectAltName=DNS:localhost") \
-CA ca.crt -CAkey ca.key -out ${CLIENT}.crt -days ${DAYS} -sha256 -CAcreateserial
}

generate_client client.a Client-A Client-A-OU
generate_client client.b Client-B Client-B-OU

rm *.csr *.srl
22 changes: 22 additions & 0 deletions tests/certs/testcerts1/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDszCCApugAwIBAgIUV/xSc/ZUdzxRmjpNCdzn39Ue/6UwDQYJKoZIhvcNAQEL
BQAwaTELMAkGA1UEBhMCU08xDjAMBgNVBAgMBUVhcnRoMREwDwYDVQQHDAhNb3Vu
dGFpbjEQMA4GA1UECgwHSW50VGVzdDERMA8GA1UECwwISW50Q2xvdWQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0yNDA5MTQyMjMwNTVaFw0zNDA5MTIyMjMwNTVaMGkx
CzAJBgNVBAYTAlNPMQ4wDAYDVQQIDAVFYXJ0aDERMA8GA1UEBwwITW91bnRhaW4x
EDAOBgNVBAoMB0ludFRlc3QxETAPBgNVBAsMCEludENsb3VkMRIwEAYDVQQDDAls
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwza/O8MMa
e1GfW6POO+1No2oB7E1FuBDW2IzWRXBuQ0+a6l7z9rf7dKV7hiscY8CnR5jG2k+q
Fgv1CIjg9F7lUtwZ0/yF+vyO9YCuljagpe7zNn2nNgC/T9Kmpn9+kXzjCff3rC04
kxpcmeBukqF86fWMuiaexyQ5O8or9PPfF6ztZm/nt16A8rW4RG+9/4eXrESOkc/K
ryhU4/7bCtYyUOmcCwNhoi0Q6ZgNZyyMV2bC5YvFb6j3u06fK8VW19yQKF1g/Wnh
TiRJ/nBhgtMoD4HlhCiLO1iz/GnvzmmS3qmNuRCMByCwl9rujauEGn/30lVzRiPd
ohJcmroaS2S9AgMBAAGjUzBRMB0GA1UdDgQWBBQrvkEu/Q4G7J/hQcZb2R0uY7zI
lTAfBgNVHSMEGDAWgBQrvkEu/Q4G7J/hQcZb2R0uY7zIlTAPBgNVHRMBAf8EBTAD
AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDpv5ATJo9iv5VmgGd+k5V5jxzxtGlBticU
S3zMCvI2wG/U/Q0TzUQIvb97FUp4FNl+sP0zGUmNLUHx1UPeq17E2+lEcJGtq1Xz
xHEEo6w5PEvdXHKh7S/m7HWd1xoIcYBpWa+dtsMH25aanRJcXLVnVFvJ2s4bpOBs
3h+CagPnso/zxKTci4ar9OVmVH68f+CodzKLxZP1JYc5FDT4t5WIkkMzeJSevwWC
g0m4/kxhG/Doy9+foQK3U2MQyFLf7OrVYs8D+MfUWmEpP7NSFNYEg30mWI3+jw9I
W8Y1ftmWnKqhN4sGkK9MhrAuKAV8vzDnGqtYI8ynaWzPq4dmzXHp
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions tests/certs/testcerts1/ca.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDwza/O8MMae1Gf
W6POO+1No2oB7E1FuBDW2IzWRXBuQ0+a6l7z9rf7dKV7hiscY8CnR5jG2k+qFgv1
CIjg9F7lUtwZ0/yF+vyO9YCuljagpe7zNn2nNgC/T9Kmpn9+kXzjCff3rC04kxpc
meBukqF86fWMuiaexyQ5O8or9PPfF6ztZm/nt16A8rW4RG+9/4eXrESOkc/KryhU
4/7bCtYyUOmcCwNhoi0Q6ZgNZyyMV2bC5YvFb6j3u06fK8VW19yQKF1g/WnhTiRJ
/nBhgtMoD4HlhCiLO1iz/GnvzmmS3qmNuRCMByCwl9rujauEGn/30lVzRiPdohJc
mroaS2S9AgMBAAECggEAFOtxmhNNh9fty9/ct7Z6E7QeKgOa5PMsN1YByj2nGlUr
rcQtspkc+7HzDGuoBCPgYUf1jFT79cVLjaKLratdMSQFLhM+DW+0EEFgyAoRvdwc
ibGSsfudjgFDximRyu+dv2WmzrP9C7yv3hqaJlEDyYPTysGDSGj5iyVYF4CEzT0o
qRaj/SyuGsIkZ4povjzBNT+ifYRLAe+dqxSU+145IVI2TSO87x19KIjuI9Ma1ebK
FpFywf5gXKBKVANR/QWyQL40VbQI1IOQNuP99AgZzOt6bo00AGCp8klhuSoWoa3f
AIazZwzZtIaH25DhH4GOjlQEAMDDGDDVqPn4DYfdGwKBgQD9c7J+zp9dSJBZcHs4
O9Afr1s3riJzSqQBUsGI4xDBBwzW9lOlHcDGefjq9Jn/Ndw3VMhgEh73cwacOofO
64jOwuiYC9Tq6XTj4GLQLP+qMRHqRaXzTup7K4OJCQVa5zX7ZdsBUTzvAEuTcnio
KolQt3Ukmhu7WwvvP+/+NWiUSwKBgQDzOW+6hUS7H/U6AozuMFoC5Y70Ud2FXJtS
xYMydUf1q4gTIEkAek/0LcQzCCdpaKnQHSII2zbeTU3MwCg5Ci3hg/AdSRdixhnr
4hQXFnOnt1UH1GbfTn8HQvGa76bTpNMHHukZ63C0hAVPPI92e/TGq95g8n2ewsCt
7EfEknL2FwKBgFbQOmOVAqFBKbB5BrvMQQ2ZUvVPgB1dg6+wWPo6TJLRh2RaG2yD
NHTHcquH/Pedy0BomfnOOy0nCSyaH8qQgtvPhzwNUXDlZlRnl3EW+Lui8y/7i0w7
y7VEwlk97celSqhByzI4UVbMExb+LrXrW/152XYgEz+pf/0DOYEpahjhAoGAew7G
TrJ6scpbgjwkHabtA6Yti8OXto/CLkCsPm1EOTHOKQ8eoFVB+qLlL4SinBxOPnNe
8ugmfJbUFBHsx980k3Lzm32NjXX5S3UZAQ1k+qgxJ0L5wWWvs3bVUrSJzW8JLgMf
oPOUFGDvZsWXODVDBPmUC6zkjdyvK8/uB7Co5YUCgYEAxbbxDryPRZZ4j82lqQ4n
jjHJSF5y3QxXQpgqKVJg2YqiQMLLhmISBciMU4dUTm/Tj2qwrl6tc0Xk2dUYcLB3
9z6pQMqV3AJrQu4dLp4m39cMHSJV39yyxwGUFOG7jJlbhcl2iuzpsBmB5iw9Az6x
F/RKdj/xBeL9maYjO83zTN8=
-----END PRIVATE KEY-----
22 changes: 22 additions & 0 deletions tests/certs/testcerts1/client.a.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDvDCCAqSgAwIBAgIUK50cAFltk8nri254afjfpf6lFOIwDQYJKoZIhvcNAQEL
BQAwaTELMAkGA1UEBhMCU08xDjAMBgNVBAgMBUVhcnRoMREwDwYDVQQHDAhNb3Vu
dGFpbjEQMA4GA1UECgwHSW50VGVzdDERMA8GA1UECwwISW50Q2xvdWQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0yNDA5MTQyMjMwNTVaFw0zNDA5MTIyMjMwNTVaMG0x
CzAJBgNVBAYTAlNPMQ4wDAYDVQQIDAVFYXJ0aDERMA8GA1UEBwwITW91bnRhaW4x
ETAPBgNVBAoMCENsaWVudC1BMRQwEgYDVQQLDAtDbGllbnQtQS1PVTESMBAGA1UE
AwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4jC
jg8cuCsm68vMLHuH9/X+Yq9SA59iw+pK4vgkxXQ8f1OOCTtZRspI28YOWz3zri1+
QUm24IfZKa4IXl//r6aWQ2fTKb7jBp2/1zjBOkEbT0C/tUkIRzwFXkaDUHPMm74+
P6KcyV31AQyopBQhpxw3u2Jj1r+p9DczuTXVxGeJxxk9aG5iJ9X549dI/NWO+gTx
oidZrRjFfwpzGdMaT99lkTqFDl80YnPExxrnghTrKWC5EhrQAYDkhhLOenZcqQs1
wP0SAYnKLurER5D8L+Z9/Xqw7rbKaIaQiVmSk13C4uaxBm8cg7hbWdhsyPvl40EZ
LuaS/h16O7c75EF7vQIDAQABo1gwVjAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYD
VR0OBBYEFOHeJpvQhmilTExOASIwKo4NZDV6MB8GA1UdIwQYMBaAFCu+QS79Dgbs
n+FBxlvZHS5jvMiVMA0GCSqGSIb3DQEBCwUAA4IBAQAScTZZe9/GoHz32TyiK4ZW
oJ7isXA2Klbl2sU4z/C8Uwbvxr6o1CcViIV9XQ+x4eCNLvndQY66LqP6boNTHJvo
oBKpgji6SQ+bDlUL7hvjS3ZUcPyhbb3IeiHgPPxjrq0o/23KVO/fQb2TGb2qQqKA
sE3p5jOS4Xs4j/7R/bjjeawfM6TQq3W5cMFlVuTO/XOJ6YOZs35ZN1U3WbxkXpr0
tplFHthi5bKQih0X1Lc5SgxHNPnAwgvw6g4WumFQnBU8zd2GPZDgJRUMpNsg1Xt9
z3vxivQ+CNV823iP1f8H7YcCAarmw2MwglSMnlBKfubnXfwXVvfOBVAGTyL263vq
-----END CERTIFICATE-----
Loading

0 comments on commit 7b34168

Please sign in to comment.