go_multi_library(
name = "http",
srcs = [
+ "authorizer_client_cert.go",
+ "authorizer_saml.go",
"http.go",
"proxy_protocol.go",
"range.go",
"route_action_proxy.go",
"route_action_redirect.go",
"route_action_s3.go",
- "route_action_saml.go",
"server.go",
"sni_listener.go",
"tcp.go",
--- /dev/null
+package http
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "gopkg.in/yaml.v3"
+
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type clientCertAuthorizer struct{}
+
+var _ Authorizer = &clientCertAuthorizer{}
+
+type clientCertAuthorizationHandler struct {
+ verbose bool
+ verifier mtls.MTLSPeerVerifier
+}
+
+var _ AuthorizationHandler = &clientCertAuthorizationHandler{}
+
+const (
+ clientCertAuthorizerName = "client_certificate"
+)
+
+func (a *clientCertAuthorizer) New(node *yaml.Node) (AuthorizationHandler, error) {
+ var rawNode struct {
+ Verbose bool `yaml:"verbose_errors"`
+ }
+
+ if err := node.Decode(&rawNode); err != nil {
+ return nil, err
+ }
+
+ rules, err := mtls.NewVerifyYamlConfig(node)
+ if err != nil {
+ return nil, err
+ }
+
+ handler := &clientCertAuthorizationHandler{
+ verifier: mtls.NewPeerNameVerifier(),
+ verbose: rawNode.Verbose,
+ }
+ if err := rules.Apply(handler.verifier); err != nil {
+ return nil, err
+ }
+
+ return handler, nil
+}
+
+func (h *clientCertAuthorizationHandler) Check(_ http.ResponseWriter, r *http.Request) (string, error) {
+ logger := LoggerFromContext(r.Context())
+
+ vh := VirtualHostFromContext(r.Context())
+ if vh == nil {
+ return "", errors.New("could not look up virtual host from request context")
+ }
+
+ var rid *mtls.RemoteIdentity
+ if tlsConn, ok := r.Context().Value(kTlsConn).(*tls.Conn); ok {
+ if verifier := vh.(*HTTPVirtualHost).tlsVerifier; verifier != nil {
+ if id, err := verifier.GetPeerIdentity(tlsConn); err == nil && id != nil {
+ logger.V(3).Debugf("set conn %p kRemoteIdentity = %v", tlsConn, id.ToSpiffe())
+ rid = id
+ } else if err != nil {
+ return "", fmt.Errorf("conn %p RemoteIdentity err: %v", tlsConn, err)
+ }
+ } else {
+ return "", fmt.Errorf("conn %p: no verifier", tlsConn)
+ }
+ }
+
+ if rid == nil {
+ return "", errors.New("failed to get remote identity")
+ }
+
+ log.StateFromContext(r.Context()).Set(kRemoteIdentity, rid)
+
+ err := h.verifier.MatchRemoteIdentity(rid)
+ if err != nil {
+ return "", NewHTTPErrorFromError(err, http.StatusUnauthorized)
+ }
+
+ return rid.ToSpiffe().String(), nil
+}
+
+func (h *clientCertAuthorizationHandler) Reject(w http.ResponseWriter, r *http.Request) {
+ var rid *mtls.RemoteIdentity
+ if id, ok := log.StateFromContext(r.Context()).Get(kRemoteIdentity).(*mtls.RemoteIdentity); ok {
+ rid = id
+ }
+
+ if h.verbose && rid != nil {
+ msg := fmt.Sprintf("access denied based on peer identity %q",
+ rid.ToSpiffe().String())
+ http.Error(w, msg, http.StatusUnauthorized)
+ } else {
+ http.Error(w, "The provided client identity does not have access to this resource.", http.StatusUnauthorized)
+ }
+}
+
+func init() {
+ AddAuthorizer(clientCertAuthorizerName, &clientCertAuthorizer{})
+}
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
+ "errors"
"fmt"
"math/big"
"net/http"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
+ "gopkg.in/yaml.v3"
"go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/utils/hashset"
- "gopkg.in/yaml.v3"
)
type SAMLServiceProvider struct {
mw map[string]*samlsp.Middleware
}
-type samlAction struct {
+type samlAuthorizer struct{}
+
+var _ Authorizer = &samlAuthorizer{}
+
+type samlAuthorizationHandler struct {
sp *SAMLServiceProvider
requireAuth bool
- usernameHeader string
preserveCookies bool
+ remapHeaders map[string]string
}
-var samlAttributeReplaceRegexp = regexp.MustCompile(`[^a-z0-9]+`)
-var restrictedHeaders = hashset.FromSlice([]string{"on-behalf-of"})
+var _ AuthorizationHandler = &samlAuthorizationHandler{}
-func (sa *samlAction) Handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- logger := LoggerFromContext(r.Context())
+var (
+ samlAttributeReplaceRegexp = regexp.MustCompile(`[^a-z0-9]+`)
+ restrictedHeaders = hashset.FromSlice([]string{"on-behalf-of"})
+)
- // if request was already authorized, do nothing
- if authz := AuthorizationFromContext(r.Context()); authz != nil {
- logger.V(3).Debugf("req already authorized for %q by %q, skip saml", authz.Principal, authz.Authorizer)
- next(w, r)
- return
+func (a *samlAuthorizer) New(node *yaml.Node) (AuthorizationHandler, error) {
+ var rawNode struct {
+ SP *struct {
+ *SAMLServiceProvider
+ Require string `yaml:"require"`
+ PreserveCookies bool `yaml:"preserve_cookies"`
+ RemapHeaders map[string]string `yaml:"remap_headers"`
+ } `yaml:"saml,omitempty"`
}
- // ensure client isn't trying to inject saml-related headers
- if err := sa.checkRequest(r); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
+ err := node.Decode(&rawNode)
+ if err != nil {
+ return nil, err
}
+ var spConfig *SAMLServiceProvider
+ require := true
+ preserveCookies := false
+ if rawNode.SP != nil {
+ require, err = strconv.ParseBool(rawNode.SP.Require)
+ if err != nil {
+ return nil, err
+ }
+ spConfig = rawNode.SP.SAMLServiceProvider
+ preserveCookies = rawNode.SP.PreserveCookies
+ }
+
+ var remapHeaders map[string]string = nil
+ if rawNode.SP != nil && rawNode.SP.RemapHeaders != nil {
+ remapHeaders = rawNode.SP.RemapHeaders
+ }
+
+ sa := &samlAuthorizationHandler{
+ sp: spConfig,
+ requireAuth: require,
+ remapHeaders: remapHeaders,
+ preserveCookies: preserveCookies,
+ }
+
+ return sa, nil
+}
+
+func (sa *samlAuthorizationHandler) getProvider(r *http.Request) (*samlsp.Middleware, error) {
sp := sa.sp
if sp == nil {
sp = serverDefaultSamlConfig(r.Context())
}
if sp == nil {
- http.Error(w, "SAML auth requested but no SP config present",
- http.StatusInternalServerError)
+ return nil, errors.New("saml auth requested but no SP config present")
}
- provider, err := sp.getMiddleware(r.Host)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ return sp.getMiddleware(r.Host)
+}
+
+func (sa *samlAuthorizationHandler) Check(w http.ResponseWriter, r *http.Request) (string, error) {
+ logger := LoggerFromContext(r.Context())
+
+ if authzn := AuthorizationFromContext(r.Context()); authzn != nil {
+ if authzn.Authorizer == "saml" {
+ return authzn.Principal, nil
+ }
}
- if r.URL.Path == "/saml/acs" {
- provider.ServeACS(w, r)
- return
+ // ensure client isn't trying to inject saml-related headers
+ if err := sa.checkRequest(r); err != nil {
+ return "", NewHTTPErrorFromError(err, http.StatusBadRequest)
+ }
+
+ provider, err := sa.getProvider(r)
+ if err != nil {
+ return "", err
}
session, sessionErr := provider.Session.GetSession(r)
if sessionErr != nil && sessionErr != samlsp.ErrNoSession {
- http.Error(w, sessionErr.Error(), http.StatusBadRequest)
- return
+ return "", NewHTTPErrorFromError(sessionErr, http.StatusBadRequest)
}
if sessionErr == samlsp.ErrNoSession && sa.requireAuth {
- // cleanup previous saml cookies, because these really pile up
- logger.V(3).Debugf("request tracker is a %T", provider.RequestTracker)
- if t, ok := provider.RequestTracker.(*samlsp.CookieRequestTracker); ok {
- sa.cleanCookies(w, r, t)
- }
- if r.Header.Get("sec-fetch-mode") == "cors" || r.Header.Get("sec-fetch-mode") == "websocket" {
- // XHRs should not be redirected to auth as this creates a ton of cookie spam
- errMsg := "SAML session required"
- if r.Header.Get("accept") == "application/json" {
- w.Header().Set("content-type", "application/json")
- errMsg = `{"error": "` + errMsg + `"}`
- }
- http.Error(w, errMsg, http.StatusUnauthorized)
- return
- }
- logger.V(3).Debugf("route requires a valid session, redirecting")
-
- provider.HandleStartAuthFlow(w, r)
- return
+ return "", nil
}
- var principal string
+ principal := "anonymous"
if swa, ok := session.(samlsp.SessionWithAttributes); ok {
attrs := swa.GetAttributes()
if uid := attrs.Get("uid"); uid != "" {
}
if jwts, ok := session.(samlsp.JWTSessionClaims); ok {
- r.Header.Set("x-saml-audience", jwts.StandardClaims.Audience)
+ r.Header.Set(sa.headerName("x-saml-audience"), jwts.StandardClaims.Audience)
logger.V(3).Debugf("setting origin request header: %s: %s", "x-saml-audience", jwts.StandardClaims.Audience)
iat := strconv.FormatInt(jwts.StandardClaims.IssuedAt, 10)
- r.Header.Set("x-saml-issued-at", iat)
+ r.Header.Set(sa.headerName("x-saml-issued-at"), iat)
logger.V(3).Debugf("setting origin request header: %s: %s", "x-saml-issued-at", iat)
eat := strconv.FormatInt(jwts.StandardClaims.ExpiresAt, 10)
- r.Header.Set("x-saml-expires-at", eat)
+ r.Header.Set(sa.headerName("x-saml-expires-at"), eat)
logger.V(3).Debugf("setting origin request header: %s: %s", "x-saml-expires-at", eat)
- r.Header.Set("x-saml-subject", jwts.StandardClaims.Subject)
+ r.Header.Set(sa.headerName("x-saml-subject"), jwts.StandardClaims.Subject)
logger.V(3).Debugf("setting origin request header: %s: %s", "x-saml-subject", jwts.StandardClaims.Subject)
if principal == "" {
principal = jwts.StandardClaims.Subject
for attr, values := range jwts.Attributes {
headerName := fmt.Sprintf("x-saml-%s",
samlAttributeReplaceRegexp.ReplaceAllString(strings.ToLower(attr), "-"))
+ headerName = sa.headerName(headerName)
headerValue := strings.Join(values, ", ")
logger.V(3).Debugf("setting origin request header: %s: %s",
headerName, headerValue)
r.Header.Set(headerName, headerValue)
}
- oboHeader := sa.usernameHeader
- if oboHeader == "" {
- oboHeader = "on-behalf-of"
- }
+ oboHeader := sa.headerName("on-behalf-of")
logger.V(3).Debugf("setting origin request header: %s: %s", oboHeader, principal)
r.Header.Set(oboHeader, principal)
} else {
- r.Header.Set("x-saml-anonymous-auth", "1")
+ r.Header.Set(sa.headerName("x-saml-anonymous-auth"), "1")
}
// don't forward saml cookies to the origin unless PreserveCookies is set
r = newReq
}
- SetAuthorizationContext(r.Context(), &Authorization{
- Authorizer: "saml",
- Principal: principal,
- })
+ return principal, nil
+}
- next(w, r)
+func (sa *samlAuthorizationHandler) headerName(in string) string {
+ if sa.remapHeaders != nil {
+ if altName, ok := sa.remapHeaders[in]; ok {
+ return altName
+ }
+ }
+
+ return in
+}
+
+func (sa *samlAuthorizationHandler) Reject(w http.ResponseWriter, r *http.Request) {
+ logger := LoggerFromContext(r.Context())
+
+ provider, err := sa.getProvider(r)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if r.URL.Path == "/saml/acs" {
+ provider.ServeACS(w, r)
+ return
+ }
+
+ // cleanup previous saml cookies, because these really pile up
+ if t, ok := provider.RequestTracker.(*samlsp.CookieRequestTracker); ok {
+ sa.cleanCookies(w, r, t)
+ }
+
+ logger.V(3).Debugf("request tracker is a %T", provider.RequestTracker)
+ if r.Header.Get("sec-fetch-mode") == "cors" || r.Header.Get("sec-fetch-mode") == "websocket" {
+ // XHRs should not be redirected to auth as this creates a ton of cookie spam
+ errMsg := "SAML session required"
+ if r.Header.Get("accept") == "application/json" {
+ w.Header().Set("content-type", "application/json")
+ errMsg = `{"error": "` + errMsg + `"}`
+ }
+ http.Error(w, errMsg, http.StatusUnauthorized)
+ return
+ }
+ logger.V(3).Debugf("route requires a valid session, redirecting")
+
+ provider.HandleStartAuthFlow(w, r)
}
-func (sa *samlAction) checkRequest(r *http.Request) error {
- for k, _ := range r.Header {
+func (sa *samlAuthorizationHandler) checkRequest(r *http.Request) error {
+ for k := range r.Header {
k = strings.ToLower(k)
- if strings.HasPrefix(k, "x-saml-") || restrictedHeaders.Contains(k) {
+ altName, remapped := sa.remapHeaders[k]
+ if strings.HasPrefix(k, "x-saml-") || restrictedHeaders.Contains(k) || (remapped && k == altName) {
return fmt.Errorf("downstream attempted to overwrite restricted header: %q", k)
}
}
//
// To mitigate this, we clear any existing saml cookies whenever we send an auth redirect or reject
// an XHR with a 401.
-func (sa *samlAction) cleanCookies(w http.ResponseWriter, r *http.Request, t *samlsp.CookieRequestTracker) {
+func (sa *samlAuthorizationHandler) cleanCookies(w http.ResponseWriter, r *http.Request, t *samlsp.CookieRequestTracker) {
for _, cookie := range r.Cookies() {
if strings.HasPrefix(cookie.Name, "saml_") {
logger := LoggerFromContext(r.Context())
})
}
-func samlInitHook(ctx context.Context, node *yaml.Node) (context.Context, error) {
- var samlConfig struct {
- SP *SAMLServiceProvider `yaml:"saml"`
- }
-
- if err := node.Decode(&samlConfig); err == nil && samlConfig.SP != nil {
- ctx = context.WithValue(ctx, kSamlDefaults, samlConfig.SP)
- }
-
- return ctx, nil
-}
-
func serverDefaultSamlConfig(ctx context.Context) *SAMLServiceProvider {
v := ctx.Value(kSamlDefaults)
if c, ok := v.(*SAMLServiceProvider); ok {
return nil
}
-func samlActionFromRouteYaml(node *yaml.Node) (RouteAction, error) {
- var rawNode struct {
- SP *struct {
- *SAMLServiceProvider
- Require string `yaml:"require"`
- PreserveCookies bool `yaml:"preserve_cookies"`
- } `yaml:"saml,omitempty"`
- Auth string `yaml:"auth"`
- }
-
- err := node.Decode(&rawNode)
- if err != nil || rawNode.Auth != "saml" {
- return nil, nil
- }
-
- var spConfig *SAMLServiceProvider
- require := true
- preserveCookies := false
- if rawNode.SP != nil {
- require, err = strconv.ParseBool(rawNode.SP.Require)
- if err != nil {
- return nil, err
- }
- spConfig = rawNode.SP.SAMLServiceProvider
- preserveCookies = rawNode.SP.PreserveCookies
+func samlInitHook(ctx context.Context, node *yaml.Node) (context.Context, error) {
+ var samlConfig struct {
+ SP *SAMLServiceProvider `yaml:"saml"`
}
- sa := &samlAction{
- sp: spConfig,
- requireAuth: require,
- preserveCookies: preserveCookies,
+ if err := node.Decode(&samlConfig); err == nil && samlConfig.SP != nil {
+ ctx = context.WithValue(ctx, kSamlDefaults, samlConfig.SP)
}
- return sa, nil
+ return ctx, nil
}
func init() {
+ AddAuthorizer("saml", &samlAuthorizer{})
AddServerInitHook(samlInitHook)
- AddRouteParseFunc(samlActionFromRouteYaml)
}
import (
"context"
"crypto/tls"
+ "crypto/x509"
"errors"
"fmt"
"net"
"os"
"regexp"
"strings"
+ "sync"
"go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/utils/log"
)
Routes []*Route `yaml:"routes"`
Certificate string `yaml:"cert"`
+ MtlsID string `yaml:"mtls_id"`
+ Verify *string `yaml:"verify"`
+ RequireCert *string `yaml:"require_cert"`
TrustUpstreamRequestID *bool `yaml:"trust_upstream_request_id"`
- tlsServer *http.Server
+ tlsServer *http.Server
+ tlsVerifier mtls.MTLSPeerVerifier
+ swappableTlsConfig *hotSwappableTlsConfig
}
+type HTTPError struct {
+ StatusCode int
+ Message string
+}
+
+var _ error = &HTTPError{}
+
func (v *HTTPVirtualHost) Serve(ctx context.Context, sniServer *SNIListener) (func() error, error) {
server, err := v.NewHTTPServerWithContext(ctx)
if err != nil {
}, nil
}
+func (h *HTTPVirtualHost) vhostIdentity() (mtls.Identity, error) {
+ if h.Certificate != "" && h.MtlsID != "" {
+ log.Default().WithPrefix("HTTPVirtualHost").Critical("ErrSSLCertAndMtlsIDMutex")
+ return nil, ErrSSLCertAndMtlsIDMutex
+ }
+
+ var id mtls.Identity
+ if h.Certificate != "" {
+ id = mtls.NewSSLCertificate(h.Certificate)
+ } else if h.MtlsID != "" {
+ id = mtls.NewServiceIdentity(h.MtlsID)
+ } else {
+ // use parent listener identity
+ return nil, nil
+ }
+
+ return id, nil
+}
+
+func (h *HTTPVirtualHost) newTlsConfig(ctx context.Context) (*tls.Config, mtls.MTLSPeerVerifier, error) {
+ var verifier mtls.MTLSPeerVerifier
+
+ id, err := h.vhostIdentity()
+ if err != nil {
+ return nil, nil, err
+ }
+ if id == nil {
+ return nil, nil, nil
+ }
+
+ config, err := id.TlsConfig(ctx)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if h.Verify != nil && *h.Verify != verifyTokenNone {
+ clientAuth := tls.RequireAndVerifyClientCert
+
+ if h.RequireCert != nil {
+ switch *h.RequireCert {
+ case tls.RequestClientCert.String():
+ clientAuth = tls.RequestClientCert
+ case tls.RequireAndVerifyClientCert.String():
+ clientAuth = tls.RequireAndVerifyClientCert
+ case tls.RequireAnyClientCert.String():
+ clientAuth = tls.RequireAnyClientCert
+ case tls.VerifyClientCertIfGiven.String():
+ clientAuth = tls.VerifyClientCertIfGiven
+ case tls.NoClientCert.String():
+ return nil, nil, fmt.Errorf("invalid value for require_cert %q; unset \"verify\" to disable client cert", *h.RequireCert)
+ default:
+ return nil, nil, fmt.Errorf("invalid value for require_cert %q; valid values", *h.RequireCert)
+ }
+ }
+ config.ClientAuth = clientAuth
+ if *h.Verify == verifyTokenMtls {
+ // verify with global mtls root CA
+ verifier = mtls.NewPeerNameVerifier()
+ if err = verifier.ConfigureServer(config); err != nil {
+ return nil, nil, err
+ }
+ verifier.AllowFrom(mtls.All)
+ } else {
+ certs, err := certutil.LoadCertificatesFromPEM(*h.Verify)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to load client CAs from %s: %v", *h.Verify, err)
+ }
+
+ pool := x509.NewCertPool()
+ for _, cert := range certs {
+ pool.AddCert(cert)
+ }
+
+ config.ClientCAs = pool
+ }
+ } else {
+ config.ClientAuth = tls.NoClientCert
+ config.VerifyPeerCertificate = nil
+ config.VerifyConnection = nil
+ config.RootCAs = nil
+ }
+
+ return config, verifier, nil
+}
+
// NewHTTPServerWithContext creates an http.Server using the proxy's virtual host
// and other settings.
func (v *HTTPVirtualHost) NewHTTPServerWithContext(ctx context.Context) (*http.Server, error) {
lm := log.NewLoggingMiddlewareWithLogger(
http.HandlerFunc(v.handle),
- logger.AppendPrefix(".access").WithLevel(log.INFO))
+ logger.AppendPrefix(".access").WithLevel(log.INFO),
+ )
lm.ExtraFunc = v.injectLogAttrs
server := &http.Server{
Addr: "",
BaseContext: func(l net.Listener) context.Context {
- return context.WithValue(serverCtx, kListenAddr, l.Addr())
+ ctx := context.WithValue(serverCtx, kListenAddr, l.Addr())
+ ctx = context.WithValue(ctx, kVirtualHost, v)
+ return ctx
+ },
+ ConnContext: func(ctx context.Context, c net.Conn) context.Context {
+ if tlsConn, ok := c.(*tls.Conn); ok {
+ ctx = context.WithValue(ctx, kTlsConn, tlsConn)
+ }
+
+ return ctx
},
Handler: lm.HandlerFunc(),
}
- if v.Certificate != "" {
- cert := mtls.NewSSLCertificate(v.Certificate)
- tlsConfig, err := cert.TlsConfig(serverCtx)
- if err != nil {
- return nil, err
- }
+ tlsConfig, verifier, err := v.newTlsConfig(serverCtx)
+ if err != nil {
+ return nil, err
+ }
+ if verifier != nil {
+ v.tlsVerifier = verifier
+ }
+
+ if tlsConfig != nil {
tlsConfig.NextProtos = []string{"h2"}
- server.TLSConfig = tlsConfig
+
+ server.TLSConfig, v.swappableTlsConfig = newHotSwappableTlsConfig(tlsConfig)
}
return server, nil
}
func (v *HTTPVirtualHost) swapTlsConfig(c *tls.Config) {
- v.tlsServer.TLSConfig = c
+ v.swappableTlsConfig.swap(c)
}
func vhostGlobToRegexp(name string) (*regexp.Regexp, error) {
}
if match {
+ if len(route.Authorizers) > 0 {
+ for _, authorizer := range route.Authorizers {
+ if princ, err := authorizer.handler.Check(w, r); princ != "" && err == nil {
+ SetAuthorizationContext(r.Context(), &Authorization{
+ Authorizer: authorizer.name,
+ Principal: princ,
+ })
+ break
+ } else if err != nil {
+ herr := NewHTTPErrorFromError(err, http.StatusInternalServerError)
+ herr.Write(w)
+ return
+ }
+ }
+
+ if AuthorizationFromContext(r.Context()) == nil {
+ route.Authorizers[0].handler.Reject(w, r)
+ return
+ }
+ }
if route.Action != nil {
logger.V(3).Debugf("route has action %T, dispatching: %+v", route.Action, route.Action)
next := http.NotFound
http.Error(w,
fmt.Sprintf("no action configured for route %s", route.Path.String()),
http.StatusInternalServerError)
+ return
}
}
}
entry["request_id"] = rid
}
}
+
+func (h *HTTPError) Error() string {
+ return fmt.Sprintf("HTTP %d %s\n%s", h.StatusCode, http.StatusText(h.StatusCode), h.Message)
+}
+
+func (h *HTTPError) Write(w http.ResponseWriter) {
+ http.Error(w, h.Message, h.StatusCode)
+}
+
+func NewHTTPError(statusCode int, msg string) *HTTPError {
+ return &HTTPError{
+ StatusCode: statusCode,
+ Message: msg,
+ }
+}
+
+func NewHTTPErrorFromError(err error, statusCode int) *HTTPError {
+ if h, ok := err.(*HTTPError); ok {
+ return h
+ }
+
+ return NewHTTPError(statusCode, err.Error())
+}
+
+type hotSwappableTlsConfig struct {
+ inner *tls.Config
+ mu sync.RWMutex
+}
+
+func (c *hotSwappableTlsConfig) GetCertificate(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.GetCertificate == nil {
+ return nil, nil
+ }
+ return c.inner.GetCertificate(chi)
+}
+
+func (c *hotSwappableTlsConfig) GetClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.GetClientCertificate == nil {
+ return nil, nil
+ }
+ return c.inner.GetClientCertificate(cri)
+}
+
+func (c *hotSwappableTlsConfig) GetConfigForClient(chi *tls.ClientHelloInfo) (*tls.Config, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.GetConfigForClient == nil {
+ return c.inner, nil
+ }
+ return c.inner.GetConfigForClient(chi)
+}
+
+func (c *hotSwappableTlsConfig) UnwrapSession(identity []byte, cs tls.ConnectionState) (*tls.SessionState, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.UnwrapSession == nil {
+ return c.inner.DecryptTicket(identity, cs)
+ }
+
+ return c.inner.UnwrapSession(identity, cs)
+}
+
+func (c *hotSwappableTlsConfig) WrapSession(cs tls.ConnectionState, ss *tls.SessionState) ([]byte, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.WrapSession == nil {
+ return c.inner.EncryptTicket(cs, ss)
+ }
+
+ return c.inner.WrapSession(cs, ss)
+}
+
+func (c *hotSwappableTlsConfig) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.VerifyPeerCertificate == nil {
+ return nil
+ }
+ return c.inner.VerifyPeerCertificate(rawCerts, verifiedChains)
+}
+
+func (c *hotSwappableTlsConfig) VerifyConnection(cs tls.ConnectionState) error {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ if c.inner.VerifyConnection == nil {
+ return nil
+ }
+
+ return c.inner.VerifyConnection(cs)
+}
+
+func (c *hotSwappableTlsConfig) swap(n *tls.Config) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.inner = n
+}
+
+func newHotSwappableTlsConfig(inner *tls.Config) (*tls.Config, *hotSwappableTlsConfig) {
+ hs := &hotSwappableTlsConfig{inner, sync.RWMutex{}}
+
+ outer := inner.Clone()
+ outer.GetCertificate = hs.GetCertificate
+ outer.GetClientCertificate = hs.GetClientCertificate
+ outer.GetConfigForClient = hs.GetConfigForClient
+ outer.WrapSession = hs.WrapSession
+ outer.UnwrapSession = hs.UnwrapSession
+ outer.VerifyPeerCertificate = hs.VerifyPeerCertificate
+ outer.VerifyConnection = hs.VerifyConnection
+
+ return outer, hs
+}
mc, err := a.minioClient()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(fmt.Sprintf("failed to init minio client: %+v", err)))
+ fmt.Fprintf(w, "failed to init minio client: %+v", err)
return
}
reqPath, err = url.PathUnescape(reqPath)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(fmt.Sprintf("failed decoding path: %+v", err)))
+ fmt.Fprintf(w, "failed decoding path: %+v", err)
return
}
objPath := strings.Trim(strings.Join(
strings.Trim(a.ObjectPrefix, "/"),
strings.Trim(reqPath, "/"),
},
- "/"), "/")
+ "/",
+ ), "/")
rc := http.StatusOK
var rh *RangeHeader
r.Context(),
a.BucketName,
objPath,
- minio.GetObjectOptions{})
-
+ minio.GetObjectOptions{},
+ )
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(fmt.Sprintf("failed to GetObject %q: %+v", reqPath, err)))
+ fmt.Fprintf(w, "failed to GetObject %q: %+v", reqPath, err)
return
}
if err != nil {
er := minio.ToErrorResponse(err)
w.WriteHeader(coalesce(er.StatusCode, http.StatusInternalServerError))
- w.Write([]byte(fmt.Sprintf("failed to stat object %q: %+v", objPath, err)))
+ fmt.Fprintf(w, "failed to stat object %q: %+v", objPath, err)
return
}
}
var seek int64
- var size = stat.Size
+ size := stat.Size
if rh != nil {
if rh.SuffixLength > 0 {
if rh.SuffixLength > uint64(stat.Size) {
http.Error(w,
fmt.Sprintf(
"requested suffix of %d bytes exceeds file size of %d bytes",
- rh.SuffixLength, stat.Size),
+ rh.SuffixLength, stat.Size,
+ ),
http.StatusRequestedRangeNotSatisfiable)
return
http.Error(w,
fmt.Sprintf(
"requested range %d-%d exceeds file size of %d bytes",
- rng.Start, rng.End, stat.Size),
+ rng.Start, rng.End, stat.Size,
+ ),
http.StatusRequestedRangeNotSatisfiable)
return
}
LoggerFromContext(r.Context()).V(3).Debugf(
"satisfiable range is %d bytes long, seeking to byte %d: %s",
- size, seek, respRange)
+ size, seek, respRange,
+ )
} else {
http.Error(w,
"multiple ranges are not supported",
"sync"
"time"
+ "gopkg.in/yaml.v3"
+
"go.fuhry.dev/runtime/mtls"
"go.fuhry.dev/runtime/utils/hashset"
"go.fuhry.dev/runtime/utils/log"
"go.fuhry.dev/runtime/utils/stringmatch"
- "gopkg.in/yaml.v3"
)
type serverCtxVar int
Handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}
+type Authorizer interface {
+ New(*yaml.Node) (AuthorizationHandler, error)
+}
+
+type AuthorizationHandler interface {
+ Check(w http.ResponseWriter, r *http.Request) (string, error)
+ Reject(w http.ResponseWriter, r *http.Request)
+}
+
type Route struct {
- Path stringmatch.StringMatcher
- Action RouteAction
+ Path stringmatch.StringMatcher
+ Authorizers []*authznEntry
+ Action RouteAction
}
type NameMatcher interface {
ProxyProtocol bool `yaml:"proxy_protocol"`
InsecureAddr string `yaml:"listen_insecure"`
Certificate string `yaml:"cert"`
+ MtlsID string `yaml:"mtls_id"`
+ Verify string `yaml:"verify"`
+ RequireCert string `yaml:"require_cert"`
TrustUpstreamRequestID bool `yaml:"trust_upstream_request_id"`
DrainTime time.Duration `yaml:"drain_time"`
VirtualHosts []VirtualHost `yaml:"-"`
Principal string
}
-type initHook func(context.Context, *yaml.Node) (context.Context, error)
-type routeParseFunc func(*yaml.Node) (RouteAction, error)
+type authznEntry struct {
+ name string
+ handler AuthorizationHandler
+}
+
+type (
+ initHook func(context.Context, *yaml.Node) (context.Context, error)
+ routeParseFunc func(*yaml.Node) (RouteAction, error)
+)
const (
kLogger serverCtxVar = iota
kSamlDefaults
kRequestID
kAuthorization
+ kTlsConn
+ kRemoteIdentity
)
const (
hostnameWildcardSingle = `[A-Za-z0-9-]+`
hostnameWildcardMulti = `[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*`
+
+ verifyTokenMtls = "mtls"
+ verifyTokenNone = "none"
)
const (
DefaultDrainTime = 30 * time.Second
)
-var ErrServerClosed = http.ErrServerClosed
-var randSrc = rand.New(rand.NewSource(time.Now().UnixNano()))
-var portSpec = regexp.MustCompile(":[0-9]{1,5}$")
-var domainComponentSpec = regexp.MustCompile("^" + hostnameWildcardSingle + "$")
-var initHooks []initHook
-var routeParseFuncs []routeParseFunc
+var (
+ ErrServerClosed = http.ErrServerClosed
+ ErrSSLCertAndMtlsIDMutex = errors.New("cert and mtls_id options are mutually exclusive")
+ randSrc = rand.New(rand.NewSource(time.Now().UnixNano()))
+ portSpec = regexp.MustCompile(":[0-9]{1,5}$")
+ domainComponentSpec = regexp.MustCompile("^" + hostnameWildcardSingle + "$")
+ initHooks []initHook
+ routeParseFuncs []routeParseFunc
+ authorizers map[string]Authorizer
+ authorizersOnce sync.Once
+)
func AddServerInitHook(hook initHook) {
initHooks = append(initHooks, hook)
routeParseFuncs = append(routeParseFuncs, rpf)
}
+func AddAuthorizer(name string, a Authorizer) {
+ authorizersOnce.Do(func() {
+ authorizers = make(map[string]Authorizer)
+ })
+
+ if _, ok := authorizers[name]; ok {
+ log.Panicf("authorizer %q already registered", name)
+ return
+ }
+
+ authorizers[name] = a
+}
+
func NewServer() *Server {
return NewServerWithContext(context.Background())
}
// UnmarshalYAML implements yaml.Unmarshaler
func (r *Route) UnmarshalYAML(node *yaml.Node) error {
var rawNode struct {
- Path *stringmatch.MatchRule `yaml:"path"`
+ Path *stringmatch.MatchRule `yaml:"path"`
+ Authorizers []string `yaml:"auth"`
}
if err := node.Decode(&rawNode); err != nil {
return errors.New("error unmarshaling route: path must be specified")
}
+ if len(rawNode.Authorizers) > 0 {
+ for _, azName := range rawNode.Authorizers {
+ authorizer, ok := authorizers[azName]
+ if !ok {
+ return fmt.Errorf("unknown authorizer: %q", azName)
+ }
+
+ handler, err := authorizer.New(node)
+ if err != nil {
+ return fmt.Errorf("error initializing authorizer %q: %v", azName, err)
+ }
+
+ r.Authorizers = append(r.Authorizers, &authznEntry{
+ name: azName,
+ handler: handler,
+ })
+ }
+ }
+
for _, rpf := range routeParseFuncs {
action, err := rpf(node)
if err != nil {
ProxyProtocol bool `yaml:"proxy_protocol"`
InsecureAddr string `yaml:"listen_insecure"`
Certificate string `yaml:"cert"`
+ MtlsID string `yaml:"mtls_id"`
+ Verify string `yaml:"verify"`
+ RequireCert string `yaml:"require_cert"`
TrustUpstreamRequestID bool `yaml:"trust_upstream_request_id"`
DrainTime string `yaml:"drain_time"`
}
l.ProxyProtocol = rawNode.ProxyProtocol
l.InsecureAddr = rawNode.InsecureAddr
l.Certificate = rawNode.Certificate
+ l.MtlsID = rawNode.MtlsID
+ l.Verify = rawNode.Verify
+ l.RequireCert = rawNode.RequireCert
l.TrustUpstreamRequestID = rawNode.TrustUpstreamRequestID
if rawNode.DrainTime != "" {
l.DrainTime = DefaultDrainTime
}
+ if rawNode.Certificate != "" && rawNode.MtlsID != "" {
+ return ErrSSLCertAndMtlsIDMutex
+ }
+
coveredVhosts := hashset.NewHashSet[string]()
if httpNode.HTTPVirtualHosts != nil {
if vhost.BaseVirtualHost == nil {
vhost.BaseVirtualHost = &BaseVirtualHost{}
}
+ if vhost.Verify == nil && l.Verify != "" {
+ vhost.Verify = &l.Verify
+ }
+ if vhost.RequireCert == nil && l.RequireCert != "" {
+ vhost.RequireCert = &l.RequireCert
+ }
coveredVhosts.Add(name)
vhost.Aliases = append(vhost.Aliases, name)
+ if vhost.Certificate == "" && vhost.MtlsID == "" {
+ vhost.Certificate = l.Certificate
+ vhost.MtlsID = l.MtlsID
+ }
l.VirtualHosts = append(l.VirtualHosts, vhost)
}
}
if coveredVhosts.Contains(name) {
return fmt.Errorf(
"virtual host %q contains both http routes and a SNI backend, this is not allowed",
- name)
+ name,
+ )
}
coveredVhosts.Add(name)
if coveredVhosts.Len() < len(httpNode.HTTPVirtualHosts) {
return fmt.Errorf(
"%d virtual hosts are unconfigured, specify either routes for HTTP or tcp_backend "+
- "for SNI proxying", len(httpNode.HTTPVirtualHosts)-coveredVhosts.Len())
+ "for SNI proxying", len(httpNode.HTTPVirtualHosts)-coveredVhosts.Len(),
+ )
}
return nil
}
+func (l *Listener) serverIdentity() (mtls.Identity, error) {
+ if l.Certificate != "" && l.MtlsID != "" {
+ return nil, ErrSSLCertAndMtlsIDMutex
+ }
+
+ var id mtls.Identity
+ if l.Certificate != "" {
+ id = mtls.NewSSLCertificate(l.Certificate)
+ } else if l.MtlsID != "" {
+ id = mtls.NewServiceIdentity(l.MtlsID)
+ } else {
+ return nil, nil
+ }
+
+ return id, nil
+}
+
// UnmarshalYAML implements yaml.Unmarshaler
func (s *Server) UnmarshalYAML(node *yaml.Node) error {
var lc struct {
for i, vhost := range listener.VirtualHosts {
switch vhost := vhost.(type) {
case *HTTPVirtualHost:
- if vhost.Certificate == "" {
+ if vhost.Certificate == "" && vhost.MtlsID == "" {
vhost.Certificate = listener.Certificate
+ vhost.MtlsID = listener.MtlsID
}
if vhost.TrustUpstreamRequestID == nil {
vhost.TrustUpstreamRequestID = &listener.TrustUpstreamRequestID
if err := vhost.precompileAliases(); err != nil {
return fmt.Errorf("bootstrapping sni vhost %d: %v", i, err)
}
- log.Default().Debugf("sni vhost %+v bootatrapped", vhost.Names())
+ log.Default().Debugf("sni vhost %+v bootstrapped", vhost.Names())
default:
return fmt.Errorf("unsupported vhost type: %T", vhost)
}
return fmt.Errorf("hot reload refused: listener %d: toggling proxy protocol is not supported", i)
}
+ oldId, err := oldL.serverIdentity()
+ if err != nil {
+ return fmt.Errorf("unable to get old listener server identity: %v", err)
+ }
+ newId, err := newL.serverIdentity()
+ if err != nil {
+ return fmt.Errorf("unable to get new listener server identity: %v", err)
+ }
+
oldL.TrustUpstreamRequestID = newL.TrustUpstreamRequestID
oldL.ExternalPort = newL.ExternalPort
oldL.DrainTime = newL.DrainTime
oldL.Certificate = newL.Certificate
+ oldL.MtlsID = newL.MtlsID
// virtual hosts that need to be started or closed
var (
continue
}
+ // copy all properties from the new vhost to the old one
switch oldVh := vh.(type) {
case *HTTPVirtualHost:
newVh := match.(*HTTPVirtualHost)
oldVh.Routes = newVh.Routes
oldVh.TrustUpstreamRequestID = newVh.TrustUpstreamRequestID
- if oldVh.Certificate != newVh.Certificate {
- newCert := mtls.NewSSLCertificate(newVh.Certificate)
- if tlsConfig, err := newCert.TlsConfig(listenerCtx); err == nil {
+ oldVhostId, err := oldVh.vhostIdentity()
+ logger := LoggerFromContext(listenerCtx)
+ if err != nil {
+ logger.Errorf("error getting identity of old vhost %v: %v", vh.Names(), err)
+ break
+ }
+ newVhostId, err := newVh.vhostIdentity()
+ if err != nil {
+ logger.Errorf("error getting identity of new vhost %v: %v", vh.Names(), err)
+ break
+ }
+ if oldVhostId == nil {
+ if oldId == nil {
+ return errors.New("neither cert nor mtls_id specified for old server")
+ }
+ oldVhostId = oldId
+ }
+ if newVhostId == nil {
+ if oldId == nil {
+ return errors.New("neither cert nor mtls_id specified for new server")
+ }
+ newVhostId = newId
+ }
+
+ verifyChanged := false
+ if newVh.Verify != nil {
+ if oldVh.Verify == nil || *oldVh.Verify != *newVh.Verify {
+ oldVh.Verify = newVh.Verify
+ verifyChanged = true
+ }
+ } else if newL.Verify != verifyTokenNone && newL.Verify != "" {
+ if oldVh.Verify != nil || (*oldVh.Verify != newL.Verify) {
+ oldVh.Verify = &newL.Verify
+ verifyChanged = true
+ }
+ } else {
+ if oldVh.Verify != nil && *oldVh.Verify != verifyTokenNone {
+ oldVh.Verify = nil
+ verifyChanged = true
+ }
+ }
+
+ if !oldVhostId.Equals(newVhostId) || verifyChanged {
+ if tlsConfig, tlsVerifier, err := newVh.newTlsConfig(listenerCtx); err == nil {
oldVh.swapTlsConfig(tlsConfig)
+ oldVh.Certificate = newVh.Certificate
+ oldVh.MtlsID = newVh.MtlsID
+ oldVh.tlsVerifier = tlsVerifier
} else {
- LoggerFromContext(listenerCtx).Errorf(
+ logger.Errorf(
"vhost %+v: error loading new ssl certificate %q: %v",
- oldVh.Names(), newVh.Certificate, err)
+ oldVh.Names(), newVh.Certificate, err,
+ )
}
}
- LoggerFromContext(listenerCtx).Debugf(
+ logger.Debugf(
"loaded %d new routes for HTTP vhost %+v",
- len(oldVh.Routes), oldVh.Names())
+ len(oldVh.Routes), oldVh.Names(),
+ )
case *SNIVirtualHost:
newVh := match.(*SNIVirtualHost)
oldVh.Backend = newVh.Backend
if err != nil {
LoggerFromContext(listenerCtx).Warningf(
"failed to start vhost(%T, %+v): %v",
- vhost, vhost.Names(), err)
+ vhost, vhost.Names(), err,
+ )
continue
}
oldL.wg.Add(1)
- go (func() {
+ go func() {
defer oldL.wg.Done()
serveFunc()
- })()
+ }()
oldL.VirtualHosts = append(oldL.VirtualHosts, vhost)
if err := vhost.Shutdown(shutdownCtx); err != nil {
LoggerFromContext(listenerCtx).Warningf(
"failed to shutdown vhost(%T, %+v): %v",
- vhost, vhost.Names(), err)
+ vhost, vhost.Names(), err,
+ )
}
LoggerFromContext(listenerCtx).Debugf("shut down removed vhost %+v", vhost.Names())
func (s *Server) ListenAndServeTLS() (err error) {
s.servingTLS = true
- defer (func() { s.servingTLS = false })()
+ defer func() { s.servingTLS = false }()
wg := &sync.WaitGroup{}
for _, listener := range s.Listeners {
listenerCtx := context.WithValue(s.Context, kListener, listener)
wg.Add(1)
- go (func() {
+ go func() {
defer wg.Done()
err = listener.ListenAndServeSNI(listenerCtx)
- })()
+ }()
}
wg.Wait()
func (s *Server) ListenAndServe() (err error) {
s.servingHTTP = true
- defer (func() { s.servingHTTP = false })()
+ defer func() { s.servingHTTP = false }()
wg := &sync.WaitGroup{}
for _, listener := range s.Listeners {
listener.httpServer = server
wg.Add(1)
- go (func() {
+ go func() {
defer wg.Done()
err = server.Serve(netListener)
- })()
+ }()
}
wg.Wait()
if listener.httpServer != nil {
wg.Add(1)
- go (func() {
+ go func() {
defer wg.Done()
err = listener.httpServer.Shutdown(shutdownCtx)
- })()
+ }()
}
if listener.sniServer != nil {
wg.Add(1)
- go (func() {
+ go func() {
defer wg.Done()
err = listener.sniServer.Close()
- })()
+ }()
for _, vhost := range listener.VirtualHosts {
wg.Add(1)
- go (func() {
+ go func() {
defer wg.Done()
err = vhost.Shutdown(shutdownCtx)
- })()
+ }()
}
}
}
}
l.wg.Add(1)
- go (func() {
+ go func() {
defer l.wg.Done()
serveFunc()
- })()
+ }()
}
l.wg.Add(1)
- go (func() {
+ go func() {
defer l.wg.Done()
err = l.sniServer.Serve()
- })()
+ }()
return err
}
lm := log.NewLoggingMiddlewareWithLogger(
http.HandlerFunc(l.handleRedirectToHTTPS),
- logger.AppendPrefix(".access").WithLevel(log.INFO))
+ logger.AppendPrefix(".access").WithLevel(log.INFO),
+ )
server := &http.Server{
Addr: l.InsecureAddr,
return nil
}
+func VirtualHostFromContext(ctx context.Context) VirtualHost {
+ vh := ctx.Value(kVirtualHost)
+ if vh, ok := vh.(VirtualHost); ok {
+ return vh
+ }
+
+ return nil
+}
+
func AuthorizationFromContext(ctx context.Context) *Authorization {
a := log.StateFromContext(ctx).Get(kAuthorization)
if authorization, ok := a.(*Authorization); ok {
l.logger.Errorf(
"permanent error calling Accept(), propagating to vhosts: %v",
- netErr)
+ netErr,
+ )
} else {
l.logger.Errorf(
"non-net.Error of type %T calling Accept(), propagating to vhosts: %v",
- err, err)
+ err, err,
+ )
}
l.vhostsMu.Lock()
defer l.vhostsMu.Unlock()
logger.V(1).Infof(
"new conn matched vhost %+v for name %q, dispatch",
vhost.matcher.Names(),
- clientHello.ServerName)
+ clientHello.ServerName,
+ )
vhost.msg(childConn, nil)
return
}
}
}
+func SplitChain(certs []*x509.Certificate) (leaf *x509.Certificate, chain []*x509.Certificate) {
+ for _, cert := range certs {
+ if cert.IsCA {
+ chain = append(chain, cert)
+ } else {
+ leaf = cert
+ }
+ }
+
+ return
+}
+
+func SplitChainRoot(certs []*x509.Certificate) (leaf *x509.Certificate, chain []*x509.Certificate, root *x509.Certificate) {
+ for _, cert := range certs {
+ if cert.IsCA {
+ if IsSelfSigned(cert) {
+ root = cert
+ } else {
+ chain = append(chain, cert)
+ }
+ } else {
+ leaf = cert
+ }
+ }
+
+ return
+}
+
+func IsSelfSigned(cert *x509.Certificate) bool {
+ if cert.Issuer.ToRDNSequence().String() != cert.Subject.ToRDNSequence().String() {
+ return false
+ }
+
+ return cert.CheckSignatureFrom(cert) == nil
+}
+
func LoadPrivateKeyFromPEM(path string) (crypto.PrivateKey, error) {
contents, err := os.ReadFile(path)
if err != nil {
var _ Identity = &stubIdentity{}
-type IdentityLoaderFunc func(cls PrincipalClass, name string) (CertificateProvider, error)
-
-type identityDriver struct {
- name string
- load IdentityLoaderFunc
-}
-
-var identityDrivers []*identityDriver
-
var ErrUnsupportedClass = errors.New("this driver does not support loading this identity class")
-func RegisterIdentityDriver(name string, load IdentityLoaderFunc) {
- driver := &identityDriver{
- name: name,
- load: load,
- }
-
- identityDrivers = append(identityDrivers, driver)
-}
+type IdentityLoaderFunc func(cls PrincipalClass, name string) (CertificateProvider, error)
func (id *substantiatedIdentity) Name() string {
cert, err := id.CertificateProvider.LeafCertificate()
"go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/mtls/fsnotify"
"go.fuhry.dev/runtime/utils/fsutil"
+ "go.fuhry.dev/runtime/utils/hashset"
"go.fuhry.dev/runtime/utils/subst"
)
type FileBackedCertificate struct {
+ obj SwappableCert
+
LeafPath string
IntermediatesPath string
PrivateKeyPath string
mu sync.Mutex
tlsConfig *tls.Config
- leaf *x509.Certificate
- ints []*x509.Certificate
- pkey crypto.PrivateKey
- root *x509.Certificate
+ subs map[SubscriptionHandle]ReloadNotifyFunc
}
+var (
+ _ CertificateProvider = &FileBackedCertificate{}
+ _ CertificateNotifyOnReload = &FileBackedCertificate{}
+)
+
type fileBackedRoots struct {
RootPath string
IntermediatesPath string
IntermediatesPath: path.Join(k8sSpiffeDir, c.leaf),
PrivateKeyPath: path.Join(k8sSpiffeDir, c.key),
RootPath: path.Join(k8sSpiffeDir, c.ca),
+ obj: NewEmptySwappableCert(),
}
if cert, err := fbc.LeafCertificate(); err == nil {
return nil, fmt.Errorf("cannot find files for service identity %q in %q", serviceIdentity, k8sSpiffeDir)
}
+func loadCertificatesFromPEMs(paths ...string) []*x509.Certificate {
+ pathsSet := hashset.FromSlice(paths)
+
+ var out []*x509.Certificate
+ outSet := hashset.NewHashSet[string]()
+
+ for _, f := range pathsSet.AsSlice() {
+ contents, err := certutil.LoadCertificatesFromPEM(f)
+ if err == nil {
+ for _, cert := range contents {
+ raw := string(cert.Raw)
+ if outSet.Contains(raw) {
+ continue
+ }
+ outSet.Add(raw)
+
+ out = append(out, cert)
+ }
+ }
+ }
+
+ return out
+}
+
func newFileBackedCertificateFromBaseDir(mtlsRootPath string, serviceIdentity string) (*FileBackedCertificate, error) {
certDirectory := path.Join(mtlsRootPath, serviceIdentity)
logger.V(2).Errorf(
"cannot load identity %q from %s: cannot read from either possible leaf "+
"certificate path:\n %s: %v\n %s: %v",
- serviceIdentity, certDirectory, leafPath, leafErr, fullchainPath, err)
+ serviceIdentity, certDirectory, leafPath, leafErr, fullchainPath, err,
+ )
return nil, err
}
}
logger.V(2).Errorf(
"cannot load identity %q from %s: cannot read from either possible "+
"intermediate chain path:\n %s: %v\n %s: %v",
- serviceIdentity, certDirectory, chainPath, chainErr, fullchainPath, err)
+ serviceIdentity, certDirectory, chainPath, chainErr, fullchainPath, err,
+ )
}
}
for _, file := range []string{keyPath, rootPath} {
}
}
+ obj := NewEmptySwappableCert()
+ certs := loadCertificatesFromPEMs(leafPath, chainPath, rootPath)
+ leaf, ints, root := certutil.SplitChainRoot(certs)
+ pkey, err := certutil.LoadPrivateKeyFromPEM(keyPath)
+ if leaf != nil && ints != nil && root != nil && pkey != nil && err == nil {
+ obj = NewSwappableCert(leaf, pkey, ints, root)
+ }
+
return &FileBackedCertificate{
LeafPath: leafPath,
IntermediatesPath: chainPath,
PrivateKeyPath: keyPath,
RootPath: rootPath,
+ obj: obj,
}, nil
}
IntermediatesPath: fullChainPath,
PrivateKeyPath: keyPath,
RootPath: rootPath,
+ obj: NewEmptySwappableCert(),
}
leaf, err := cert.LeafCertificate()
IntermediatesPath: chainPath,
PrivateKeyPath: keyPath,
RootPath: rootPath,
+ obj: NewEmptySwappableCert(),
}, nil
}
c.mu.Lock()
defer c.mu.Unlock()
- if c.leaf != nil {
- return c.leaf, nil
+ if leaf := c.obj.LiveLeaf(); leaf != nil {
+ return leaf, nil
}
leaf, err := c.tryLoadLeaf()
return nil, err
}
- c.leaf = leaf
+ c.obj.ReplaceLeaf(leaf)
return leaf, nil
}
return nil, err
}
- for _, cert := range certs {
- if cert.IsCA {
- continue
- }
-
- return cert, nil
+ leaf, _ := certutil.SplitChain(certs)
+ if leaf == nil {
+ return nil, fmt.Errorf("leaf certificate path %q contains no end-entity certificates", c.LeafPath)
}
- return nil, fmt.Errorf("leaf certificate path %q contains no end-entity certificates", c.LeafPath)
+ return leaf, nil
}
func (c *FileBackedCertificate) PrivateKey() (crypto.PrivateKey, error) {
c.mu.Lock()
defer c.mu.Unlock()
- if c.pkey != nil {
- return c.pkey, nil
+ if pkey := c.obj.LiveKey(); pkey != nil {
+ return pkey, nil
}
pkey, err := c.tryLoadPrivateKey()
return nil, err
}
- c.pkey = pkey
- return c.pkey, nil
+ c.obj.ReplaceKey(pkey)
+ return pkey, nil
}
func (c *FileBackedCertificate) tryLoadPrivateKey() (crypto.PrivateKey, error) {
c.mu.Lock()
defer c.mu.Unlock()
- if c.root != nil {
- return c.root, nil
+ if root := c.obj.LiveRoot(); root != nil {
+ return root, nil
}
root, err := c.tryLoadRoot()
return nil, err
}
- c.root = root
+ c.obj.ReplaceRoot(root)
return root, nil
}
c.mu.Lock()
defer c.mu.Unlock()
- if c.ints != nil {
- return c.ints, nil
+ if ints := c.obj.LiveIntermediates(); ints != nil {
+ return ints, nil
}
newInts, err := c.tryLoadIntermediates()
return nil, err
}
- c.ints = newInts
- return c.ints, nil
+ c.obj.ReplaceIntermediates(newInts)
+ return newInts, nil
}
func (c *FileBackedCertificate) tryLoadIntermediates() ([]*x509.Certificate, error) {
if err != nil {
return nil, err
}
- newInts := make([]*x509.Certificate, 0)
+ _, newInts := certutil.SplitChain(certs)
- for _, cert := range certs {
- if !cert.IsCA {
- continue
- }
+ // don't check length of newInts here - sometimes there may not be any
+ return newInts, nil
+}
- newInts = append(newInts, cert)
+func (c *FileBackedCertificate) Subscribe(f ReloadNotifyFunc) SubscriptionHandle {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.subs == nil {
+ c.subs = make(map[SubscriptionHandle]ReloadNotifyFunc)
}
- // don't check length of newInts here - sometimes there may not be any
- return newInts, nil
+ h := NewSubscriptionHandle()
+ c.subs[h] = f
+ return h
+}
+
+func (c *FileBackedCertificate) Unsubscribe(h SubscriptionHandle) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.subs == nil {
+ return
+ }
+
+ delete(c.subs, h)
+}
+
+func (c *FileBackedCertificate) notifyWatchers() {
+ // copy the live state
+ leaf, pkey, ints, root := c.obj.LiveState()
+ if leaf == nil || ints == nil || pkey == nil {
+ return
+ }
+
+ // copy the notify handlers
+ c.mu.Lock()
+ if c.subs == nil {
+ c.mu.Unlock()
+ return
+ }
+
+ var watchers []ReloadNotifyFunc
+ for _, f := range c.subs {
+ watchers = append(watchers, f)
+ }
+ c.mu.Unlock()
+
+ for _, f := range watchers {
+ f(leaf, pkey, ints, root)
+ }
}
func (r *fileBackedRoots) init() {
cert, err := c.tryLoadLeaf()
if err == nil {
logger.Infof("detected change to leaf certificate %s, reloaded", c.LeafPath)
- c.leaf = cert
+ c.obj.ReplaceLeaf(cert)
} else {
logger.Warningf("leaf certificate %s changed but unable to reload: %v", c.LeafPath, err)
}
ints, err := c.tryLoadIntermediates()
if err == nil {
logger.Infof("detected change to intermediate certificates %s, reloaded", c.IntermediatesPath)
- c.ints = ints
+ c.obj.ReplaceIntermediates(ints)
} else {
logger.Warningf("intermediate certificates %s changed but unable to reload: %v", c.IntermediatesPath, err)
}
pkey, err := c.tryLoadPrivateKey()
if err == nil {
logger.Infof("detected change to private key %s, reloaded", c.PrivateKeyPath)
- c.pkey = pkey
+ c.obj.ReplaceKey(pkey)
} else {
logger.Warningf("private key %s changed but unable to reload: %v", c.PrivateKeyPath, err)
}
root, err := c.tryLoadRoot()
if err == nil {
logger.Infof("detected change to root certificate %s, reloaded", c.RootPath)
- c.root = root
+ c.obj.ReplaceRoot(root)
} else {
logger.Warningf("root certificate %s changed but unable to reload: %v", c.RootPath, err)
}
PrivateKeyPath: key,
IntermediatesPath: chain,
RootPath: root,
+ obj: NewEmptySwappableCert(),
}
if err := validateIdentity(fbc); err != nil {
logger.V(2).Noticef("loaded certificate failed validation: %v", err)
return nil, fmt.Errorf(
- "loaded certificate is not valid: %v", err)
+ "loaded certificate is not valid: %v", err,
+ )
}
id := &substantiatedIdentity{fbc}
if id.Class() != cls || id.Name() != princ {
return nil, fmt.Errorf(
"loaded identity did not match the one requested:\n requested: %s %s\n loaded: %s %s",
- cls.String(), princ, id.Class().String(), id.Name())
+ cls.String(), princ, id.Class().String(), id.Name(),
+ )
}
logger.V(2).Infof(
"fileProviderFactory: loaded %s identity %q:\n"+
" leaf: %s\n key: %s\n chain: %s\n root: %s",
- cls.String(), princ, leaf, key, chain, root)
+ cls.String(), princ, leaf, key, chain, root,
+ )
return fbc, nil
}, nil
flag.StringVar(&sslCertsBaseDir, "tls.certs-dir", sslCertsBaseDir, "directory to look under for public-site SSL certificates (NOT mTLS certs)")
flag.Func("mtls.certs-dir", "additional directory to search for mTLS certificates", appendMtlsCertificateDir)
- RegisterIdentityDriver("file_service_global", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
- if cls != ServicePrincipal {
- return nil, ErrUnsupportedClass
- }
- return LoadServiceIdentityFromFilesystem(serviceName)
- })
- RegisterIdentityDriver("file_service_csi_spiffe", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
- if cls != ServicePrincipal {
- return nil, ErrUnsupportedClass
- }
- return LoadServiceIdentityFromKubernetesCSIDriverSPIFFE(serviceName)
- })
- RegisterIdentityDriver("file_user_home", func(cls PrincipalClass, username string) (CertificateProvider, error) {
- if cls != UserPrincipal {
- return nil, ErrUnsupportedClass
- }
- return LoadUserIdentityFromFilesystem(username)
- })
RegisterRootDriver("file_etc_mtls", func() (RootsPrimitive, error) {
return defaultFileBackedRoots, nil
})
"crypto/tls"
"crypto/x509"
"errors"
+ "math/rand"
"net"
)
NewTlsCertificate() (*tls.Certificate, error)
}
+type ReloadNotifyFunc func(leaf *x509.Certificate, pkey crypto.PrivateKey, ints []*x509.Certificate, root *x509.Certificate)
+
+type SubscriptionHandle int64
+
+type CertificateNotifyOnReload interface {
+ Subscribe(ReloadNotifyFunc) SubscriptionHandle
+ Unsubscribe(SubscriptionHandle)
+}
+
type inaccessibleCertificate struct{}
var ErrCertificateInaccessible = errors.New("requested certificate is inaccessible")
func (c *inaccessibleCertificate) NewDialContextFunc() DialContextFunc {
return MakeDialContextFunc(c)
}
+
+func NewSubscriptionHandle() SubscriptionHandle {
+ return SubscriptionHandle(rand.Int63())
+}
+
+type certObject struct {
+ leaf *x509.Certificate
+ pkey crypto.PrivateKey
+ ints []*x509.Certificate
+ root *x509.Certificate
+}
func init() {
kcLogger = log.WithPrefix("mtls.macOSKeychain")
- RegisterIdentityDriver("macos_keychain", NewCertificateFromMacKeychain)
-
RegisterProviderFactory("macos_keychain", &macosKeychainProviderFactory{})
RegisterRootDriver("macos_keychain", func() (RootsPrimitive, error) {
import (
"context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
"crypto/tls"
+ "crypto/x509"
"net"
+ "sync"
)
+// SwappableCert wraps a leaf certificate, intermediates, root, and private key.
+//
+// The `Live*` methods copy the respective properties before returning them.
+//
+// The `Replace*` methods are concurrency-safe. Each accepts the respective new property
+// and constructs a pending state from each of the four properties. If the properties
+// align to build a complete chain including a leaf certificate corresponding to the
+// private key, the live properties are updated. Otherwise, the replaced property is
+// stored in the pending state.
+type SwappableCert interface {
+ LiveLeaf() *x509.Certificate
+ LiveKey() crypto.PrivateKey
+ LiveIntermediates() []*x509.Certificate
+ LiveRoot() *x509.Certificate
+
+ // LiveState returns an atomic snapshot of the live state.
+ LiveState() (*x509.Certificate, crypto.PrivateKey, []*x509.Certificate, *x509.Certificate)
+
+ ReplaceLeaf(*x509.Certificate)
+ ReplaceKey(crypto.PrivateKey)
+ ReplaceIntermediates([]*x509.Certificate)
+ ReplaceRoot(*x509.Certificate)
+}
+
+type swappableCertObject struct {
+ live certObject
+ pending certObject
+ mu sync.Mutex
+}
+
+var _ SwappableCert = &swappableCertObject{}
+
+type hasPublic interface {
+ Public() crypto.PublicKey
+}
+
+type comparablePublicKey interface {
+ Equal(crypto.PublicKey) bool
+}
+
func MakeTlsCertificate(id CertificatePrimitive) (*tls.Certificate, error) {
leafCertificate, err := id.LeafCertificate()
if err != nil {
return dcf
}
+
+func checkAlignment(cert *x509.Certificate, ints []*x509.Certificate, root *x509.Certificate, key crypto.PrivateKey) bool {
+ // ints may be nil,
+ if cert == nil || key == nil || root == nil {
+ return false
+ }
+
+ privkey, ok := key.(hasPublic)
+ if !ok {
+ return false
+ }
+
+ certPub, ok := cert.PublicKey.(comparablePublicKey)
+ if !ok {
+ return false
+ }
+
+ if !certPub.Equal(privkey.Public()) {
+ return false
+ }
+
+ rootPool := x509.NewCertPool()
+ rootPool.AddCert(root)
+ intsPool := x509.NewCertPool()
+ for _, i := range ints {
+ intsPool.AddCert(i)
+ }
+ vo := x509.VerifyOptions{
+ Roots: rootPool,
+ Intermediates: intsPool,
+ }
+
+ _, err := cert.Verify(vo)
+ return err == nil
+}
+
+func NewEmptySwappableCert() SwappableCert {
+ return &swappableCertObject{}
+}
+
+func NewSwappableCert(leaf *x509.Certificate, pkey crypto.PrivateKey, ints []*x509.Certificate, root *x509.Certificate) SwappableCert {
+ return &swappableCertObject{
+ live: certObject{leaf, pkey, ints, root},
+ }
+}
+
+func (s *swappableCertObject) pendingLeaf() *x509.Certificate {
+ if s.pending.leaf != nil {
+ return s.pending.leaf
+ }
+
+ return s.live.leaf
+}
+
+func (s *swappableCertObject) pendingKey() crypto.PrivateKey {
+ if s.pending.pkey != nil {
+ return s.pending.pkey
+ }
+
+ return s.live.pkey
+}
+
+func (s *swappableCertObject) pendingIntermediates() []*x509.Certificate {
+ if s.pending.ints != nil {
+ return s.pending.ints
+ }
+
+ return s.live.ints
+}
+
+func (s *swappableCertObject) pendingRoot() *x509.Certificate {
+ if s.pending.root != nil {
+ return s.pending.root
+ }
+
+ return s.live.root
+}
+
+func (s *swappableCertObject) LiveLeaf() *x509.Certificate {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.live.leaf == nil {
+ return nil
+ }
+ leafCopy := *s.live.leaf
+ return &leafCopy
+}
+
+func (s *swappableCertObject) LiveKey() crypto.PrivateKey {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.live.pkey == nil {
+ return nil
+ }
+
+ switch k := s.live.pkey.(type) {
+ case *rsa.PrivateKey:
+ kCopy := *k
+ return &kCopy
+ case *ecdsa.PrivateKey:
+ kCopy := *k
+ return &kCopy
+ case *ed25519.PrivateKey:
+ kCopy := *k
+ return &kCopy
+ }
+
+ // fall back to not copying... not ideal
+ return s.live.pkey
+}
+
+func (s *swappableCertObject) LiveIntermediates() []*x509.Certificate {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.live.ints == nil {
+ return nil
+ }
+
+ intsCopy := make([]*x509.Certificate, len(s.live.ints))
+ for i, c := range s.live.ints {
+ cCopy := *c
+ intsCopy[i] = &cCopy
+ }
+ return intsCopy
+}
+
+func (s *swappableCertObject) LiveRoot() *x509.Certificate {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.live.root == nil {
+ return nil
+ }
+
+ rootCopy := *s.live.root
+ return &rootCopy
+}
+
+func (s *swappableCertObject) LiveState() (leaf *x509.Certificate, pkey crypto.PrivateKey, ints []*x509.Certificate, root *x509.Certificate) {
+ leafCopy := *s.live.leaf
+ leaf = &leafCopy
+
+ switch k := s.live.pkey.(type) {
+ case *rsa.PrivateKey:
+ kCopy := *k
+ pkey = kCopy
+ case *ecdsa.PrivateKey:
+ kCopy := *k
+ pkey = kCopy
+ case *ed25519.PrivateKey:
+ kCopy := *k
+ pkey = kCopy
+ default:
+ // fall back to not copying
+ pkey = s.live.pkey
+ }
+
+ ints = make([]*x509.Certificate, len(s.live.ints))
+ for i, c := range s.live.ints {
+ cCopy := *c
+ ints[i] = &cCopy
+ }
+
+ rootCopy := *s.live.root
+ root = &rootCopy
+
+ return
+}
+
+func (s *swappableCertObject) ReplaceLeaf(cert *x509.Certificate) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ pkey := s.pendingKey()
+ ints := s.pendingIntermediates()
+ root := s.pendingRoot()
+
+ if checkAlignment(cert, ints, root, pkey) {
+ s.live.leaf, s.live.ints, s.live.root, s.pending.pkey = cert, ints, root, pkey
+ s.pending = certObject{}
+ } else {
+ s.pending.leaf = cert
+ }
+}
+
+func (s *swappableCertObject) ReplaceKey(pkey crypto.PrivateKey) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ cert := s.pendingLeaf()
+ ints := s.pendingIntermediates()
+ root := s.pendingRoot()
+
+ if checkAlignment(cert, ints, root, pkey) {
+ s.live.leaf, s.live.ints, s.live.root, s.pending.pkey = cert, ints, root, pkey
+ s.pending = certObject{}
+ } else {
+ s.pending.pkey = pkey
+ }
+}
+
+func (s *swappableCertObject) ReplaceIntermediates(ints []*x509.Certificate) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ cert := s.pendingLeaf()
+ pkey := s.pendingKey()
+ root := s.pendingRoot()
+
+ if checkAlignment(cert, ints, root, pkey) {
+ s.live.leaf, s.live.ints, s.live.root, s.pending.pkey = cert, ints, root, pkey
+ s.pending = certObject{}
+ } else {
+ s.pending.pkey = pkey
+ }
+}
+
+func (s *swappableCertObject) ReplaceRoot(root *x509.Certificate) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ cert := s.pendingLeaf()
+ pkey := s.pendingKey()
+ ints := s.pendingIntermediates()
+
+ if checkAlignment(cert, ints, root, pkey) {
+ s.live.leaf, s.live.ints, s.live.root, s.pending.pkey = cert, ints, root, pkey
+ s.pending = certObject{}
+ } else {
+ s.pending.pkey = pkey
+ }
+}
}
func init() {
- RegisterIdentityDriver("tpm2_pkcs11", func(cls PrincipalClass, serviceName string) (CertificateProvider, error) {
- if cls != ServicePrincipal || serviceName != constants.DeviceTrustPrincipal {
- return nil, ErrUnsupportedClass
- }
- return NewTPMBackedCertificate()
- })
-
RegisterProviderFactory("tpm2_pkcs11", &tpmProviderFactory{})
}
import (
"crypto/tls"
"crypto/x509"
+ "errors"
"fmt"
+ "net"
"net/url"
"regexp"
"strings"
+ "gopkg.in/yaml.v3"
+
+ "go.fuhry.dev/runtime/mtls/certutil"
"go.fuhry.dev/runtime/utils/hashset"
"go.fuhry.dev/runtime/utils/log"
)
Principal string
}
+type verifyYamlRule struct {
+ User string `yaml:"user"`
+ Service string `yaml:"service"`
+ Domain string `yaml:"domain"`
+}
+
+type VerifyYamlConfig struct {
+ AllowedPrincipals []*verifyYamlRule `yaml:"allowed_principals"`
+}
+
const (
Domain VerifyMatchBy = iota
Service
exprSpiffeServiceIdentity = `^spiffe://(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/service/(?P<principal>[a-z0-9_-]+)$`
exprSpiffeUserIdentity = `^spiffe://(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)/user/(?P<principal>[a-z0-9_-]+)$`
exprMtlsInternalIdenitty = `^(?P<principal>[A-Za-z0-9_-]+)\.(?P<domain>[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)\.mtls\.internal$`
+ exprDomain = `^\.?([a-z0-9-]+)(\.[a-z0-9-]+)*$`
+ exprUserOrService = `^[a-z0-9_-]+$`
+ tokenWildcard = `*`
)
func (c VerifyMatchBy) String() string {
panic("invalid value for IdentityClass")
}
-var spiffeServiceIdentity, spiffeUserIdentity, mtlsInternalIdentity *regexp.Regexp
+var spiffeServiceIdentity,
+ spiffeUserIdentity,
+ mtlsInternalIdentity,
+ aclDomainExpr,
+ aclUserOrServiceExpr *regexp.Regexp
func init() {
spiffeServiceIdentity = regexp.MustCompile(exprSpiffeServiceIdentity)
spiffeUserIdentity = regexp.MustCompile(exprSpiffeUserIdentity)
mtlsInternalIdentity = regexp.MustCompile(exprMtlsInternalIdenitty)
+ aclDomainExpr = regexp.MustCompile(exprDomain)
+ aclUserOrServiceExpr = regexp.MustCompile(exprUserOrService)
}
// MTLSPeerVerifier validates SPIFFE CNs and SANs of peer certificates, and facilitates
// configuration of conventional certificate validation (chain, notbefore/notafter, key
// usage, etc.) against the mTLS CA.
//
-// It will not configure the local identity's certificates, private keys, etc. - for that,
-// use `Identity.TlsConfig()`.
+// Identities may be verified at the time of connection using ConfigureClient/ConfigureServer, or
+// later by setting `AllowFrom(All)` and creating a separate verifier for checking names using
+// application-layer information (web route, etc.).
+//
+// MTLSPeerVerifier does not configure the local identity's certificates, private keys, etc. - for
+// that, use `Identity.TlsConfig()`.
type MTLSPeerVerifier interface {
// ConfigureServer modifies a tls.Config struct to require client certificates, and verify
// the client certificate using the custom chain and name verifiers.
//
// This function is called after VerifyPeerCertificate completes without error.
VerifyPeerCert(*x509.Certificate, x509.VerifyOptions) error
+
+ // GetPeerIdentity attempts to extract the peer identity from a tls.Conn.
+ GetPeerIdentity(net.Conn) (*RemoteIdentity, error)
+
+ // MatchRemoteIdentity returns nil if the provided RemoteIdentity is allow-listed.
+ MatchRemoteIdentity(*RemoteIdentity) error
}
type mtlsPeerVerifier struct {
- MTLSPeerVerifier
-
allowedPrincipals map[VerifyMatchBy]*hashset.HashSet[string]
log log.Logger
}
+var _ MTLSPeerVerifier = &mtlsPeerVerifier{}
+
func NewPeerNameVerifier() MTLSPeerVerifier {
cv := &mtlsPeerVerifier{
allowedPrincipals: make(map[VerifyMatchBy]*hashset.HashSet[string]),
return ErrNoCertificatePresented
}
- peerCert := cs.PeerCertificates[0]
+ peerCert, intermediates := certutil.SplitChain(cs.PeerCertificates)
+ verifyOptsCopy := verifyOpts
+ intermediatesPool := verifyOpts.Intermediates.Clone()
+ for _, i := range intermediates {
+ intermediatesPool.AddCert(i)
+ }
+ verifyOptsCopy.Intermediates = intermediatesPool
- return cv.VerifyPeerCert(peerCert, verifyOpts)
+ return cv.VerifyPeerCert(peerCert, verifyOptsCopy)
}
}
func (cv *mtlsPeerVerifier) VerifyPeerCert(peerCert *x509.Certificate, verifyOpts x509.VerifyOptions) error {
+ if peerCert == nil {
+ return ErrNoCertificatePresented
+ }
+
_, err := peerCert.Verify(verifyOpts)
if err != nil {
return err
}
}
-func (cv *mtlsPeerVerifier) checkName(name string) error {
- id, err := ParseRemoteIdentity(name)
- if err != nil {
- return err
- }
- cv.log.V(3).Debugf("parsed name %q to %+v (class=%s)", name, id, id.Class.String())
-
+func (cv *mtlsPeerVerifier) MatchRemoteIdentity(rid *RemoteIdentity) error {
if _, ok := cv.allowedPrincipals[All]; ok {
return nil
}
if allowedDomains, ok := cv.allowedPrincipals[Domain]; ok {
- if allowedDomains.Contains(id.Domain) {
- cv.log.V(3).Debugf("domain %q exactly matched allowlist", id.Domain)
- return nil
- }
domainOk := false
- for _, val := range allowedDomains.AsSlice() {
- if val[0] != '.' {
- continue
- }
- if strings.HasSuffix(id.Domain, val) {
- cv.log.V(3).Debugf("domain %q matched suffix rule %q", id.Domain, val)
- domainOk = true
- break
+ if allowedDomains.Contains("*") {
+ cv.log.V(3).Debug("list contains '*' entry; all domains allowed")
+ domainOk = true
+ } else if allowedDomains.Contains(rid.Domain) {
+ cv.log.V(3).Debugf("domain %q exactly matched allowlist", rid.Domain)
+ domainOk = true
+ } else {
+ for _, val := range allowedDomains.AsSlice() {
+ if val[0] == '.' && strings.HasSuffix(rid.Domain, val) {
+ cv.log.V(3).Debugf("domain %q matched suffix rule %q", rid.Domain, val)
+ domainOk = true
+ break
+ }
}
}
if !domainOk {
- return fmt.Errorf("trust domain %q is not allowed to authenticate to this service", id.Domain)
+ return fmt.Errorf("trust domain %q is not allowed to authenticate to this service", rid.Domain)
}
}
- if allowedPrincipals, ok := cv.allowedPrincipals[id.Class]; ok {
- if !allowedPrincipals.Contains(id.Principal) {
- return fmt.Errorf("principal %q is not allowed to authenticate to this service", id.Principal)
+ if allowedPrincipals, ok := cv.allowedPrincipals[rid.Class]; ok {
+ if allowedPrincipals.Contains("*") {
+ cv.log.V(3).Debugf("list contains '*' entry; all principals of type %s are allowed", rid.Class.String())
+ }
+
+ if !allowedPrincipals.Contains(rid.Principal) {
+ return fmt.Errorf("principal %q is not allowed to authenticate to this service", rid.Principal)
}
- cv.log.V(3).Debugf("principal %q matched allowlist", id.Principal)
+ cv.log.V(3).Debugf("principal %q matched allowlist", rid.Principal)
return nil
}
return fmt.Errorf("principals of this type are not allowed")
}
+func (cv *mtlsPeerVerifier) checkName(name string) error {
+ id, err := ParseRemoteIdentity(name)
+ if err != nil {
+ return err
+ }
+ cv.log.V(3).Debugf("parsed name %q to %+v (class=%s)", name, id, id.Class.String())
+
+ return cv.MatchRemoteIdentity(id)
+}
+
+func (cv *mtlsPeerVerifier) GetPeerIdentity(conn net.Conn) (*RemoteIdentity, error) {
+ tlsConn, ok := conn.(*tls.Conn)
+ if !ok {
+ return nil, fmt.Errorf("conn is %T, not *tls.Conn", conn)
+ }
+
+ cs := tlsConn.ConnectionState()
+ if len(cs.PeerCertificates) == 0 {
+ return nil, errors.New("no peer certificate provided")
+ }
+
+ leaf, _ := certutil.SplitChain(cs.PeerCertificates)
+
+ var lastErr error
+ if ri, err := ParseRemoteIdentity(leaf.Subject.CommonName); err == nil {
+ logger.V(2).Debugf("found peer identity in CN: %s", ri.ToSpiffe().String())
+ return ri, nil
+ } else {
+ lastErr = err
+ }
+
+ for i, url := range leaf.URIs {
+ if ri, err := ParseRemoteIdentity(url.String()); err == nil {
+ logger.V(2).Debugf("found peer identity in URI SAN %d: %s", i, ri.ToSpiffe().String())
+ return ri, nil
+ } else {
+ lastErr = err
+ }
+ }
+
+ if lastErr == nil {
+ lastErr = errors.New("found no recognizable SPIFFE URLs in peer certificate")
+ }
+ return nil, lastErr
+}
+
func ParseRemoteIdentity(name string) (*RemoteIdentity, error) {
exps := []struct {
expr *regexp.Regexp
i.Domain)
}
}
+
+func NewVerifyYamlConfig(node *yaml.Node) (*VerifyYamlConfig, error) {
+ out := &VerifyYamlConfig{}
+ if err := node.Decode(out); err != nil {
+ return nil, err
+ }
+
+ if len(out.AllowedPrincipals) == 0 {
+ return nil, errors.New("found no allowed_principals")
+ }
+
+ for i, ent := range out.AllowedPrincipals {
+ if err := ent.validate(); err != nil {
+ return nil, fmt.Errorf("rule %d: %v", i, err)
+ }
+ }
+
+ return out, nil
+}
+
+func (v *VerifyYamlConfig) Apply(verifier MTLSPeerVerifier) error {
+ for i, rule := range v.AllowedPrincipals {
+ if err := rule.validate(); err != nil {
+ return fmt.Errorf("rule %d: %v", i, err)
+ }
+
+ if rule.Domain != "" {
+ verifier.AllowFrom(Domain, rule.Domain)
+ } else if rule.User != "" {
+ verifier.AllowFrom(User, rule.User)
+ } else if rule.Service != "" {
+ verifier.AllowFrom(Service, rule.Service)
+ }
+ }
+ return nil
+}
+
+func (r *verifyYamlRule) validate() error {
+ var count uint
+ if r.Domain != "" {
+ if !aclDomainExpr.MatchString(r.Domain) {
+ return fmt.Errorf("domain expression is invalid: %q", r.Domain)
+ }
+ count++
+ }
+ if r.User != "" {
+ if r.User != "*" && !aclUserOrServiceExpr.MatchString(r.User) {
+ return fmt.Errorf("user expression is invalid: %q", r.User)
+ }
+ count++
+ }
+ if r.Service != "" {
+ if r.Service != "*" && !aclUserOrServiceExpr.MatchString(r.Service) {
+ return fmt.Errorf("service expression is invalid: %q", r.Service)
+ }
+ count++
+ }
+ if count != 1 {
+ return fmt.Errorf("each rule must specify exactly one of domain, user or service")
+ }
+
+ return nil
+}