config/k8s: allow configuring claims used to detect user/group

adds 'kubernetes_impersonate_user_claim' (default 'email')  and
'kubernetes_impersonate_group_claim' (default 'groups') route settings,
which can be used to control the claims that are used to obtain user
and group names for kubernetes impersonation.
This commit is contained in:
Joe Kralicky 2024-09-19 15:04:30 -04:00
parent 0e13248685
commit cc3431bd4a
No known key found for this signature in database
GPG key ID: 75C4875F34A9FB79
6 changed files with 698 additions and 613 deletions

View file

@ -23,6 +23,8 @@ type HeadersRequest struct {
EnableRoutingKey bool `json:"enable_routing_key"`
Issuer string `json:"issuer"`
KubernetesServiceAccountToken string `json:"kubernetes_service_account_token"`
KubernetesImpersonateUserClaim string `json:"kubernetes_impersonate_user_claim"`
KubernetesImpersonateGroupClaim string `json:"kubernetes_impersonate_group_claim"`
ToAudience string `json:"to_audience"`
Session RequestSession `json:"session"`
ClientCertificate ClientCertificateInfo `json:"client_certificate"`
@ -38,6 +40,8 @@ func NewHeadersRequestFromPolicy(policy *config.Policy, http RequestHTTP) *Heade
input.EnableRoutingKey = policy.EnvoyOpts.GetLbPolicy() == envoy_config_cluster_v3.Cluster_RING_HASH ||
policy.EnvoyOpts.GetLbPolicy() == envoy_config_cluster_v3.Cluster_MAGLEV
input.KubernetesServiceAccountToken = policy.KubernetesServiceAccountToken
input.KubernetesImpersonateUserClaim = policy.KubernetesImpersonateUserClaim
input.KubernetesImpersonateGroupClaim = policy.KubernetesImpersonateGroupClaim
for _, wu := range policy.To {
input.ToAudience = "https://" + wu.URL.Hostname()
}

View file

@ -81,10 +81,14 @@ func TestHeadersEvaluator(t *testing.T) {
ctx := context.Background()
ctx = storage.WithQuerier(ctx, storage.NewStaticQuerier(data...))
store := store.New()
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "CUSTOM_KEY"))
store.UpdateJWTClaimHeaders(config.NewJWTClaimHeaders("email", "groups", "user", "pomerium_io_groups", "CUSTOM_KEY"))
store.UpdateSigningKey(privateJWK)
var buf bytes.Buffer
e, err := NewHeadersEvaluator(ctx, store, rego.Time(iat))
require.NoError(t, err)
t.Cleanup(func() {
t.Log(buf.String())
})
return e.Evaluate(ctx, input, rego.EvalTime(iat))
}
@ -240,18 +244,24 @@ func TestHeadersEvaluator(t *testing.T) {
output, err := eval(t,
[]protoreflect.ProtoMessage{
&session.Session{Id: "s1", UserId: "u1"},
&user.User{Id: "u1", Email: "u1@example.com"},
&user.User{Id: "u1", Email: "u1@example.com", Claims: map[string]*structpb.ListValue{
"pomerium_io_groups": {Values: []*structpb.Value{
structpb.NewStringValue("admin"),
}},
}},
},
&HeadersRequest{
Issuer: "from.example.com",
ToAudience: "to.example.com",
KubernetesServiceAccountToken: "TOKEN",
Session: RequestSession{ID: "s1"},
Issuer: "from.example.com",
ToAudience: "to.example.com",
KubernetesServiceAccountToken: "TOKEN",
KubernetesImpersonateUserClaim: "email",
KubernetesImpersonateGroupClaim: "pomerium_io_groups",
Session: RequestSession{ID: "s1"},
})
require.NoError(t, err)
assert.Equal(t, "Bearer TOKEN", output.Headers.Get("Authorization"))
assert.Equal(t, "u1@example.com", output.Headers.Get("Impersonate-User"))
assert.Empty(t, output.Headers["Impersonate-Group"])
assert.Equal(t, "admin", output.Headers.Get("Impersonate-Group"))
})
}

View file

