mirror of
https://github.com/pomerium/pomerium.git
synced 2025-07-18 00:58:08 +02:00
Configure Envoy to validate client certificates, using the union of all relevant client CA bundles (that is, a bundle of the main client CA setting together with all per-route client CAs). Pass the validation status from Envoy through to the authorize service, by configuring Envoy to use the newly-added SetClientCertificateMetadata filter, and by also adding the relevant metadata namespace to the ExtAuthz configuration. Remove the existing 'include_peer_certificate' setting from the ExtAuthz configuration, as the metadata from the Lua filter will include the full certificate chain (when it validates successfully by Envoy). Update policy evaluation to consider the validation status from Envoy, in addition to its own certificate chain validation. (Policy evaluation cannot rely solely on the Envoy validation status while we still support the per-route client CA setting.)
213 lines
6.4 KiB
Go
213 lines
6.4 KiB
Go
package authorize
|
|
|
|
import (
|
|
"context"
|
|
"encoding/pem"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
"github.com/pomerium/pomerium/authorize/evaluator"
|
|
"github.com/pomerium/pomerium/config"
|
|
"github.com/pomerium/pomerium/config/envoyconfig"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/sessions"
|
|
"github.com/pomerium/pomerium/internal/telemetry/requestid"
|
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
|
"github.com/pomerium/pomerium/internal/urlutil"
|
|
"github.com/pomerium/pomerium/pkg/contextutil"
|
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
|
"github.com/pomerium/pomerium/pkg/storage"
|
|
)
|
|
|
|
// Check implements the envoy auth server gRPC endpoint.
|
|
func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) {
|
|
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
|
|
defer span.End()
|
|
|
|
querier := storage.NewTracingQuerier(
|
|
storage.NewCachingQuerier(
|
|
storage.NewCachingQuerier(
|
|
storage.NewQuerier(a.state.Load().dataBrokerClient),
|
|
a.globalCache,
|
|
),
|
|
storage.NewLocalCache(),
|
|
),
|
|
)
|
|
ctx = storage.WithQuerier(ctx, querier)
|
|
|
|
state := a.state.Load()
|
|
|
|
// convert the incoming envoy-style http request into a go-style http request
|
|
hreq := getHTTPRequestFromCheckRequest(in)
|
|
ctx = requestid.WithValue(ctx, requestid.FromHTTPHeader(hreq.Header))
|
|
|
|
sessionState, _ := state.sessionStore.LoadSessionState(hreq)
|
|
|
|
var s sessionOrServiceAccount
|
|
var u *user.User
|
|
var err error
|
|
if sessionState != nil {
|
|
s, err = a.getDataBrokerSessionOrServiceAccount(ctx, sessionState.ID, sessionState.DatabrokerRecordVersion)
|
|
if err != nil {
|
|
log.Warn(ctx).Err(err).Msg("clearing session due to missing session or service account")
|
|
sessionState = nil
|
|
}
|
|
}
|
|
if sessionState != nil && s != nil {
|
|
u, _ = a.getDataBrokerUser(ctx, s.GetUserId()) // ignore any missing user error
|
|
}
|
|
|
|
req, err := a.getEvaluatorRequestFromCheckRequest(ctx, in, sessionState)
|
|
if err != nil {
|
|
log.Warn(ctx).Err(err).Msg("error building evaluator request")
|
|
return nil, err
|
|
}
|
|
|
|
// take the state lock here so we don't update while evaluating
|
|
a.stateLock.RLock()
|
|
res, err := state.evaluator.Evaluate(ctx, req)
|
|
a.stateLock.RUnlock()
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Msg("error during OPA evaluation")
|
|
return nil, err
|
|
}
|
|
|
|
// if show error details is enabled, attach the policy evaluation traces
|
|
if req.Policy != nil && req.Policy.ShowErrorDetails {
|
|
ctx = contextutil.WithPolicyEvaluationTraces(ctx, res.Traces)
|
|
}
|
|
|
|
resp, err := a.handleResult(ctx, in, req, res)
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Str("request-id", requestid.FromContext(ctx)).Msg("grpc check ext_authz_error")
|
|
}
|
|
a.logAuthorizeCheck(ctx, in, resp, res, s, u)
|
|
return resp, err
|
|
}
|
|
|
|
func (a *Authorize) getEvaluatorRequestFromCheckRequest(
|
|
ctx context.Context,
|
|
in *envoy_service_auth_v3.CheckRequest,
|
|
sessionState *sessions.State,
|
|
) (*evaluator.Request, error) {
|
|
requestURL := getCheckRequestURL(in)
|
|
attrs := in.GetAttributes()
|
|
clientCertMetadata :=
|
|
attrs.GetMetadataContext().GetFilterMetadata()["com.pomerium.client-certificate-info"]
|
|
req := &evaluator.Request{
|
|
IsInternal: envoyconfig.ExtAuthzContextExtensionsIsInternal(attrs.GetContextExtensions()),
|
|
HTTP: evaluator.NewRequestHTTP(
|
|
attrs.GetRequest().GetHttp().GetMethod(),
|
|
requestURL,
|
|
getCheckRequestHeaders(in),
|
|
getClientCertificateInfo(ctx, clientCertMetadata),
|
|
attrs.GetSource().GetAddress().GetSocketAddress().GetAddress(),
|
|
),
|
|
}
|
|
if sessionState != nil {
|
|
req.Session = evaluator.RequestSession{
|
|
ID: sessionState.ID,
|
|
}
|
|
}
|
|
req.Policy = a.getMatchingPolicy(envoyconfig.ExtAuthzContextExtensionsRouteID(attrs.GetContextExtensions()))
|
|
return req, nil
|
|
}
|
|
|
|
func (a *Authorize) getMatchingPolicy(routeID uint64) *config.Policy {
|
|
options := a.currentOptions.Load()
|
|
|
|
for _, p := range options.GetAllPolicies() {
|
|
id, _ := p.RouteID()
|
|
if id == routeID {
|
|
return &p
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v3.CheckRequest) *http.Request {
|
|
hattrs := req.GetAttributes().GetRequest().GetHttp()
|
|
u := getCheckRequestURL(req)
|
|
hreq := &http.Request{
|
|
Method: hattrs.GetMethod(),
|
|
URL: &u,
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader(hattrs.GetBody())),
|
|
Host: hattrs.GetHost(),
|
|
RequestURI: hattrs.GetPath(),
|
|
}
|
|
for k, v := range getCheckRequestHeaders(req) {
|
|
hreq.Header.Set(k, v)
|
|
}
|
|
return hreq
|
|
}
|
|
|
|
func getCheckRequestHeaders(req *envoy_service_auth_v3.CheckRequest) map[string]string {
|
|
hdrs := make(map[string]string)
|
|
ch := req.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
|
for k, v := range ch {
|
|
hdrs[http.CanonicalHeaderKey(k)] = v
|
|
}
|
|
return hdrs
|
|
}
|
|
|
|
func getCheckRequestURL(req *envoy_service_auth_v3.CheckRequest) url.URL {
|
|
h := req.GetAttributes().GetRequest().GetHttp()
|
|
u := url.URL{
|
|
Scheme: h.GetScheme(),
|
|
Host: h.GetHost(),
|
|
}
|
|
u.Host = urlutil.GetDomainsForURL(&u)[0]
|
|
// envoy sends the query string as part of the path
|
|
path := h.GetPath()
|
|
if idx := strings.Index(path, "?"); idx != -1 {
|
|
u.RawPath, u.RawQuery = path[:idx], path[idx+1:]
|
|
u.RawQuery = u.Query().Encode()
|
|
} else {
|
|
u.RawPath = path
|
|
}
|
|
u.Path, _ = url.PathUnescape(u.RawPath)
|
|
return u
|
|
}
|
|
|
|
// getClientCertificateInfo translates from the client certificate Envoy
|
|
// metadata to the ClientCertificateInfo type.
|
|
func getClientCertificateInfo(
|
|
ctx context.Context, metadata *structpb.Struct,
|
|
) evaluator.ClientCertificateInfo {
|
|
var c evaluator.ClientCertificateInfo
|
|
if metadata == nil {
|
|
return c
|
|
}
|
|
c.Presented = metadata.Fields["presented"].GetBoolValue()
|
|
c.Validated = metadata.Fields["validated"].GetBoolValue()
|
|
escapedChain := metadata.Fields["chain"].GetStringValue()
|
|
if escapedChain == "" {
|
|
// No validated client certificate.
|
|
return c
|
|
}
|
|
|
|
chain, err := url.QueryUnescape(escapedChain)
|
|
if err != nil {
|
|
log.Warn(ctx).Str("chain", escapedChain).Err(err).
|
|
Msg(`received unexpected client certificate "chain" value`)
|
|
return c
|
|
}
|
|
|
|
// Split the chain into the leaf and any intermediate certificates.
|
|
p, rest := pem.Decode([]byte(chain))
|
|
if p == nil {
|
|
log.Warn(ctx).Str("chain", escapedChain).
|
|
Msg(`received unexpected client certificate "chain" value (no PEM block found)`)
|
|
return c
|
|
}
|
|
c.Leaf = string(pem.EncodeToMemory(p))
|
|
c.Intermediates = string(rest)
|
|
return c
|
|
}
|