@ -132,6 +132,14 @@ base_jwt_claims := [
["name", jwt_payload_name],
]
session_claims := c if {
c := session.claims
} else := {}
user_claims := c if {
c := user.claims
} else := {}
additional_jwt_claims := [[k, v] |
some header_name
claim_key := data.jwt_claim_headers[header_name]
@ -143,7 +151,7 @@ additional_jwt_claims := [[k, v] |
]) == 0
# the claim value can come from session claims or user claims
claim_value := object.get(session.claims, claim_key, object.get(user.claims, claim_key, null))
claim_value := object.get(session_claims, claim_key, object.get(user_claims, claim_key, null))
k := claim_key
v := get_header_string_value(claim_value)
@ -159,13 +167,33 @@ jwt_payload := {key: value |
signed_jwt := io.jwt.encode_sign(jwt_headers, jwt_payload, data.signing_key)
impersonate_user_claim := u if {
u := input.kubernetes_impersonate_user_claim
u != ""
} else := "email"
impersonate_group_claim := g if {
g := input.kubernetes_impersonate_group_claim
g != ""
} else := "groups"
impersonate_user := v if {
[k, v] := jwt_claims[_]
k == impersonate_user_claim
}
impersonate_group := v if {
[k, v] := jwt_claims[_]
k == impersonate_group_claim
}
kubernetes_headers := h if {
input.kubernetes_service_account_token != ""
h := remove_empty_header_values([
["Authorization", concat(" ", ["Bearer", input.kubernetes_service_account_token])],
["Impersonate-User", jwt_payload_email],
["Impersonate-Group", get_header_string_value(jwt_payload_groups)],
["Impersonate-User", impersonate_user],
["Impersonate-Group", get_header_string_value(impersonate_group)],
])
} else := []

View file

@ -154,6 +154,9 @@ type Policy struct {
// KubernetesServiceAccountTokenFile contains the kubernetes token to use for upstream requests.
KubernetesServiceAccountTokenFile string `mapstructure:"kubernetes_service_account_token_file" yaml:"kubernetes_service_account_token_file,omitempty"`
KubernetesImpersonateUserClaim string `mapstructure:"kubernetes_impersonate_user_claim" yaml:"kubernetes_impersonate_user_claim,omitempty"`
KubernetesImpersonateGroupClaim string `mapstructure:"kubernetes_impersonate_group_claim" yaml:"kubernetes_impersonate_group_claim,omitempty"`
// EnableGoogleCloudServerlessAuthentication adds "Authorization: Bearer ID_TOKEN" headers
// to upstream requests.
EnableGoogleCloudServerlessAuthentication bool `mapstructure:"enable_google_cloud_serverless_authentication" yaml:"enable_google_cloud_serverless_authentication,omitempty"`
@ -274,6 +277,8 @@ func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
HostPathRegexRewriteSubstitution: pb.GetHostPathRegexRewriteSubstitution(),
PassIdentityHeaders: pb.PassIdentityHeaders,
KubernetesServiceAccountToken: pb.GetKubernetesServiceAccountToken(),
KubernetesImpersonateUserClaim: pb.GetKubernetesImpersonateUserClaim(),
KubernetesImpersonateGroupClaim: pb.GetKubernetesImpersonateGroupClaim(),
SetResponseHeaders: pb.GetSetResponseHeaders(),
EnableGoogleCloudServerlessAuthentication: pb.GetEnableGoogleCloudServerlessAuthentication(),
IDPClientID: pb.GetIdpClientId(),
@ -434,6 +439,12 @@ func (p *Policy) ToProto() (*configpb.Route, error) {
if p.IDPClientSecret != "" {
pb.IdpClientSecret = proto.String(p.IDPClientSecret)
}
if p.KubernetesImpersonateUserClaim != "" {
pb.KubernetesImpersonateUserClaim = proto.String(p.KubernetesImpersonateUserClaim)
}
if p.KubernetesImpersonateGroupClaim != "" {
pb.KubernetesImpersonateGroupClaim = proto.String(p.KubernetesImpersonateGroupClaim)
}
if p.Redirect != nil {
pb.Redirect = &configpb.RouteRedirect{
HttpsRedirect: p.Redirect.HTTPSRedirect,

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,7 @@ message RouteDirectResponse {
string body = 2;
}
// Next ID: 63.
// Next ID: 66.
message Route {
string name = 1;
@ -103,6 +103,8 @@ message Route {
optional bool pass_identity_headers = 25;
string kubernetes_service_account_token = 26;
optional string kubernetes_impersonate_user_claim = 64;
optional string kubernetes_impersonate_group_claim = 65;
bool enable_google_cloud_serverless_authentication = 42;
envoy.config.cluster.v3.Cluster envoy_opts = 36;