From c25d2d554b9e4855ba08e7b8ffd9da8e2a7310ae Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 21 Jul 2026 21:19:38 -0400 Subject: [PATCH 01/73] Sign legacy session JWT with the durable session id. Fixes #4125 - reorders the edge client create-session flow so Session.Create resolves the session id (via dedup or persist) before CreateJwt signs the token, so the JWT jti always matches the stored session record - prevents a permanent invalid-session retry storm for legacy-authenticated clients when a create dedups to an existing session, seen after expanding a single-node controller into a multi-node HA cluster - preserves delete-as-revoke semantics: the session remains a durable record, the controller still loads it via Session.Read - adds an apitests regression test asserting the returned token id matches the persisted session id, including on a deduped create --- controller/internal/routes/session_router.go | 21 ++-- tests/session_legacy_token_id_test.go | 108 +++++++++++++++++++ 2 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 tests/session_legacy_token_id_test.go diff --git a/controller/internal/routes/session_router.go b/controller/internal/routes/session_router.go index 2227fd624..0a6d0d3a7 100644 --- a/controller/internal/routes/session_router.go +++ b/controller/internal/routes/session_router.go @@ -188,6 +188,19 @@ func (r *SessionRouter) Create(ae *env.AppEnv, rc *response.RequestContext, para } entity := MapCreateSessionToModel(identity.Id, apiSession.Id, params.Session) + + // Legacy sessions are backed by a durable record, and the JWT must be signed with that + // record's id. Create must run before CreateJwt: its dedup can resolve entity.Id to a + // pre-existing session for the same (api-session, type, service) rather than the freshly + // generated id. Signing first would mint a token whose id has no backing session, so every + // subsequent create-circuit/create-terminator would fail to load the session. + if rc.HasLegacySecurityToken() { + if _, err = ae.Managers.Session.Create(entity, rc.NewChangeContext()); err != nil { + rc.RespondWithError(err) + return + } + } + jwtStr, err := ae.Managers.Session.CreateJwt(entity, rc.HasLegacySecurityToken()) if err != nil { @@ -217,14 +230,6 @@ func (r *SessionRouter) Create(ae *env.AppEnv, rc *response.RequestContext, para Meta: &rest_model.Meta{}, } - if rc.HasLegacySecurityToken() { - _, err = ae.Managers.Session.Create(entity, rc.NewChangeContext()) - if err != nil { - rc.RespondWithError(err) - return - } - } - rc.Respond(newSessionEnvelope, http.StatusCreated) r.createTimer.UpdateSince(start) diff --git a/tests/session_legacy_token_id_test.go b/tests/session_legacy_token_id_test.go new file mode 100644 index 000000000..f448493c3 --- /dev/null +++ b/tests/session_legacy_token_id_test.go @@ -0,0 +1,108 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/Jeffail/gabs" + "github.com/openziti/ziti/v2/common/eid" +) + +// jtiFromServiceToken returns the "jti" (session id) claim from a service-access JWT without +// verifying its signature. +func jtiFromServiceToken(t *testing.T, token string) string { + parts := strings.Split(token, ".") + if len(parts) != 3 { + t.Fatalf("expected a 3-segment JWT, got %d segments", len(parts)) + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("failed to base64-decode JWT payload: %v", err) + } + + claims := map[string]any{} + if err := json.Unmarshal(payload, &claims); err != nil { + t.Fatalf("failed to parse JWT payload: %v", err) + } + + jti, _ := claims["jti"].(string) + return jti +} + +// Test_Legacy_Session_Token_Matches_Persisted_Session guards the legacy create-session flow +// against a regression where the JWT was minted with a freshly generated session id before the +// durable Create ran. When a session already existed for the (api-session, type, service), Create +// deduped the entity to the existing id, but the token had already committed to the new id, so the +// client held a token whose id backed no stored session. Every subsequent create-circuit / +// create-terminator then failed to load the session. The token's id must always match the durable +// session's id, including when a create dedups to an existing session. +func Test_Legacy_Session_Token_Matches_Persisted_Session(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + ctx.CreateEnrollAndStartEdgeRouter() + + identityRole := eid.New() + serviceRole := eid.New() + + _, identityAuth := ctx.AdminManagementSession.requireCreateIdentityOttEnrollment(eid.New(), false, identityRole) + clientSession, err := identityAuth.AuthenticateClientApi(ctx) + ctx.Req.NoError(err) + + service := ctx.AdminManagementSession.requireNewService(s(serviceRole), nil) + ctx.AdminManagementSession.requireNewServicePolicy("Dial", s("#"+serviceRole), s("#"+identityRole), nil) + ctx.AdminManagementSession.requireNewEdgeRouterPolicy(s("#all"), s("#"+identityRole)) + ctx.AdminManagementSession.requireNewServiceEdgeRouterPolicy(s("#all"), s("#"+serviceRole)) + + // first create persists a new durable session; its id and token must agree + resp, err := clientSession.createNewSession(service.Id) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) + + first, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + firstId, ok := first.Path("data.id").Data().(string) + ctx.Req.True(ok, "first response is missing data.id") + firstToken, ok := first.Path("data.token").Data().(string) + ctx.Req.True(ok, "first response is missing data.token") + ctx.Req.Equal(firstId, jtiFromServiceToken(t, firstToken), "token id must match session id on first create") + + // a second create for the same (api-session, type, service) dedups to the existing session; the + // returned token must still reference that persisted session id, not a freshly generated one + resp, err = clientSession.createNewSession(service.Id) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode()) + + second, err := gabs.ParseJSON(resp.Body()) + ctx.Req.NoError(err) + secondId, ok := second.Path("data.id").Data().(string) + ctx.Req.True(ok, "second response is missing data.id") + secondToken, ok := second.Path("data.token").Data().(string) + ctx.Req.True(ok, "second response is missing data.token") + + ctx.Req.Equal(firstId, secondId, "dedup must return the existing session id") + ctx.Req.Equal(secondId, jtiFromServiceToken(t, secondToken), "token id must match the persisted (deduped) session id") +} From 47f21654dd6d8bf982cbe262fc62d5b9ba885e33 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 22 Jul 2026 15:44:46 -0400 Subject: [PATCH 02/73] Filter service policies by string type in enforcer/list queries. Fixes #4141 - fixes the service-policy enforcer, list-service-identities, and list-identity-services queries to filter policy type by the string-mapped value (type = "Dial"/"Bind") instead of the numeric id - the numeric predicate never matched after the type symbol was changed to a string mapping, so the enforcer treated every non-admin legacy session as policy-less and deleted it on each startup scan, and the list filters returned empty - adds a db regression test asserting the enforcer's identity->servicePolicies type/services predicate matches a covering policy via the string form --- controller/db/service_policy_store_test.go | 29 +++++++++++++++++++ .../policy/service_policy_enforcer.go | 2 +- controller/internal/routes/identity_router.go | 4 +-- controller/internal/routes/service_router.go | 4 +-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/controller/db/service_policy_store_test.go b/controller/db/service_policy_store_test.go index 02d5317d7..4ab3b5fe7 100644 --- a/controller/db/service_policy_store_test.go +++ b/controller/db/service_policy_store_test.go @@ -24,6 +24,35 @@ func Test_ServicePolicyStore(t *testing.T) { t.Run("test service policy evaluation", ctx.testServicePolicyRoleEvaluation) t.Run("test update/delete referenced entities", ctx.testServicePolicyUpdateDeleteRefs) t.Run("test filter service policies by type name", ctx.testServicePolicyFilterByTypeName) + t.Run("test service policy enforcer type query", ctx.testServicePolicyEnforcerTypeQuery) +} + +// testServicePolicyEnforcerTypeQuery guards the predicate the ServicePolicyEnforcer uses to decide +// whether a session's identity still has a covering policy. The policy type symbol is string-mapped +// ("Dial"/"Bind"), so the predicate must filter on the string form; filtering on the numeric id +// (type = 1) matches nothing against the string symbol, which previously made the enforcer delete +// valid legacy sessions on startup. +func (ctx *TestContext) testServicePolicyEnforcerTypeQuery(_ *testing.T) { + ctx.CleanupAll() + + dialer := ctx.RequireNewIdentity(eid.New(), false) + service := newEdgeService(eid.New()) + boltztest.RequireCreate(ctx, service) + ctx.requireNewServicePolicy(PolicyTypeDial, ss(entityRef(dialer.Id)), ss(entityRef(service.Id))) + + ctx.NoError(ctx.GetDb().View(func(tx *bbolt.Tx) error { + matched, _, err := ctx.stores.Identity.QueryIds(tx, fmt.Sprintf( + `id = "%v" and not isEmpty(from servicePolicies where type = "%v" and anyOf(services) = "%v")`, + dialer.Id, PolicyTypeDial.String(), service.Id)) + ctx.NoError(err) + ctx.Contains(matched, dialer.Id, "string policy-type predicate must match the covering Dial policy") + + numeric, _, _ := ctx.stores.Identity.QueryIds(tx, fmt.Sprintf( + `id = "%v" and not isEmpty(from servicePolicies where type = %v and anyOf(services) = "%v")`, + dialer.Id, PolicyTypeDial.Id(), service.Id)) + ctx.NotContains(numeric, dialer.Id, "numeric policy-type predicate must not match the string-mapped symbol") + return nil + })) } func newServicePolicy(name string) *ServicePolicy { diff --git a/controller/internal/policy/service_policy_enforcer.go b/controller/internal/policy/service_policy_enforcer.go index 64d02f676..c01ff49ce 100644 --- a/controller/internal/policy/service_policy_enforcer.go +++ b/controller/internal/policy/service_policy_enforcer.go @@ -153,7 +153,7 @@ func (enforcer *ServicePolicyEnforcer) Run() error { if session.Type == db.SessionTypeBind { policyType = db.PolicyTypeBind } - query := fmt.Sprintf(`id = "%v" and not isEmpty(from servicePolicies where type = %v and anyOf(services) = "%v")`, identity.Id, policyType.Id(), session.ServiceId) + query := fmt.Sprintf(`id = "%v" and not isEmpty(from servicePolicies where type = "%v" and anyOf(services) = "%v")`, identity.Id, policyType.String(), session.ServiceId) _, count, err := enforcer.appEnv.GetStores().Identity.QueryIds(tx, query) if err != nil { return err diff --git a/controller/internal/routes/identity_router.go b/controller/internal/routes/identity_router.go index 2be4af4b3..f2229c8a4 100644 --- a/controller/internal/routes/identity_router.go +++ b/controller/internal/routes/identity_router.go @@ -327,11 +327,11 @@ func (r *IdentityRouter) listServices(ae *env.AppEnv, rc *response.RequestContex typeFilter := "" if params.PolicyType != nil { if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeBind.String()) } if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeDial.String()) } } diff --git a/controller/internal/routes/service_router.go b/controller/internal/routes/service_router.go index 32672a7c4..398cfa951 100644 --- a/controller/internal/routes/service_router.go +++ b/controller/internal/routes/service_router.go @@ -386,11 +386,11 @@ func (r *ServiceRouter) listIdentities(ae *env.AppEnv, rc *response.RequestConte typeFilter := "" if params.PolicyType != nil { if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeBind.String()) } if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) { - typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id()) + typeFilter = fmt.Sprintf(` and type = "%s"`, db.PolicyTypeDial.String()) } } From 9054e77fff41628a878deb873b706cbf6ee2488e Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Thu, 23 Jul 2026 10:53:22 -0400 Subject: [PATCH 03/73] Accept legacy sessions and signal recovery for invalid service tokens. Fixes #4145 - loadFromBolt now accepts a non-JWT (legacy/durable) service session token by looking the session up via Session.ReadByToken and verifying it belongs to the api session, so a legacy client's existing session keeps working across a controller upgrade instead of failing as a malformed JWT - classifies service-access token validation failures (malformed, expired, bad claims, mismatched api session, revoked) as InvalidSession so the client re-creates, while revocation-store/datastore read failures remain internalError so a transient controller fault does not make clients discard valid sessions - adds a typed common.InvalidTokenError so ValidateServiceAccessToken can distinguish token-level failures from infrastructure failures --- common/oidc_tokens.go | 20 ++++++++++++ controller/env/appenv.go | 16 +++++----- controller/handler_edge_ctrl/common.go | 43 ++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/common/oidc_tokens.go b/common/oidc_tokens.go index 9714d51af..7f8e9d685 100644 --- a/common/oidc_tokens.go +++ b/common/oidc_tokens.go @@ -209,6 +209,26 @@ func (c *ServiceAccessClaims) HasAudience(targetAud string) bool { return false } +// InvalidTokenError indicates a token was rejected because of the token itself (bad signature or +// parse, expiry, unexpected claims/audience/type, a mismatched api session, or revocation) rather +// than an infrastructure failure (e.g. a datastore read) encountered while validating it. Callers +// use it to distinguish a client that should discard and re-create its session from a transient +// controller failure that must not invalidate otherwise-valid client state. +type InvalidTokenError struct { + Err error +} + +func (e *InvalidTokenError) Error() string { + if e.Err == nil { + return "invalid token" + } + return e.Err.Error() +} + +func (e *InvalidTokenError) Unwrap() error { + return e.Err +} + type AccessClaims struct { oidc.AccessTokenClaims CustomClaims diff --git a/controller/env/appenv.go b/controller/env/appenv.go index 08f5a1cfb..80a68b12f 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -254,19 +254,19 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) parsedToken, err := jwt.ParseWithClaims(token, serviceAccessClaims, ae.JwtSignerKeyFunc) if err != nil { - return nil, err + return nil, &common.InvalidTokenError{Err: err} } if !parsedToken.Valid { - return nil, errors.New("service access token is invalid") + return nil, &common.InvalidTokenError{Err: errors.New("service access token is invalid")} } if !serviceAccessClaims.HasAudience(common.ClaimAudienceOpenZiti) && !serviceAccessClaims.HasAudience(common.ClaimLegacyNative) { - return nil, fmt.Errorf("invalid audience, expected an instance of %s or %s, got %v", common.ClaimAudienceOpenZiti, common.ClaimLegacyNative, serviceAccessClaims.Audience) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid audience, expected an instance of %s or %s, got %v", common.ClaimAudienceOpenZiti, common.ClaimLegacyNative, serviceAccessClaims.Audience)} } if serviceAccessClaims.TokenType != common.TokenTypeServiceAccess { - return nil, fmt.Errorf("invalid token type, expected %s, got %s", common.TokenTypeServiceAccess, serviceAccessClaims.Type) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid token type, expected %s, got %s", common.TokenTypeServiceAccess, serviceAccessClaims.Type)} } if apiSessionId != nil { @@ -275,10 +275,12 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if serviceAccessClaims.ApiSessionId != *apiSessionId { - return nil, fmt.Errorf("invalid api session id, expected %s, got %s", *apiSessionId, serviceAccessClaims.ApiSessionId) + return nil, &common.InvalidTokenError{Err: fmt.Errorf("invalid api session id, expected %s, got %s", *apiSessionId, serviceAccessClaims.ApiSessionId)} } } + // Revocation.Read failures below are infrastructure errors and are returned raw (not wrapped as + // InvalidTokenError), so a transient datastore failure does not make callers discard valid sessions. tokenRevocation, err := ae.GetManagers().Revocation.Read(serviceAccessClaims.ID) if err != nil && !boltz.IsErrNotFoundErr(err) { @@ -286,7 +288,7 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if tokenRevocation != nil { - return nil, errors.New("service access token has been revoked by id") + return nil, &common.InvalidTokenError{Err: errors.New("service access token has been revoked by id")} } revocation, err := ae.GetManagers().Revocation.Read(serviceAccessClaims.IdentityId) @@ -296,7 +298,7 @@ func (ae *AppEnv) ValidateServiceAccessToken(token string, apiSessionId *string) } if revocation != nil && revocation.CreatedAt.After(serviceAccessClaims.IssuedAt.Time) { - return nil, errors.New("service access token has been revoked by identity") + return nil, &common.InvalidTokenError{Err: errors.New("service access token has been revoked by identity")} } return serviceAccessClaims, nil diff --git a/controller/handler_edge_ctrl/common.go b/controller/handler_edge_ctrl/common.go index 50176054c..e3f3e75de 100644 --- a/controller/handler_edge_ctrl/common.go +++ b/controller/handler_edge_ctrl/common.go @@ -11,7 +11,6 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/sdk-golang/ziti/edge" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/logcontext" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" @@ -23,6 +22,7 @@ import ( "github.com/openziti/ziti/v2/controller/models" "github.com/openziti/ziti/v2/controller/network" "github.com/openziti/ziti/v2/controller/oidc_auth" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/xt" "github.com/sirupsen/logrus" ) @@ -314,10 +314,49 @@ func (self *baseSessionRequestContext) loadFromBolt(sessionToken string, apiSess return } + if !strings.HasPrefix(sessionToken, oidc_auth.JwtTokenPrefix) { + self.session, err = self.handler.getAppEnv().Managers.Session.ReadByToken(sessionToken) + + if err != nil { + if boltz.IsErrNotFoundErr(err) { + self.err = InvalidSessionError{} + } else { + self.err = internalError(err) + } + logrus. + WithField("operation", self.handler.Label()). + WithError(self.err).Errorf("invalid session") + return + } + + if self.session.ApiSessionId != self.apiSession.Id { + self.err = InvalidSessionError{} + logrus. + WithField("operation", self.handler.Label()). + WithField("sessionId", self.session.Id). + WithField("sessionApiSessionId", self.session.ApiSessionId). + WithField("apiSessionId", self.apiSession.Id). + WithError(self.err).Error("session does not belong to api session") + } + return + } + serviceAccessClaims, err := self.env.ValidateServiceAccessToken(sessionToken, &self.apiSession.Id) if err != nil { - self.err = internalError(err) + // A token-level failure (bad/expired/mismatched/revoked token) means the client should discard + // and re-create its session, so return InvalidSession. An infrastructure failure (e.g. a + // revocation datastore read) must stay an internalError, otherwise a transient controller fault + // would make clients discard valid sessions and trigger a reauthentication storm. + var invalidToken *common.InvalidTokenError + if errors.As(err, &invalidToken) { + self.err = InvalidSessionError{} + logrus. + WithField("operation", self.handler.Label()). + WithError(err).Error("service access token invalid; treating as invalid session") + } else { + self.err = internalError(err) + } return } From c0bf5b712619820a08dc90c301f6254acfe650fa Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 24 Jul 2026 11:43:33 -0400 Subject: [PATCH 04/73] Signal retry on leaderless terminator operations. Fixes #4161 Backport of #4160 to release-v2.0.x. - adds command.WasLeaderless to classify cluster-has-no-leader dispatch errors as retriable - replies busy instead of dropping or hard-failing terminator creates when the cluster is briefly leaderless, so the router backs off and requeues promptly rather than waiting for its multi-minute recovery scan - removes the racy up-front leaderless pre-check in the sdk create handler in favor of classifying the actual dispatch result - applies the same retriable classification to the ert tunnel create and batch remove terminator handlers --- controller/command/rate_limiter.go | 11 +++++++++++ controller/handler_ctrl/remove_terminators.go | 4 +++- .../handler_edge_ctrl/create_terminator_v2.go | 13 +++++++------ .../create_tunnel_terminator_v2.go | 4 +++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/controller/command/rate_limiter.go b/controller/command/rate_limiter.go index 51c7afc27..a204b73e7 100644 --- a/controller/command/rate_limiter.go +++ b/controller/command/rate_limiter.go @@ -438,6 +438,17 @@ func WasRateLimited(err error) bool { return false } +// WasLeaderless returns true if the given error indicates that a command could not be dispatched because the +// cluster currently has no leader. This is a transient condition during membership changes; callers should +// treat it like rate limiting and signal the requester to retry rather than fail permanently. +func WasLeaderless(err error) bool { + var apiErr *errorz.ApiError + if errors.As(err, &apiErr) { + return apiErr.AppCode == apierror.ClusterHasNoLeaderCode + } + return false +} + // AdaptiveRateLimitTrackerConfig contains configuration values used to create a new AdaptiveRateLimitTracker type AdaptiveRateLimitTrackerConfig struct { AdaptiveRateLimiterConfig diff --git a/controller/handler_ctrl/remove_terminators.go b/controller/handler_ctrl/remove_terminators.go index f9e324d5e..857072df7 100644 --- a/controller/handler_ctrl/remove_terminators.go +++ b/controller/handler_ctrl/remove_terminators.go @@ -75,7 +75,9 @@ func (self *removeTerminatorsHandler) handleRemoveTerminators(msg *channel.Messa WithField("terminatorIds", request.TerminatorIds). Info("removed terminators") handler_common.SendSuccess(msg, ch, "") - } else if command.WasRateLimited(err) { + } else if command.WasRateLimited(err) || command.WasLeaderless(err) { + // A leaderless cluster (during a membership change) is transient; signal busy so the router retries + // rather than treating the removal as a permanent failure. handler_common.SendServerBusy(msg, ch, "remove.terminators") } else { handler_common.SendFailure(msg, ch, err.Error()) diff --git a/controller/handler_edge_ctrl/create_terminator_v2.go b/controller/handler_edge_ctrl/create_terminator_v2.go index 3499642a3..33e9944e0 100644 --- a/controller/handler_edge_ctrl/create_terminator_v2.go +++ b/controller/handler_edge_ctrl/create_terminator_v2.go @@ -58,11 +58,6 @@ func (self *createTerminatorV2Handler) Label() string { } func (self *createTerminatorV2Handler) HandleReceive(msg *channel.Message, ch channel.Channel) { - if self.appEnv.GetCommandDispatcher().IsLeaderless() { - pfxlog.ContextLogger(ch.Label()).Error("cluster has no leader, unable to handle create terminator request") - return - } - req := &edge_ctrl_pb.CreateTerminatorV2Request{} if err := proto.Unmarshal(msg.Body, req); err != nil { pfxlog.ContextLogger(ch.Label()).WithError(err).Error("could not unmarshal CreateTerminatorV2Request") @@ -122,6 +117,12 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV }, ctx.newChangeContext()) if err != nil { + // A rate-limited or leaderless dispatch is transient; reply busy so the router requeues + // promptly instead of treating it as a hard failure. + if command.WasRateLimited(err) || command.WasLeaderless(err) { + self.returnError(ctx, busyError(err), logger) + return + } self.returnError(ctx, internalError(err), logger) return } @@ -160,7 +161,7 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV return } } else { - if command.WasRateLimited(err) { + if command.WasRateLimited(err) || command.WasLeaderless(err) { self.returnError(ctx, busyError(err), logger) return } diff --git a/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go b/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go index bfaf312f6..10e9e4933 100644 --- a/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go +++ b/controller/handler_edge_ctrl/create_tunnel_terminator_v2.go @@ -148,7 +148,9 @@ func (self *createTunnelTerminatorV2Handler) CreateTerminator(ctx *createTunnelT return } } else { - if command.WasRateLimited(err) { + // A rate-limited or leaderless dispatch is transient; reply busy so the router requeues + // promptly instead of treating it as a hard failure. + if command.WasRateLimited(err) || command.WasLeaderless(err) { self.returnError(ctx, busyError(err), logger) return } From 8ef4e50b02a69f4dde08c035cfbe8a4c3bc9cc7f Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 24 Jul 2026 15:50:38 -0400 Subject: [PATCH 05/73] Reject terminator removals from a router that does not own them. Fixes #4166 - filters the fabric RemoveTerminators handler so a router can only remove terminators whose owning router matches the request source; ids owned by another router are dropped and logged rather than deleted - keeps absent ids so a delete racing a not-yet-applied create is still ordered after it - the edge control channel already enforces this via verifyTerminator Backport of #4165. release-v2.0.x has no RemoveTerminatorsV2, so only the v1 handler is affected. --- controller/handler_ctrl/base.go | 47 ++++++++++++ controller/handler_ctrl/base_test.go | 71 +++++++++++++++++++ controller/handler_ctrl/remove_terminators.go | 19 +++-- 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 controller/handler_ctrl/base_test.go diff --git a/controller/handler_ctrl/base.go b/controller/handler_ctrl/base.go index 547d04721..fe962dd18 100644 --- a/controller/handler_ctrl/base.go +++ b/controller/handler_ctrl/base.go @@ -17,10 +17,12 @@ package handler_ctrl import ( + "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/network" + "github.com/openziti/ziti/v2/controller/storage/boltz" ) type baseHandler struct { @@ -31,3 +33,48 @@ type baseHandler struct { func (self *baseHandler) newChangeContext(ch channel.Channel, method string) *change.Context { return change.NewControlChannelChange(self.router.Id, self.router.Name, method, ch) } + +// lookupTerminatorOwner returns the id of the router that owns terminator id and whether the +// terminator currently exists. A not-found terminator returns ("", false, nil); other read errors +// are returned so the caller can default to keeping the id rather than acting on incomplete state. +func (self *baseHandler) lookupTerminatorOwner(id string) (routerId string, present bool, err error) { + terminator, err := self.network.Terminator.Read(id) + if err != nil { + if boltz.IsErrNotFoundErr(err) { + return "", false, nil + } + return "", false, err + } + return terminator.Router, true, nil +} + +// selectOwnedTerminators returns the terminator ids from a remove request that this router may +// remove, dropping (and logging) any whose terminator is owned by a different router, so a router +// can only remove terminators it owns. Absent ids are kept so a delete racing a not-yet-applied +// create is still ordered after it. +func (self *baseHandler) selectOwnedTerminators(ids []string) []string { + kept, rejected := filterOwnedTerminators(ids, self.router.Id, self.lookupTerminatorOwner) + if rejected > 0 { + pfxlog.Logger(). + WithField("routerId", self.router.Id). + WithField("rejected", rejected). + Warn("router attempted to remove terminators it does not own; rejected") + } + return kept +} + +// filterOwnedTerminators returns the ids owned by requestingRouterId (or not currently present), +// dropping ids whose terminator resolves to a different router; rejected counts those drops. A +// lookup error keeps the id, so an unresolved owner is not treated as an ownership violation. +func filterOwnedTerminators(ids []string, requestingRouterId string, lookup func(string) (routerId string, present bool, err error)) (kept []string, rejected int) { + kept = make([]string, 0, len(ids)) + for _, id := range ids { + routerId, present, err := lookup(id) + if err == nil && present && routerId != requestingRouterId { + rejected++ + continue + } + kept = append(kept, id) + } + return kept, rejected +} diff --git a/controller/handler_ctrl/base_test.go b/controller/handler_ctrl/base_test.go new file mode 100644 index 000000000..dfa5310b8 --- /dev/null +++ b/controller/handler_ctrl/base_test.go @@ -0,0 +1,71 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_ctrl + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_filterOwnedTerminators(t *testing.T) { + const me = "router-me" + + // mine: present, owned by the requesting router; other: present, owned by a different router; + // gone: not present; err: lookup fails. + lookup := func(id string) (string, bool, error) { + switch id { + case "mine": + return me, true, nil + case "other": + return "router-other", true, nil + case "err": + return "", false, errors.New("boom") + default: // "gone" and anything else + return "", false, nil + } + } + + t.Run("keeps owned and absent ids", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"mine", "gone"}, me, lookup) + req.Equal([]string{"mine", "gone"}, kept) + req.Zero(rejected) + }) + + t.Run("rejects ids owned by another router", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"other", "mine"}, me, lookup) + req.Equal([]string{"mine"}, kept, "a router may only remove terminators it owns") + req.Equal(1, rejected) + }) + + t.Run("a lookup error keeps the id", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"err"}, me, lookup) + req.Equal([]string{"err"}, kept, "an unresolved owner must not be treated as an ownership violation") + req.Zero(rejected) + }) + + t.Run("mixes kept and rejected", func(t *testing.T) { + req := require.New(t) + kept, rejected := filterOwnedTerminators([]string{"mine", "other", "gone", "err"}, me, lookup) + req.Equal([]string{"mine", "gone", "err"}, kept) + req.Equal(1, rejected) + }) +} diff --git a/controller/handler_ctrl/remove_terminators.go b/controller/handler_ctrl/remove_terminators.go index f9e324d5e..4c950377f 100644 --- a/controller/handler_ctrl/remove_terminators.go +++ b/controller/handler_ctrl/remove_terminators.go @@ -59,20 +59,25 @@ func (self *removeTerminatorsHandler) HandleReceive(msg *channel.Message, ch cha func (self *removeTerminatorsHandler) handleRemoveTerminators(msg *channel.Message, ch channel.Channel, request *ctrl_pb.RemoveTerminatorsRequest) { log := pfxlog.ContextLogger(ch.Label()) - // Don't pre-filter by IsEntityPresent here. The create for a terminator may be - // in-flight in raft but not yet applied to the DB. If we skip it here, the create - // will apply after we return success, leaving an orphan. By sending all IDs through - // raft, the delete will be ordered after the create and ApplyDeleteBatch will handle - // non-existent IDs gracefully. if len(request.TerminatorIds) == 0 { handler_common.SendSuccess(msg, ch, "") return } - if err := self.network.Terminator.DeleteBatch(request.TerminatorIds, self.newChangeContext(ch, "fabric.remove.terminators.batch")); err == nil { + // Drop ids this router doesn't own, so it can't remove another router's terminators. Absent ids + // are kept (not pre-filtered by presence): the create for a terminator may be in-flight in raft + // but not yet applied to the DB, so sending it through raft orders the delete after the create, + // and ApplyDeleteBatch handles non-existent ids gracefully. + toDelete := self.selectOwnedTerminators(request.TerminatorIds) + if len(toDelete) == 0 { + handler_common.SendSuccess(msg, ch, "") + return + } + + if err := self.network.Terminator.DeleteBatch(toDelete, self.newChangeContext(ch, "fabric.remove.terminators.batch")); err == nil { log. WithField("routerId", ch.Id()). - WithField("terminatorIds", request.TerminatorIds). + WithField("terminatorIds", toDelete). Info("removed terminators") handler_common.SendSuccess(msg, ch, "") } else if command.WasRateLimited(err) { From dbe3b6714376ad00428964ee82150f84433640c3 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 14:25:05 -0400 Subject: [PATCH 06/73] Add shared client certificate chain verification helper --- common/cert/verify.go | 61 +++++++++++++ common/cert/verify_test.go | 172 +++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 common/cert/verify.go create mode 100644 common/cert/verify_test.go diff --git a/common/cert/verify.go b/common/cert/verify.go new file mode 100644 index 000000000..8021f1813 --- /dev/null +++ b/common/cert/verify.go @@ -0,0 +1,61 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/x509" + "errors" + "fmt" + + "github.com/openziti/identity" +) + +// VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// root in the given CA pool, and returns that verified leaf. Only certs[0] is verified: it is the +// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a +// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate +// intermediates (added alongside the pool's intermediates), never as independent grounds for +// acceptance. +// +// Client-authentication extended key usage is required. A certificate with no ExtKeyUsage extension +// is unrestricted and passes (the form ziti pki produces); a leaf whose EKU excludes client +// authentication is rejected. Callers that must accept server-auth-only client identities should +// verify with an explicit {ClientAuth, ServerAuth} usage set instead. +func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + if pool == nil { + return nil, errors.New("no ca pool provided") + } + + if len(certs) == 0 { + return nil, errors.New("no certificates presented") + } + + intermediates := pool.IntermediatesAsStdPool() + for _, intermediate := range certs[1:] { + intermediates.AddCert(intermediate) + } + + if _, err := certs[0].Verify(x509.VerifyOptions{ + Roots: pool.RootsAsStdPool(), + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }); err != nil { + return nil, fmt.Errorf("leaf certificate not trusted: %w", err) + } + + return certs[0], nil +} diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go new file mode 100644 index 000000000..27bfcdfbd --- /dev/null +++ b/common/cert/verify_test.go @@ -0,0 +1,172 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net/url" + "testing" + "time" + + "github.com/openziti/identity" + "github.com/stretchr/testify/require" +) + +const vTestTrustDomain = "spiffe://verify-test" + +type vCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var vSerial int64 + +func vNextSerial() *big.Int { + vSerial++ + return big.NewInt(vSerial) +} + +func vMkCA(t *testing.T, cn string, parent *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + signParent, signKey := tmpl, key + if parent != nil { + signParent, signKey = parent.cert, parent.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +// vMkLeaf builds an end-entity cert. signer==nil yields a leaf that does not chain to any CA. A nil +// ekus produces a cert with no ExtKeyUsage extension (unrestricted). +func vMkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: ekus, + } + if spiffePath != "" { + u, err := url.Parse(vTestTrustDomain + spiffePath) + require.NoError(t, err) + tmpl.URIs = []*url.URL{u} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +// TestVerifyClientCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to +// the CA is rejected even when another presented certificate does chain - i.e. verification is bound +// to the leaf, not to "some presented certificate verifies". +func TestVerifyClientCertChain_RejectsUnchainedLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + // A trust-domain-signed cert (chains to the CA) presented as an extra certificate. + extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + // The leaf itself is self-signed and does NOT chain to the CA. + unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + + _, err := VerifyClientCertChain(pool, []*x509.Certificate{unchained.cert, extra.cert}) + req.Error(err, "leaf not chaining to the CA must be rejected even alongside a chaining extra cert") +} + +func TestVerifyClientCertChain_AcceptsLegitLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + leaf, err := VerifyClientCertChain(pool, []*x509.Certificate{legit.cert}) + req.NoError(err) + req.Equal(legit.cert, leaf, "returns the verified leaf (certs[0]) for identity use") +} + +func TestVerifyClientCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + + // Pool holds ONLY the root; the intermediate must be supplied on the wire (certs[1:]). + rootOnly := identity.NewCaPool([]*x509.Certificate{root.cert}) + _, err := VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert}) + req.Error(err, "without the intermediate anywhere the leaf cannot be verified") + _, err = VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) + req.NoError(err, "peer-supplied intermediate lets a valid peer verify") +} + +func TestVerifyClientCertChain_EKUCompat(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + // No ExtKeyUsage extension (the form ziti pki produces) is unrestricted -> accepted. + noEKU := vMkLeaf(t, "ziti-pki", "/identity/real", nil, inter) + _, err := VerifyClientCertChain(pool, []*x509.Certificate{noEKU.cert}) + req.NoError(err, "no-EKU leaf accepted (ziti pki backward compat)") + + // EKU present but excludes client auth (server-auth only) -> rejected. + serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.Error(err, "server-auth-only leaf rejected by the client-auth requirement") +} + +func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + pool := identity.NewCaPool([]*x509.Certificate{root.cert}) + + _, err := VerifyClientCertChain(nil, []*x509.Certificate{root.cert}) + req.Error(err, "nil pool rejected") + _, err = VerifyClientCertChain(pool, nil) + req.Error(err, "no certs rejected") +} From 2c60866d3eb20a4ece9bd7585ac78038b1c1bfa7 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 14:25:05 -0400 Subject: [PATCH 07/73] Add stricter certificate chain validation for peer connections --- controller/raft/mesh/mesh.go | 17 +- .../mesh/mesh_peer_cert_validation_test.go | 240 ++++++++++++++++++ controller/webapis/metrics-api.go | 12 +- router/xlink_transport/connect.go | 21 +- 4 files changed, 260 insertions(+), 30 deletions(-) create mode 100644 controller/raft/mesh/mesh_peer_cert_validation_test.go diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index e828d3e89..d2f4a84e5 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -29,6 +29,7 @@ import ( "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/cert" "github.com/openziti/ziti/v2/controller/event" "github.com/hashicorp/raft" @@ -598,18 +599,12 @@ func (self *impl) checkClusterIds(ch channel.Channel) error { } func (self *impl) checkCerts(ch channel.Channel) error { - certs := ch.Underlay().Certificates() - if len(certs) == 0 { - return errors.New("unable to validate peer connection, no certs presented") + // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that + // must chain to a trusted root; VerifyClientCertChain verifies that leaf specifically. + if _, err := cert.VerifyClientCertChain(self.env.GetNodeId().CaPool(), ch.Underlay().Certificates()); err != nil { + return fmt.Errorf("unable to validate peer connection: %w", err) } - - for _, cert := range ch.Underlay().Certificates() { - if _, err := self.env.GetNodeId().CaPool().VerifyToRoot(cert); err == nil { - return nil - } - } - - return errors.New("unable to validate peer connection, no certs presented matched the CA for this node") + return nil } func (self *impl) GetPeerInfo(address string, timeout time.Duration) (raft.ServerID, raft.ServerAddress, error) { diff --git a/controller/raft/mesh/mesh_peer_cert_validation_test.go b/controller/raft/mesh/mesh_peer_cert_validation_test.go new file mode 100644 index 000000000..cd90fb684 --- /dev/null +++ b/controller/raft/mesh/mesh_peer_cert_validation_test.go @@ -0,0 +1,240 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mesh + +// End-to-end validation of the mesh peer certificate check over a real TCP+TLS socket, using the +// production controller TLS config (identity.ServerTLSConfig). The ctrl listener uses +// RequireAnyClientCert and does not verify the client chain, so the handshake completes for any +// presented client leaf; peer admission is enforced afterward by verifyClientCertChain against the +// node CA pool. Unit-level coverage of the shared check lives in common/cert. + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "net/url" + "strings" + "testing" + "time" + + "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" + "github.com/stretchr/testify/require" +) + +const testTrustDomain = "spiffe://mesh-cert-test" + +type certAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var testSerial int64 + +func nextTestSerial() *big.Int { + testSerial++ + return big.NewInt(testSerial) +} + +func mkTestKey(t *testing.T) *ecdsa.PrivateKey { + t.Helper() + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + return k +} + +func signTestCert(t *testing.T, tmpl, parent *x509.Certificate, pub *ecdsa.PublicKey, signerKey *ecdsa.PrivateKey) *x509.Certificate { + t.Helper() + der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, pub, signerKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return c +} + +func mkCA(t *testing.T, cn string, parent *certAndKey) *certAndKey { + t.Helper() + key := mkTestKey(t) + tmpl := &x509.Certificate{ + SerialNumber: nextTestSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + signParent, signKey := tmpl, key + if parent != nil { + signParent, signKey = parent.cert, parent.key + } + return &certAndKey{cert: signTestCert(t, tmpl, signParent, &key.PublicKey, signKey), key: key} +} + +func mkLeafWithKey(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *certAndKey, key *ecdsa.PrivateKey) *certAndKey { + t.Helper() + tmpl := &x509.Certificate{ + SerialNumber: nextTestSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: ekus, + } + if spiffePath != "" { + u, err := url.Parse(testTrustDomain + spiffePath) + require.NoError(t, err) + tmpl.URIs = []*url.URL{u} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + return &certAndKey{cert: signTestCert(t, tmpl, signParent, &key.PublicKey, signKey), key: key} +} + +func mkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *certAndKey) *certAndKey { + return mkLeafWithKey(t, cn, spiffePath, ekus, signer, mkTestKey(t)) +} + +func pemOfCerts(t *testing.T, certs ...*x509.Certificate) string { + t.Helper() + var b strings.Builder + for _, c := range certs { + require.NoError(t, pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})) + } + return b.String() +} + +func pemOfKey(t *testing.T, key *ecdsa.PrivateKey) string { + t.Helper() + der, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + var b strings.Builder + require.NoError(t, pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: der})) + return b.String() +} + +// Test_MeshPeerCert_LiveHandshake stands up a real TLS listener with the production node identity, +// then connects as both a rogue and a legitimate peer. The rogue presents a self-signed identity leaf +// plus the node's own (scraped) server cert as an extra certificate; the handshake completes, but the +// mesh check must reject it. The legitimate peer presents a CA-signed client leaf and must be accepted. +func Test_MeshPeerCert_LiveHandshake(t *testing.T) { + req := require.New(t) + + root := mkCA(t, "root", nil) + inter := mkCA(t, "int", root) + + // Real node identity: one key backs the client and server certs (LoadIdentity uses the default + // key for the server cert when no server_key is configured). + nodeKey := mkTestKey(t) + clientCert := mkLeafWithKey(t, "node-client", "/controller/real-id", + []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter, nodeKey) + serverCert := mkLeafWithKey(t, "node-server", "", + []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter, nodeKey) + + id, err := identity.LoadIdentity(identity.Config{ + Key: "pem:" + pemOfKey(t, nodeKey), + Cert: "pem:" + pemOfCerts(t, clientCert.cert, inter.cert), + ServerCert: "pem:" + pemOfCerts(t, serverCert.cert, inter.cert), + CA: "pem:" + pemOfCerts(t, root.cert, inter.cert), + }) + req.NoError(err) + + serverCfg := id.ServerTLSConfig() + req.NotNil(serverCfg) + req.Equal(tls.RequireAnyClientCert, serverCfg.ClientAuth) + + rawLn, err := net.Listen("tcp", "127.0.0.1:0") + req.NoError(err) + defer func() { _ = rawLn.Close() }() + ln := tls.NewListener(rawLn, serverCfg) + addr := ln.Addr().String() + + type acceptResult struct { + peer []*x509.Certificate + err error + } + results := make(chan acceptResult, 3) + go func() { + for i := 0; i < 3; i++ { + c, aerr := ln.Accept() + if aerr != nil { + results <- acceptResult{err: aerr} + continue + } + tc := c.(*tls.Conn) + _ = tc.SetDeadline(time.Now().Add(10 * time.Second)) + if herr := tc.Handshake(); herr != nil { + results <- acceptResult{err: herr} + _ = tc.Close() + continue + } + results <- acceptResult{peer: tc.ConnectionState().PeerCertificates} + _ = tc.Close() + } + }() + + pool := id.CaPool() + + // Scrape the node's own server certificate from the listener (a throwaway client cert satisfies + // RequireAnyClientCert). This is the "extra" certificate the rogue peer will present. + throwaway := mkLeaf(t, "throwaway", "", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + scrapeConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{throwaway.cert.Raw}, PrivateKey: throwaway.key, Leaf: throwaway.cert}}, + }) + req.NoError(err) + serverPresented := scrapeConn.ConnectionState().PeerCertificates + _ = scrapeConn.Close() + req.NotEmpty(serverPresented) + extra := serverPresented[0] + req.Equal("node-server", extra.Subject.CommonName) + <-results + + // Rogue peer: self-signed identity leaf + the scraped extra cert. Handshake completes; the mesh + // check must reject it because the identity leaf (certs[0]) does not chain to the CA. + rogue := mkLeaf(t, "rogue", "/controller/victim-id", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + rogueConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{rogue.cert.Raw, extra.Raw}, PrivateKey: rogue.key, Leaf: rogue.cert}}, + }) + req.NoError(err, "handshake completes under RequireAnyClientCert even for a self-signed leaf") + _ = rogueConn.Close() + rogueRes := <-results + req.NoError(rogueRes.err) + _, err = cert.VerifyClientCertChain(pool, rogueRes.peer) + req.Error(err, "mesh check rejects a self-signed identity leaf backed by a scraped extra cert") + + // Legitimate peer: CA-signed client leaf. Handshake completes and the mesh check accepts it. + legitConn, err := tls.Dial("tcp", addr, &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{{Certificate: [][]byte{clientCert.cert.Raw, inter.cert.Raw}, PrivateKey: nodeKey, Leaf: clientCert.cert}}, + }) + req.NoError(err) + _ = legitConn.Close() + legitRes := <-results + req.NoError(legitRes.err) + _, err = cert.VerifyClientCertChain(pool, legitRes.peer) + req.NoError(err, "mesh check accepts a legitimate CA-signed peer leaf") +} diff --git a/controller/webapis/metrics-api.go b/controller/webapis/metrics-api.go index 313a3d39a..bd26cecaf 100644 --- a/controller/webapis/metrics-api.go +++ b/controller/webapis/metrics-api.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" @@ -147,8 +148,15 @@ func (metricsApi *MetricsApiHandler) newHandler() http.Handler { if nil != metricsApi.scrapeCert { certOk := false - for _, r := range r.TLS.PeerCertificates { - if bytes.Equal(metricsApi.scrapeCert.Signature, r.Signature) { + // Match the pinned scrape cert against the presented LEAF only (PeerCertificates[0], the + // cert whose private key the TLS handshake proved) - iterating the whole chain would let a + // client present its own leaf plus the public scrape cert as an extra cert and pass. Compare + // full DER, not just the signature, and reject a leaf outside its validity window. + if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { + leaf := r.TLS.PeerCertificates[0] + now := time.Now() + if bytes.Equal(metricsApi.scrapeCert.Raw, leaf.Raw) && + !now.Before(leaf.NotBefore) && !now.After(leaf.NotAfter) { certOk = true } } diff --git a/router/xlink_transport/connect.go b/router/xlink_transport/connect.go index ffaabea85..27f219241 100644 --- a/router/xlink_transport/connect.go +++ b/router/xlink_transport/connect.go @@ -23,6 +23,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" log "github.com/sirupsen/logrus" ) @@ -35,14 +36,6 @@ func (self *ConnectionHandler) HandleConnection(hello *channel.Hello, certificat return errors.New("no certificates provided, unable to verify dialer") } - config := self.routerId.ServerTLSConfig() - - opts := x509.VerifyOptions{ - Roots: config.RootCAs, - Intermediates: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - } - dialedRouterId, ok := hello.Headers[LinkDialedRouterId] if ok { @@ -54,15 +47,9 @@ func (self *ConnectionHandler) HandleConnection(hello *channel.Hello, certificat } } - var errorList []error - - for _, cert := range certificates { - if _, err := cert.Verify(opts); err == nil { - return nil - } else { - errorList = append(errorList, err) - } + if _, err := cert.VerifyClientCertChain(self.routerId.CaPool(), certificates); err != nil { + return fmt.Errorf("unable to verify dialing router: %w", err) } - return errors.Join(errorList...) + return nil } From cada8b364a80b2d0247f27300394771fa4be4ef5 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 14:54:59 -0400 Subject: [PATCH 08/73] Add negative-path tests for peer connection certificate validation - adds tests asserting incoming router links reject a dialer presenting an untrusted or self-signed leaf, including one backed by a scraped CA-chained filler cert, and accept a leaf that chains to the CA - adds tests asserting the metrics scrape-cert gate matches the pinned cert against the presented leaf only, honors the validity window, and rejects requests with no client certificate --- controller/webapis/metrics-api_test.go | 103 ++++++++++++++++++ router/xlink_transport/connect_test.go | 142 +++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 controller/webapis/metrics-api_test.go create mode 100644 router/xlink_transport/connect_test.go diff --git a/controller/webapis/metrics-api_test.go b/controller/webapis/metrics-api_test.go new file mode 100644 index 000000000..165e38799 --- /dev/null +++ b/controller/webapis/metrics-api_test.go @@ -0,0 +1,103 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package webapis + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var mSerial int64 + +func mMkCert(t *testing.T, cn string, notBefore, notAfter time.Time) *x509.Certificate { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + mSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(mSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return c +} + +func mScrapeRequest(peerCerts []*x509.Certificate) *http.Request { + r := httptest.NewRequest(http.MethodGet, "/metrics", nil) + if peerCerts != nil { + r.TLS = &tls.ConnectionState{PeerCertificates: peerCerts} + } + return r +} + +// Test_MetricsApi_ScrapeCertAuthorization covers the scrape-cert gate on the metrics endpoint. When a +// scrape cert is pinned, authorization must match the pinned cert against the presented LEAF only +// (PeerCertificates[0]), compare the full certificate, honor the validity window, and reject requests +// with no client certificate. Only the rejecting (401) paths are exercised here; they return before the +// handler consults the network. +func Test_MetricsApi_ScrapeCertAuthorization(t *testing.T) { + now := time.Now() + scrapeCert := mMkCert(t, "scrape", now.Add(-time.Hour), now.Add(time.Hour)) + + handler := (&MetricsApiHandler{scrapeCert: scrapeCert}).newHandler() + + assertUnauthorized := func(t *testing.T, peerCerts []*x509.Certificate) { + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, mScrapeRequest(peerCerts)) + require.Equal(t, http.StatusUnauthorized, rec.Code) + } + + t.Run("no client certificate", func(t *testing.T) { + assertUnauthorized(t, nil) + }) + + t.Run("presented leaf is not the scrape cert", func(t *testing.T) { + other := mMkCert(t, "other", now.Add(-time.Hour), now.Add(time.Hour)) + assertUnauthorized(t, []*x509.Certificate{other}) + }) + + t.Run("scrape cert presented only as a filler cert, not the leaf", func(t *testing.T) { + attacker := mMkCert(t, "attacker", now.Add(-time.Hour), now.Add(time.Hour)) + // The pinned scrape cert is present in the chain, but the leaf (index 0) is the attacker's. + assertUnauthorized(t, []*x509.Certificate{attacker, scrapeCert}) + }) + + t.Run("expired scrape cert presented as the leaf", func(t *testing.T) { + expired := mMkCert(t, "expired", now.Add(-2*time.Hour), now.Add(-time.Hour)) + expiredHandler := (&MetricsApiHandler{scrapeCert: expired}).newHandler() + rec := httptest.NewRecorder() + expiredHandler.ServeHTTP(rec, mScrapeRequest([]*x509.Certificate{expired})) + require.Equal(t, http.StatusUnauthorized, rec.Code, "a leaf outside its validity window must be rejected") + }) +} diff --git a/router/xlink_transport/connect_test.go b/router/xlink_transport/connect_test.go new file mode 100644 index 000000000..3f250a031 --- /dev/null +++ b/router/xlink_transport/connect_test.go @@ -0,0 +1,142 @@ +/* + (c) Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xlink_transport + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/identity" + "github.com/stretchr/testify/require" +) + +// caPoolIdentity is a minimal identity.Identity whose only useful method is CaPool. The link +// verification path consults nothing else on the identity, so the remaining interface methods are left +// to the embedded nil interface (never called on the paths exercised here). +type caPoolIdentity struct { + identity.Identity + pool *identity.CaPool +} + +func (f *caPoolIdentity) CaPool() *identity.CaPool { return f.pool } + +type ltCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var ltSerial int64 + +func ltMkCert(t *testing.T, cn string, isCA bool, signer *ltCertAndKey) *ltCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + ltSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(ltSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + } + if isCA { + tmpl.IsCA = true + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + } else { + tmpl.KeyUsage = x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return <CertAndKey{cert: c, key: key} +} + +func ltHandler(pool *identity.CaPool, token string) *ConnectionHandler { + return &ConnectionHandler{routerId: &identity.TokenId{Identity: &caPoolIdentity{pool: pool}, Token: token}} +} + +// Test_ConnectionHandler_RejectsUntrustedDialer covers incoming link verification: the dialing router +// must present a leaf that chains to the CA. A self-signed leaf, alone or backed by a scraped +// CA-chained filler certificate, must be rejected - verification is bound to the leaf, not to "some +// presented certificate chains". +func Test_ConnectionHandler_RejectsUntrustedDialer(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + handler := ltHandler(pool, "router1") + + legit := ltMkCert(t, "legit-router", false, inter) + forged := ltMkCert(t, "forged", false, nil) + + hello := &channel.Hello{Headers: channel.Headers{}} + + req.Error(handler.HandleConnection(hello, nil), "no certificates must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert}), + "self-signed leaf that does not chain to the CA must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert, legit.cert}), + "self-signed leaf backed by a scraped CA-chained filler cert must be rejected") +} + +func Test_ConnectionHandler_AcceptsTrustedDialer(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + handler := ltHandler(pool, "router1") + + legit := ltMkCert(t, "legit-router", false, inter) + req.NoError(handler.HandleConnection(&channel.Hello{Headers: channel.Headers{}}, []*x509.Certificate{legit.cert}), + "a leaf chaining to the CA must be accepted") +} + +// Test_ConnectionHandler_DialedRouterIdMismatch verifies the dial is rejected when it names a different +// target router, even if the presented certificate is otherwise valid. +func Test_ConnectionHandler_DialedRouterIdMismatch(t *testing.T) { + req := require.New(t) + + root := ltMkCert(t, "root", true, nil) + inter := ltMkCert(t, "int", true, root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + handler := ltHandler(pool, "router1") + legit := ltMkCert(t, "legit-router", false, inter) + + mismatch := channel.Headers{} + mismatch.PutStringHeader(LinkDialedRouterId, "some-other-router") + req.Error(handler.HandleConnection(&channel.Hello{Headers: mismatch}, []*x509.Certificate{legit.cert}), + "a link dial meant for a different router must be rejected") + + match := channel.Headers{} + match.PutStringHeader(LinkDialedRouterId, "router1") + req.NoError(handler.HandleConnection(&channel.Hello{Headers: match}, []*x509.Certificate{legit.cert}), + "a matching dialed router id with a trusted cert must be accepted") +} From dd54f108b40275478c8e6ded7f7914cfea0e4cc2 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 22:46:06 -0400 Subject: [PATCH 09/73] Make mesh peer certificate validation direction-aware - verifies an outbound dial peer's leaf (its TLS server certificate) against server-authentication key usage, and an inbound peer's leaf against client-authentication key usage, so a split client-auth/server-auth external PKI does not reject legitimate controller mesh connections - adds a server-auth verification helper alongside the client-auth one and covers both directions with tests, including the outbound server-certificate path in the live handshake test --- common/cert/verify.go | 36 +++++++++++++------ common/cert/verify_test.go | 35 ++++++++++++++++++ controller/raft/mesh/mesh.go | 23 +++++++----- .../mesh/mesh_peer_cert_validation_test.go | 13 +++++-- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/common/cert/verify.go b/common/cert/verify.go index 8021f1813..360f65e22 100644 --- a/common/cert/verify.go +++ b/common/cert/verify.go @@ -25,17 +25,33 @@ import ( ) // VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool, and returns that verified leaf. Only certs[0] is verified: it is the -// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a -// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate -// intermediates (added alongside the pool's intermediates), never as independent grounds for -// acceptance. +// root in the given CA pool with client-authentication extended key usage, and returns that verified +// leaf. Use this for inbound connections, where the peer's leaf is its TLS client certificate. See +// verifyCertChain for how the leaf and any certs[1:] are treated. // -// Client-authentication extended key usage is required. A certificate with no ExtKeyUsage extension -// is unrestricted and passes (the form ziti pki produces); a leaf whose EKU excludes client -// authentication is rejected. Callers that must accept server-auth-only client identities should -// verify with an explicit {ClientAuth, ServerAuth} usage set instead. +// A certificate with no ExtKeyUsage extension is unrestricted and passes (the form ziti pki produces); +// a leaf whose EKU excludes client authentication is rejected. func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + return verifyCertChain(pool, certs, x509.ExtKeyUsageClientAuth) +} + +// VerifyServerCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// root in the given CA pool with server-authentication extended key usage, and returns that verified +// leaf. Use this for outbound connections, where the peer's leaf is its TLS server certificate: an +// external PKI may issue distinct client-auth and server-auth certificates, and the peer of an +// outbound dial presents the latter. See verifyCertChain for how the leaf and any certs[1:] are +// treated. +func VerifyServerCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + return verifyCertChain(pool, certs, x509.ExtKeyUsageServerAuth) +} + +// verifyCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted root in +// the given CA pool with the requested extended key usage, and returns that verified leaf. Only certs[0] +// is verified: it is the certificate whose private key the TLS handshake proved the peer holds, and it +// is the certificate a caller derives peer identity from. Any remaining certs[1:] are treated only as +// candidate intermediates (added alongside the pool's intermediates), never as independent grounds for +// acceptance. +func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage x509.ExtKeyUsage) (*x509.Certificate, error) { if pool == nil { return nil, errors.New("no ca pool provided") } @@ -52,7 +68,7 @@ func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x if _, err := certs[0].Verify(x509.VerifyOptions{ Roots: pool.RootsAsStdPool(), Intermediates: intermediates, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsages: []x509.ExtKeyUsage{keyUsage}, }); err != nil { return nil, fmt.Errorf("leaf certificate not trusted: %w", err) } diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go index 27bfcdfbd..65b95abf4 100644 --- a/common/cert/verify_test.go +++ b/common/cert/verify_test.go @@ -170,3 +170,38 @@ func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { _, err = VerifyClientCertChain(pool, nil) req.Error(err, "no certs rejected") } + +// TestVerifyCertChain_DirectionAwareEKU verifies that the client-auth and server-auth variants each +// enforce their own extended key usage. An external PKI may issue separate client-auth and server-auth +// certificates; verifying with the wrong direction (e.g. requiring client auth of a peer's server +// certificate on an outbound connection) would reject a legitimate peer. A leaf carrying both usages, +// or none at all, satisfies either variant. +func TestVerifyCertChain_DirectionAwareEKU(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + clientOnly := vMkLeaf(t, "client-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + both := vMkLeaf(t, "both", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter) + none := vMkLeaf(t, "none", "/identity/real", nil, inter) + + _, err := VerifyClientCertChain(pool, []*x509.Certificate{clientOnly.cert}) + req.NoError(err, "client-auth leaf accepted for inbound direction") + _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.Error(err, "server-auth-only leaf rejected for inbound direction") + + _, err = VerifyServerCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.NoError(err, "server-auth leaf accepted for outbound direction") + _, err = VerifyServerCertChain(pool, []*x509.Certificate{clientOnly.cert}) + req.Error(err, "client-auth-only leaf rejected for outbound direction") + + // A leaf carrying both usages, or none, satisfies either direction (the common ziti pki case). + for _, c := range []*x509.Certificate{both.cert, none.cert} { + _, err = VerifyClientCertChain(pool, []*x509.Certificate{c}) + req.NoError(err) + _, err = VerifyServerCertChain(pool, []*x509.Certificate{c}) + req.NoError(err) + } +} diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index d2f4a84e5..997a06664 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -512,7 +512,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer peer.Channel = binding.GetChannel() - if err = self.validateConnection(peer.Channel); err != nil { + if err = self.validateConnection(peer.Channel, true); err != nil { return err } @@ -581,12 +581,12 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer return peer, nil } -func (self *impl) validateConnection(ch channel.Channel) error { +func (self *impl) validateConnection(ch channel.Channel, dialed bool) error { if err := self.checkClusterIds(ch); err != nil { return err } - return self.checkCerts(ch) + return self.checkCerts(ch, dialed) } func (self *impl) checkClusterIds(ch channel.Channel) error { @@ -598,10 +598,17 @@ func (self *impl) checkClusterIds(ch channel.Channel) error { return nil } -func (self *impl) checkCerts(ch channel.Channel) error { +func (self *impl) checkCerts(ch channel.Channel, dialed bool) error { // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that - // must chain to a trusted root; VerifyClientCertChain verifies that leaf specifically. - if _, err := cert.VerifyClientCertChain(self.env.GetNodeId().CaPool(), ch.Underlay().Certificates()); err != nil { + // must chain to a trusted root. The required key usage is direction-dependent: on an inbound + // connection certs[0] is the peer's client certificate, while on an outbound dial it is the peer's + // server certificate. An external PKI may issue these with distinct EKUs, so verifying an outbound + // peer's server certificate against client-auth usage would reject a legitimate controller. + verify := cert.VerifyClientCertChain + if dialed { + verify = cert.VerifyServerCertChain + } + if _, err := verify(self.env.GetNodeId().CaPool(), ch.Underlay().Certificates()); err != nil { return fmt.Errorf("unable to validate peer connection: %w", err) } return nil @@ -651,7 +658,7 @@ func (self *impl) GetPeerInfo(address string, timeout time.Duration) (raft.Serve return err } - if err = self.validateConnection(binding.GetChannel()); err != nil { + if err = self.validateConnection(binding.GetChannel(), true); err != nil { return err } @@ -871,7 +878,7 @@ func (self *impl) AcceptUnderlay(underlay channel.Underlay) error { peer.PreferredLeader = true } - if err = self.validateConnection(peer.Channel); err != nil { + if err = self.validateConnection(peer.Channel, false); err != nil { return err } diff --git a/controller/raft/mesh/mesh_peer_cert_validation_test.go b/controller/raft/mesh/mesh_peer_cert_validation_test.go index cd90fb684..0d88f4fe2 100644 --- a/controller/raft/mesh/mesh_peer_cert_validation_test.go +++ b/controller/raft/mesh/mesh_peer_cert_validation_test.go @@ -19,8 +19,8 @@ package mesh // End-to-end validation of the mesh peer certificate check over a real TCP+TLS socket, using the // production controller TLS config (identity.ServerTLSConfig). The ctrl listener uses // RequireAnyClientCert and does not verify the client chain, so the handshake completes for any -// presented client leaf; peer admission is enforced afterward by verifyClientCertChain against the -// node CA pool. Unit-level coverage of the shared check lives in common/cert. +// presented client leaf; peer admission is enforced afterward by the direction-aware cert-chain check +// against the node CA pool. Unit-level coverage of the shared check lives in common/cert. import ( "crypto/ecdsa" @@ -212,6 +212,15 @@ func Test_MeshPeerCert_LiveHandshake(t *testing.T) { req.Equal("node-server", extra.Subject.CommonName) <-results + // Outbound-dial direction: the peer's leaf is its TLS server certificate (here the scraped + // server-auth-only cert). It must pass server-auth verification but fail client-auth verification, + // which is why mesh validation is direction-aware - verifying an outbound peer's server certificate + // against client-auth usage would reject a legitimate controller under a split-EKU external PKI. + _, err = cert.VerifyServerCertChain(pool, serverPresented) + req.NoError(err, "outbound direction accepts the peer's server-auth certificate") + _, err = cert.VerifyClientCertChain(pool, serverPresented) + req.Error(err, "the same server-auth-only certificate fails client-auth verification") + // Rogue peer: self-signed identity leaf + the scraped extra cert. Handshake completes; the mesh // check must reject it because the identity leaf (certs[0]) does not chain to the CA. rogue := mkLeaf(t, "rogue", "/controller/victim-id", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) From f347f1a24614d0c4830a6fe89657391ec2c33cd7 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 21 Jul 2026 16:53:52 -0400 Subject: [PATCH 10/73] Verify peer leaf against full CA bundle without EKU restriction - verifies the presented leaf against the node's full trusted-CA pool (identity.CA()) instead of only self-signed roots, so intermediates distributed as trust anchors and multi-root bundles are honored - drops the client/server-auth extended-key-usage requirement so an externally managed PKI with arbitrary or absent EKUs is not rejected - removes the now-unnecessary direction-aware mesh validation, which only existed to work around the EKU requirement --- common/cert/verify.go | 49 ++---- common/cert/verify_test.go | 153 +++++++++--------- controller/raft/mesh/mesh.go | 26 ++- .../mesh/mesh_peer_cert_validation_test.go | 19 +-- router/xlink_transport/connect.go | 2 +- router/xlink_transport/connect_test.go | 34 ++-- 6 files changed, 135 insertions(+), 148 deletions(-) diff --git a/common/cert/verify.go b/common/cert/verify.go index 360f65e22..c569fb4b7 100644 --- a/common/cert/verify.go +++ b/common/cert/verify.go @@ -20,39 +20,22 @@ import ( "crypto/x509" "errors" "fmt" - - "github.com/openziti/identity" ) -// VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool with client-authentication extended key usage, and returns that verified -// leaf. Use this for inbound connections, where the peer's leaf is its TLS client certificate. See -// verifyCertChain for how the leaf and any certs[1:] are treated. +// VerifyLeafCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// certificate in the given pool, and returns that verified leaf. Only certs[0] is verified: it is the +// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a +// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate +// intermediates supplied by the peer, never as independent grounds for acceptance - so a peer cannot be +// admitted by presenting its own leaf alongside some other certificate that happens to chain. // -// A certificate with no ExtKeyUsage extension is unrestricted and passes (the form ziti pki produces); -// a leaf whose EKU excludes client authentication is rejected. -func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { - return verifyCertChain(pool, certs, x509.ExtKeyUsageClientAuth) -} - -// VerifyServerCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool with server-authentication extended key usage, and returns that verified -// leaf. Use this for outbound connections, where the peer's leaf is its TLS server certificate: an -// external PKI may issue distinct client-auth and server-auth certificates, and the peer of an -// outbound dial presents the latter. See verifyCertChain for how the leaf and any certs[1:] are -// treated. -func VerifyServerCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { - return verifyCertChain(pool, certs, x509.ExtKeyUsageServerAuth) -} - -// verifyCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted root in -// the given CA pool with the requested extended key usage, and returns that verified leaf. Only certs[0] -// is verified: it is the certificate whose private key the TLS handshake proved the peer holds, and it -// is the certificate a caller derives peer identity from. Any remaining certs[1:] are treated only as -// candidate intermediates (added alongside the pool's intermediates), never as independent grounds for -// acceptance. -func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage x509.ExtKeyUsage) (*x509.Certificate, error) { - if pool == nil { +// roots is the verifying node's full trusted-CA pool (identity.CA()). Every certificate in it is a valid +// chain terminus, whether a self-signed root or an intermediate distributed as a trust anchor, matching +// how the node's TLS configuration establishes trust. No extended-key-usage restriction is applied: +// certificates issued by an external PKI with arbitrary or absent EKUs are accepted as long as the leaf +// chains to a trusted CA. +func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate) (*x509.Certificate, error) { + if roots == nil { return nil, errors.New("no ca pool provided") } @@ -60,15 +43,15 @@ func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage return nil, errors.New("no certificates presented") } - intermediates := pool.IntermediatesAsStdPool() + intermediates := x509.NewCertPool() for _, intermediate := range certs[1:] { intermediates.AddCert(intermediate) } if _, err := certs[0].Verify(x509.VerifyOptions{ - Roots: pool.RootsAsStdPool(), + Roots: roots, Intermediates: intermediates, - KeyUsages: []x509.ExtKeyUsage{keyUsage}, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }); err != nil { return nil, fmt.Errorf("leaf certificate not trusted: %w", err) } diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go index 65b95abf4..7b64688b3 100644 --- a/common/cert/verify_test.go +++ b/common/cert/verify_test.go @@ -27,7 +27,6 @@ import ( "testing" "time" - "github.com/openziti/identity" "github.com/stretchr/testify/require" ) @@ -99,109 +98,115 @@ func vMkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signe return &vCertAndKey{cert: c, key: key} } -// TestVerifyClientCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to -// the CA is rejected even when another presented certificate does chain - i.e. verification is bound -// to the leaf, not to "some presented certificate verifies". -func TestVerifyClientCertChain_RejectsUnchainedLeaf(t *testing.T) { - req := require.New(t) - root := vMkCA(t, "root", nil) - inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) - - // A trust-domain-signed cert (chains to the CA) presented as an extra certificate. - extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - // The leaf itself is self-signed and does NOT chain to the CA. - unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) - - _, err := VerifyClientCertChain(pool, []*x509.Certificate{unchained.cert, extra.cert}) - req.Error(err, "leaf not chaining to the CA must be rejected even alongside a chaining extra cert") +func vPoolOf(certs ...*x509.Certificate) *x509.CertPool { + pool := x509.NewCertPool() + for _, c := range certs { + pool.AddCert(c) + } + return pool } -func TestVerifyClientCertChain_AcceptsLegitLeaf(t *testing.T) { +// TestVerifyLeafCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to a +// trusted CA is rejected even when another presented certificate does chain - i.e. verification is bound +// to the leaf, not to "some presented certificate verifies". +func TestVerifyLeafCertChain_RejectsUnchainedLeaf(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + roots := vPoolOf(root.cert, inter.cert) + + // A trust-anchored cert (chains to the pool) presented as an extra certificate. + extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + // The leaf itself is self-signed and does NOT chain to the pool. + unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{unchained.cert, extra.cert}) + req.Error(err, "leaf not chaining to the pool must be rejected even alongside a chaining extra cert") +} + +func TestVerifyLeafCertChain_AcceptsLegitLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + roots := vPoolOf(root.cert, inter.cert) legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - leaf, err := VerifyClientCertChain(pool, []*x509.Certificate{legit.cert}) + leaf, err := VerifyLeafCertChain(roots, []*x509.Certificate{legit.cert}) req.NoError(err) req.Equal(legit.cert, leaf, "returns the verified leaf (certs[0]) for identity use") } -func TestVerifyClientCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { +func TestVerifyLeafCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) // Pool holds ONLY the root; the intermediate must be supplied on the wire (certs[1:]). - rootOnly := identity.NewCaPool([]*x509.Certificate{root.cert}) - _, err := VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert}) + rootOnly := vPoolOf(root.cert) + _, err := VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert}) req.Error(err, "without the intermediate anywhere the leaf cannot be verified") - _, err = VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) + _, err = VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) req.NoError(err, "peer-supplied intermediate lets a valid peer verify") } -func TestVerifyClientCertChain_EKUCompat(t *testing.T) { +// TestVerifyLeafCertChain_MultipleRoots covers a trust bundle with more than one self-signed root (as a +// quickstart deployment produces, concatenating a controller root and a signer root). A leaf chaining to +// either root must verify. +func TestVerifyLeafCertChain_MultipleRoots(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signerRoot := vMkCA(t, "signer-root", nil) + signerInter := vMkCA(t, "signer-int", signerRoot) + roots := vPoolOf(ctrlRoot.cert, signerRoot.cert) + + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signerInter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signerInter.cert}) + req.NoError(err, "a leaf chaining to one of several trusted roots must verify") +} + +// TestVerifyLeafCertChain_IntermediateAsTrustAnchor covers a self-managed PKI that distributes a +// (non-self-signed) intermediate as the trust anchor without its root. Every certificate in the pool is +// a valid chain terminus, so a leaf chaining directly to that intermediate must verify. +func TestVerifyLeafCertChain_IntermediateAsTrustAnchor(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + leaf := vMkLeaf(t, "leaf", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - // No ExtKeyUsage extension (the form ziti pki produces) is unrestricted -> accepted. - noEKU := vMkLeaf(t, "ziti-pki", "/identity/real", nil, inter) - _, err := VerifyClientCertChain(pool, []*x509.Certificate{noEKU.cert}) - req.NoError(err, "no-EKU leaf accepted (ziti pki backward compat)") - - // EKU present but excludes client auth (server-auth only) -> rejected. - serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.Error(err, "server-auth-only leaf rejected by the client-auth requirement") + interAnchored := vPoolOf(inter.cert) // only the intermediate distributed, no self-signed root + _, err := VerifyLeafCertChain(interAnchored, []*x509.Certificate{leaf.cert}) + req.NoError(err, "an intermediate distributed as a trust anchor must be accepted as a chain terminus") } -func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { - req := require.New(t) - root := vMkCA(t, "root", nil) - pool := identity.NewCaPool([]*x509.Certificate{root.cert}) - - _, err := VerifyClientCertChain(nil, []*x509.Certificate{root.cert}) - req.Error(err, "nil pool rejected") - _, err = VerifyClientCertChain(pool, nil) - req.Error(err, "no certs rejected") -} - -// TestVerifyCertChain_DirectionAwareEKU verifies that the client-auth and server-auth variants each -// enforce their own extended key usage. An external PKI may issue separate client-auth and server-auth -// certificates; verifying with the wrong direction (e.g. requiring client auth of a peer's server -// certificate on an outbound connection) would reject a legitimate peer. A leaf carrying both usages, -// or none at all, satisfies either variant. -func TestVerifyCertChain_DirectionAwareEKU(t *testing.T) { +// TestVerifyLeafCertChain_ArbitraryEKU covers external PKIs whose certificates carry arbitrary or absent +// extended key usages. No EKU restriction is applied, so all of them verify as long as the chain is +// valid. +func TestVerifyLeafCertChain_ArbitraryEKU(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + roots := vPoolOf(root.cert, inter.cert) - clientOnly := vMkLeaf(t, "client-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - both := vMkLeaf(t, "both", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter) - none := vMkLeaf(t, "none", "/identity/real", nil, inter) - - _, err := VerifyClientCertChain(pool, []*x509.Certificate{clientOnly.cert}) - req.NoError(err, "client-auth leaf accepted for inbound direction") - _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.Error(err, "server-auth-only leaf rejected for inbound direction") - - _, err = VerifyServerCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.NoError(err, "server-auth leaf accepted for outbound direction") - _, err = VerifyServerCertChain(pool, []*x509.Certificate{clientOnly.cert}) - req.Error(err, "client-auth-only leaf rejected for outbound direction") - - // A leaf carrying both usages, or none, satisfies either direction (the common ziti pki case). - for _, c := range []*x509.Certificate{both.cert, none.cert} { - _, err = VerifyClientCertChain(pool, []*x509.Certificate{c}) - req.NoError(err) - _, err = VerifyServerCertChain(pool, []*x509.Certificate{c}) - req.NoError(err) + for _, ekus := range [][]x509.ExtKeyUsage{ + nil, + {x509.ExtKeyUsageClientAuth}, + {x509.ExtKeyUsageServerAuth}, + {x509.ExtKeyUsageEmailProtection}, + } { + leaf := vMkLeaf(t, "leaf", "/identity/real", ekus, inter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}) + req.NoError(err, "leaf with EKU %v must be accepted", ekus) } } + +func TestVerifyLeafCertChain_EmptyInputs(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + roots := vPoolOf(root.cert) + + _, err := VerifyLeafCertChain(nil, []*x509.Certificate{root.cert}) + req.Error(err, "nil pool rejected") + _, err = VerifyLeafCertChain(roots, nil) + req.Error(err, "no certs rejected") +} diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index 997a06664..4c201ddb3 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -512,7 +512,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer peer.Channel = binding.GetChannel() - if err = self.validateConnection(peer.Channel, true); err != nil { + if err = self.validateConnection(peer.Channel); err != nil { return err } @@ -581,12 +581,12 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer return peer, nil } -func (self *impl) validateConnection(ch channel.Channel, dialed bool) error { +func (self *impl) validateConnection(ch channel.Channel) error { if err := self.checkClusterIds(ch); err != nil { return err } - return self.checkCerts(ch, dialed) + return self.checkCerts(ch) } func (self *impl) checkClusterIds(ch channel.Channel) error { @@ -598,17 +598,11 @@ func (self *impl) checkClusterIds(ch channel.Channel) error { return nil } -func (self *impl) checkCerts(ch channel.Channel, dialed bool) error { - // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that - // must chain to a trusted root. The required key usage is direction-dependent: on an inbound - // connection certs[0] is the peer's client certificate, while on an outbound dial it is the peer's - // server certificate. An external PKI may issue these with distinct EKUs, so verifying an outbound - // peer's server certificate against client-auth usage would reject a legitimate controller. - verify := cert.VerifyClientCertChain - if dialed { - verify = cert.VerifyServerCertChain - } - if _, err := verify(self.env.GetNodeId().CaPool(), ch.Underlay().Certificates()); err != nil { +func (self *impl) checkCerts(ch channel.Channel) error { + // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that must + // chain to a trusted CA; VerifyLeafCertChain verifies that leaf specifically against the node's full + // trusted-CA pool. + if _, err := cert.VerifyLeafCertChain(self.env.GetNodeId().CA(), ch.Underlay().Certificates()); err != nil { return fmt.Errorf("unable to validate peer connection: %w", err) } return nil @@ -658,7 +652,7 @@ func (self *impl) GetPeerInfo(address string, timeout time.Duration) (raft.Serve return err } - if err = self.validateConnection(binding.GetChannel(), true); err != nil { + if err = self.validateConnection(binding.GetChannel()); err != nil { return err } @@ -878,7 +872,7 @@ func (self *impl) AcceptUnderlay(underlay channel.Underlay) error { peer.PreferredLeader = true } - if err = self.validateConnection(peer.Channel, false); err != nil { + if err = self.validateConnection(peer.Channel); err != nil { return err } diff --git a/controller/raft/mesh/mesh_peer_cert_validation_test.go b/controller/raft/mesh/mesh_peer_cert_validation_test.go index 0d88f4fe2..d1f9420df 100644 --- a/controller/raft/mesh/mesh_peer_cert_validation_test.go +++ b/controller/raft/mesh/mesh_peer_cert_validation_test.go @@ -195,7 +195,7 @@ func Test_MeshPeerCert_LiveHandshake(t *testing.T) { } }() - pool := id.CaPool() + ca := id.CA() // Scrape the node's own server certificate from the listener (a throwaway client cert satisfies // RequireAnyClientCert). This is the "extra" certificate the rogue peer will present. @@ -212,14 +212,11 @@ func Test_MeshPeerCert_LiveHandshake(t *testing.T) { req.Equal("node-server", extra.Subject.CommonName) <-results - // Outbound-dial direction: the peer's leaf is its TLS server certificate (here the scraped - // server-auth-only cert). It must pass server-auth verification but fail client-auth verification, - // which is why mesh validation is direction-aware - verifying an outbound peer's server certificate - // against client-auth usage would reject a legitimate controller under a split-EKU external PKI. - _, err = cert.VerifyServerCertChain(pool, serverPresented) - req.NoError(err, "outbound direction accepts the peer's server-auth certificate") - _, err = cert.VerifyClientCertChain(pool, serverPresented) - req.Error(err, "the same server-auth-only certificate fails client-auth verification") + // The scraped server certificate is server-auth-only, yet it chains to the node CA. Verification is + // EKU-agnostic and anchors on the node's full trusted-CA pool, so it is accepted - this is the + // certificate an outbound dial would present as its leaf. + _, err = cert.VerifyLeafCertChain(ca, serverPresented) + req.NoError(err, "a server-auth leaf that chains to the node CA is accepted") // Rogue peer: self-signed identity leaf + the scraped extra cert. Handshake completes; the mesh // check must reject it because the identity leaf (certs[0]) does not chain to the CA. @@ -232,7 +229,7 @@ func Test_MeshPeerCert_LiveHandshake(t *testing.T) { _ = rogueConn.Close() rogueRes := <-results req.NoError(rogueRes.err) - _, err = cert.VerifyClientCertChain(pool, rogueRes.peer) + _, err = cert.VerifyLeafCertChain(ca, rogueRes.peer) req.Error(err, "mesh check rejects a self-signed identity leaf backed by a scraped extra cert") // Legitimate peer: CA-signed client leaf. Handshake completes and the mesh check accepts it. @@ -244,6 +241,6 @@ func Test_MeshPeerCert_LiveHandshake(t *testing.T) { _ = legitConn.Close() legitRes := <-results req.NoError(legitRes.err) - _, err = cert.VerifyClientCertChain(pool, legitRes.peer) + _, err = cert.VerifyLeafCertChain(ca, legitRes.peer) req.NoError(err, "mesh check accepts a legitimate CA-signed peer leaf") } diff --git a/router/xlink_transport/connect.go b/router/xlink_transport/connect.go index 27f219241..22787abf2 100644 --- a/router/xlink_transport/connect.go +++ b/router/xlink_transport/connect.go @@ -47,7 +47,7 @@ func (self *ConnectionHandler) HandleConnection(hello *channel.Hello, certificat } } - if _, err := cert.VerifyClientCertChain(self.routerId.CaPool(), certificates); err != nil { + if _, err := cert.VerifyLeafCertChain(self.routerId.CA(), certificates); err != nil { return fmt.Errorf("unable to verify dialing router: %w", err) } diff --git a/router/xlink_transport/connect_test.go b/router/xlink_transport/connect_test.go index 3f250a031..4eb496d18 100644 --- a/router/xlink_transport/connect_test.go +++ b/router/xlink_transport/connect_test.go @@ -31,15 +31,23 @@ import ( "github.com/stretchr/testify/require" ) -// caPoolIdentity is a minimal identity.Identity whose only useful method is CaPool. The link -// verification path consults nothing else on the identity, so the remaining interface methods are left -// to the embedded nil interface (never called on the paths exercised here). +// caPoolIdentity is a minimal identity.Identity whose only useful method is CA. The link verification +// path consults nothing else on the identity, so the remaining interface methods are left to the +// embedded nil interface (never called on the paths exercised here). type caPoolIdentity struct { identity.Identity - pool *identity.CaPool + roots *x509.CertPool } -func (f *caPoolIdentity) CaPool() *identity.CaPool { return f.pool } +func (f *caPoolIdentity) CA() *x509.CertPool { return f.roots } + +func ltPool(certs ...*x509.Certificate) *x509.CertPool { + p := x509.NewCertPool() + for _, c := range certs { + p.AddCert(c) + } + return p +} type ltCertAndKey struct { cert *x509.Certificate @@ -78,8 +86,8 @@ func ltMkCert(t *testing.T, cn string, isCA bool, signer *ltCertAndKey) *ltCertA return <CertAndKey{cert: c, key: key} } -func ltHandler(pool *identity.CaPool, token string) *ConnectionHandler { - return &ConnectionHandler{routerId: &identity.TokenId{Identity: &caPoolIdentity{pool: pool}, Token: token}} +func ltHandler(roots *x509.CertPool, token string) *ConnectionHandler { + return &ConnectionHandler{routerId: &identity.TokenId{Identity: &caPoolIdentity{roots: roots}, Token: token}} } // Test_ConnectionHandler_RejectsUntrustedDialer covers incoming link verification: the dialing router @@ -91,8 +99,8 @@ func Test_ConnectionHandler_RejectsUntrustedDialer(t *testing.T) { root := ltMkCert(t, "root", true, nil) inter := ltMkCert(t, "int", true, root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) - handler := ltHandler(pool, "router1") + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") legit := ltMkCert(t, "legit-router", false, inter) forged := ltMkCert(t, "forged", false, nil) @@ -111,8 +119,8 @@ func Test_ConnectionHandler_AcceptsTrustedDialer(t *testing.T) { root := ltMkCert(t, "root", true, nil) inter := ltMkCert(t, "int", true, root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) - handler := ltHandler(pool, "router1") + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") legit := ltMkCert(t, "legit-router", false, inter) req.NoError(handler.HandleConnection(&channel.Hello{Headers: channel.Headers{}}, []*x509.Certificate{legit.cert}), @@ -126,8 +134,8 @@ func Test_ConnectionHandler_DialedRouterIdMismatch(t *testing.T) { root := ltMkCert(t, "root", true, nil) inter := ltMkCert(t, "int", true, root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) - handler := ltHandler(pool, "router1") + roots := ltPool(root.cert, inter.cert) + handler := ltHandler(roots, "router1") legit := ltMkCert(t, "legit-router", false, inter) mismatch := channel.Headers{} From 6fe7fabebfb7b06ffd086c9923b70a99c9ec87e8 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 27 Jul 2026 14:05:17 -0400 Subject: [PATCH 11/73] Add security advisories to 2.0.2 release notes - adds the two control-plane security advisories (GHSA-mrpr-756c-xm47 critical, GHSA-cc5m-7mhm-xh9f medium) and the bbolt memory-corruption fix (#4108) to the existing 2.0.2 release notes --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 485a72397..be2b9a15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,31 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes +## Security Advisories + +This release addresses two control-plane certificate and identity validation vulnerabilities. See the linked +GitHub Security Advisories for full details, impact, and affected versions. + +* [GHSA-mrpr-756c-xm47](https://github.com/openziti/ziti/security/advisories/GHSA-mrpr-756c-xm47) (CVE pending) (Critical) - Improper peer certificate validation on the controller + cluster mesh, router links, and metrics endpoint. TLS peer checks accepted a connection when any presented + certificate chained to the trusted CA while taking the peer identity from the leaf certificate, allowing a + peer to be admitted under a forged identity without possessing a trusted key. On HA/clustered controllers + this allows joining the controller cluster as an arbitrary controller. +* [GHSA-cc5m-7mhm-xh9f](https://github.com/openziti/ziti/security/advisories/GHSA-cc5m-7mhm-xh9f) (CVE pending) (Medium) - Control-channel connections carrying a channel-type header + bypassed router certificate and identity verification, allowing an attacker that can reach the controller + control port to be admitted as an arbitrary router identity and manipulate that router's fabric terminators, + faults, and circuit routing. Impact is limited to router data model metadata (service and identity names) and + control-plane manipulation; it does not by itself grant access to the services the network protects. + ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.0...v2.0.1) * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user + * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory # Release 2.0.1 From d0440227bb1e5dcf10e7e4912f4474af9e935fc6 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 14:25:05 -0400 Subject: [PATCH 12/73] Add shared client certificate chain verification helper --- common/cert/verify.go | 61 +++++++++++++ common/cert/verify_test.go | 172 +++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 common/cert/verify.go create mode 100644 common/cert/verify_test.go diff --git a/common/cert/verify.go b/common/cert/verify.go new file mode 100644 index 000000000..8021f1813 --- /dev/null +++ b/common/cert/verify.go @@ -0,0 +1,61 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/x509" + "errors" + "fmt" + + "github.com/openziti/identity" +) + +// VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// root in the given CA pool, and returns that verified leaf. Only certs[0] is verified: it is the +// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a +// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate +// intermediates (added alongside the pool's intermediates), never as independent grounds for +// acceptance. +// +// Client-authentication extended key usage is required. A certificate with no ExtKeyUsage extension +// is unrestricted and passes (the form ziti pki produces); a leaf whose EKU excludes client +// authentication is rejected. Callers that must accept server-auth-only client identities should +// verify with an explicit {ClientAuth, ServerAuth} usage set instead. +func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + if pool == nil { + return nil, errors.New("no ca pool provided") + } + + if len(certs) == 0 { + return nil, errors.New("no certificates presented") + } + + intermediates := pool.IntermediatesAsStdPool() + for _, intermediate := range certs[1:] { + intermediates.AddCert(intermediate) + } + + if _, err := certs[0].Verify(x509.VerifyOptions{ + Roots: pool.RootsAsStdPool(), + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }); err != nil { + return nil, fmt.Errorf("leaf certificate not trusted: %w", err) + } + + return certs[0], nil +} diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go new file mode 100644 index 000000000..27bfcdfbd --- /dev/null +++ b/common/cert/verify_test.go @@ -0,0 +1,172 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cert + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net/url" + "testing" + "time" + + "github.com/openziti/identity" + "github.com/stretchr/testify/require" +) + +const vTestTrustDomain = "spiffe://verify-test" + +type vCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var vSerial int64 + +func vNextSerial() *big.Int { + vSerial++ + return big.NewInt(vSerial) +} + +func vMkCA(t *testing.T, cn string, parent *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + signParent, signKey := tmpl, key + if parent != nil { + signParent, signKey = parent.cert, parent.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +// vMkLeaf builds an end-entity cert. signer==nil yields a leaf that does not chain to any CA. A nil +// ekus produces a cert with no ExtKeyUsage extension (unrestricted). +func vMkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signer *vCertAndKey) *vCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl := &x509.Certificate{ + SerialNumber: vNextSerial(), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: ekus, + } + if spiffePath != "" { + u, err := url.Parse(vTestTrustDomain + spiffePath) + require.NoError(t, err) + tmpl.URIs = []*url.URL{u} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &vCertAndKey{cert: c, key: key} +} + +// TestVerifyClientCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to +// the CA is rejected even when another presented certificate does chain - i.e. verification is bound +// to the leaf, not to "some presented certificate verifies". +func TestVerifyClientCertChain_RejectsUnchainedLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + // A trust-domain-signed cert (chains to the CA) presented as an extra certificate. + extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + // The leaf itself is self-signed and does NOT chain to the CA. + unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + + _, err := VerifyClientCertChain(pool, []*x509.Certificate{unchained.cert, extra.cert}) + req.Error(err, "leaf not chaining to the CA must be rejected even alongside a chaining extra cert") +} + +func TestVerifyClientCertChain_AcceptsLegitLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + leaf, err := VerifyClientCertChain(pool, []*x509.Certificate{legit.cert}) + req.NoError(err) + req.Equal(legit.cert, leaf, "returns the verified leaf (certs[0]) for identity use") +} + +func TestVerifyClientCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + + // Pool holds ONLY the root; the intermediate must be supplied on the wire (certs[1:]). + rootOnly := identity.NewCaPool([]*x509.Certificate{root.cert}) + _, err := VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert}) + req.Error(err, "without the intermediate anywhere the leaf cannot be verified") + _, err = VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) + req.NoError(err, "peer-supplied intermediate lets a valid peer verify") +} + +func TestVerifyClientCertChain_EKUCompat(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + // No ExtKeyUsage extension (the form ziti pki produces) is unrestricted -> accepted. + noEKU := vMkLeaf(t, "ziti-pki", "/identity/real", nil, inter) + _, err := VerifyClientCertChain(pool, []*x509.Certificate{noEKU.cert}) + req.NoError(err, "no-EKU leaf accepted (ziti pki backward compat)") + + // EKU present but excludes client auth (server-auth only) -> rejected. + serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.Error(err, "server-auth-only leaf rejected by the client-auth requirement") +} + +func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + pool := identity.NewCaPool([]*x509.Certificate{root.cert}) + + _, err := VerifyClientCertChain(nil, []*x509.Certificate{root.cert}) + req.Error(err, "nil pool rejected") + _, err = VerifyClientCertChain(pool, nil) + req.Error(err, "no certs rejected") +} From abf9cf8b1be04cbe121b4fd2244fb9689e5c36fe Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Thu, 16 Jul 2026 16:02:56 -0400 Subject: [PATCH 13/73] Validate router certificates on typed control channel connections Runs the router certificate fingerprint validation for router control-channel underlay types, which was previously skipped for any connection carrying a channel type header. Connections of other types (e.g. the raft mesh) continue to be deferred to their own acceptor. The already-connected / churn guard is applied only when establishing a new channel, so additional underlays of a grouped control channel are not rejected while the router is already connected. --- controller/controller.go | 16 +++- controller/handler_ctrl/connect.go | 109 ++++++++++++++---------- controller/handler_ctrl/connect_test.go | 75 ++++++++++++++++ router/env/ctrls.go | 15 +++- 4 files changed, 164 insertions(+), 51 deletions(-) create mode 100644 controller/handler_ctrl/connect_test.go diff --git a/controller/controller.go b/controller/controller.go index 9f584c5f8..762188648 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -537,9 +537,6 @@ func (c *Controller) Run() error { pfxlog.Logger().Infof("staring control channel listener on %s", c.config.Ctrl.Listener.String()) ctrlListener := channel.NewClassicListener(c.config.Id, c.config.Ctrl.Listener, ctrlChannelListenerConfig) c.ctrlListener = ctrlListener - if err := c.ctrlListener.Listen(c.ctrlConnectHandler); err != nil { - panic(err) - } ctrlAccepter := handler_ctrl.NewCtrlAccepter(c.network, c.xctrls, c.config.Ctrl.Options.Options, c.config.Ctrl.Options.RouterHeartbeatOptions, c.config.Trace.Handler) @@ -549,6 +546,19 @@ func (c *Controller) Run() error { ctrlAcceptors[mesh.ChannelTypeMesh] = c.raftController.GetMesh() } + // Channel types routed to a dedicated acceptor above validate their own peers; the connect handler + // must skip exactly those and validate everything else (which the dispatcher routes to the default + // router control acceptor, including unrecognized types). Set this before accepting connections. + separatelyValidatedTypes := map[string]struct{}{} + for chType := range ctrlAcceptors { + separatelyValidatedTypes[chType] = struct{}{} + } + c.ctrlConnectHandler.SetSeparatelyValidatedChannelTypes(separatelyValidatedTypes) + + if err := c.ctrlListener.Listen(c.ctrlConnectHandler); err != nil { + panic(err) + } + underlayDispatcher := channel.NewUnderlayDispatcher(channel.UnderlayDispatcherConfig{ Listener: ctrlListener, ConnectTimeout: c.config.Ctrl.Options.ConnectTimeout, diff --git a/controller/handler_ctrl/connect.go b/controller/handler_ctrl/connect.go index 21e6aa32c..2f04850f8 100644 --- a/controller/handler_ctrl/connect.go +++ b/controller/handler_ctrl/connect.go @@ -19,20 +19,23 @@ package handler_ctrl import ( "crypto/sha1" "crypto/x509" - "errors" "fmt" "time" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" - "github.com/openziti/foundation/v2/stringz" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/cert" "github.com/openziti/ziti/v2/controller/network" ) type ConnectHandler struct { identity identity.Identity network *network.Network + + // separatelyValidatedTypes holds the control-channel type headers that are dispatched to a + // separate, self-validating acceptor (currently the raft mesh, when clustering is enabled). + separatelyValidatedTypes map[string]struct{} } func NewConnectHandler(identity identity.Identity, network *network.Network) *ConnectHandler { @@ -42,8 +45,44 @@ func NewConnectHandler(identity identity.Identity, network *network.Network) *Co } } +// SetSeparatelyValidatedChannelTypes records the control-channel type headers that are dispatched to a +// separate, self-validating acceptor (e.g. the raft mesh). Connections carrying one of these types are +// skipped by HandleConnection; everything else - router control channel types, unrecognized types, and +// legacy (no type header) connections, all of which the dispatcher routes to the router control +// acceptor - is validated here. This must be populated before the listener begins accepting. +func (self *ConnectHandler) SetSeparatelyValidatedChannelTypes(types map[string]struct{}) { + self.separatelyValidatedTypes = types +} + +// isSeparatelyValidated reports whether the connection's channel type is handled by a separate, +// self-validating acceptor and therefore must not be validated as a router control connection here. +func (self *ConnectHandler) isSeparatelyValidated(hello *channel.Hello) bool { + underlayType, found := hello.Headers[channel.TypeHeader] + if !found { + return false + } + _, ok := self.separatelyValidatedTypes[string(underlayType)] + return ok +} + +// isFirstCtrlConnection reports whether this hello establishes a new channel rather than adding an +// underlay to an existing grouped channel. A legacy (non-grouped) dial is always a new channel; for a +// grouped dial only the connection carrying IsFirstGroupConnection is. +func isFirstCtrlConnection(hello *channel.Hello) bool { + headers := channel.Headers(hello.Headers) + if grouped, _ := headers.GetBoolHeader(channel.IsGroupedHeader); !grouped { + return true + } + first, _ := headers.GetBoolHeader(channel.IsFirstGroupConnection) + return first +} + func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates []*x509.Certificate) error { - if _, found := hello.Headers[channel.TypeHeader]; found { + // Connections whose channel type is handled by a separate, self-validating acceptor (e.g. the raft + // mesh) are validated there, so skip them. Everything else - router control channel types, + // unrecognized types, and legacy (no type header) connections - is dispatched to the router control + // acceptor and must be validated here. + if self.isSeparatelyValidated(hello) { return nil } @@ -51,63 +90,43 @@ func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates log := pfxlog.Logger().WithField("routerId", id) - // verify cert chain if len(certificates) == 0 { return fmt.Errorf("no certificates provided, unable to verify dialer, routerId: %v", id) } - config := self.identity.ServerTLSConfig() - - opts := x509.VerifyOptions{ - Roots: config.RootCAs, - Intermediates: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + // Verify the peer's leaf certificate (certificates[0], the certificate whose private key the TLS + // handshake proved) chains to the controller CA, and bind the router fingerprint check to that + // verified leaf. Matching the enrolled fingerprint against any presented certificate would let a + // peer present its own leaf followed by a target router's public certificate and pass without that + // router's private key. + leaf, err := cert.VerifyClientCertChain(self.identity.CaPool(), certificates) + if err != nil { + return fmt.Errorf("unable to verify dialer, routerId: %v: %w", id, err) } + fingerprint := fmt.Sprintf("%x", sha1.Sum(leaf.Raw)) + log.Debugf("peer leaf certificate fingerprint [%s], common name [%s]", fingerprint, leaf.Subject.CommonName) - var validFingerPrints []string - var errorList []error - - for _, cert := range certificates { - if cert.IsCA { - opts.Intermediates.AddCert(cert) - } - } - - for i, cert := range certificates { - if !cert.IsCA { - if _, err := cert.Verify(opts); err == nil { - fingerprint := fmt.Sprintf("%x", sha1.Sum(cert.Raw)) - validFingerPrints = append(validFingerPrints, fingerprint) - log.Debugf("%d): peer certificate fingerprint [%s]", i, fingerprint) - log.Debugf("%d): peer common name [%s]", i, cert.Subject.CommonName) - } else { - errorList = append(errorList, err) + // The churn / already-connected guard applies only when establishing a new channel. Additional + // underlays of an existing grouped control channel legitimately arrive while the router is already + // connected and must not be rejected here. + if isFirstCtrlConnection(hello) { + if router := self.network.GetConnectedRouter(id); router != nil { + if time.Since(router.ConnectTime) < self.network.GetOptions().RouterConnectChurnLimit { + log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met") + return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name) } + log.WithField("routerName", router.Name).Warn("router already connected, but churn threshold met. replacing connection") } } - if len(validFingerPrints) == 0 && len(errorList) > 0 { - return errors.Join(errorList...) - } - - log.Debugf("peer has [%d] valid certificates out of [%v] submitted", len(validFingerPrints), len(certificates)) - - if router := self.network.GetConnectedRouter(id); router != nil { - if time.Since(router.ConnectTime) < self.network.GetOptions().RouterConnectChurnLimit { - log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met") - return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name) - } - log.WithField("routerName", router.Name).Warn("router already connected, but churn threshold met. replacing connection") - } - if r, err := self.network.GetRouter(id); err == nil { if r.Fingerprint == nil { log.Error("router enrollment incomplete") return fmt.Errorf("router enrollment incomplete, routerId: %v", id) } - if !stringz.Contains(validFingerPrints, *r.Fingerprint) { - log.WithField("fp", *r.Fingerprint).WithField("givenFps", validFingerPrints).Error("router fingerprint mismatch") - return fmt.Errorf("incorrect fingerprint/unenrolled router, routerId: %v, given fingerprints: %v", id, validFingerPrints) + if fingerprint != *r.Fingerprint { + log.WithField("fp", *r.Fingerprint).WithField("givenFp", fingerprint).Error("router fingerprint mismatch") + return fmt.Errorf("incorrect fingerprint/unenrolled router, routerId: %v, given fingerprint: %v", id, fingerprint) } if r.Disabled { log.Error("router disabled") diff --git a/controller/handler_ctrl/connect_test.go b/controller/handler_ctrl/connect_test.go new file mode 100644 index 000000000..7cd14c243 --- /dev/null +++ b/controller/handler_ctrl/connect_test.go @@ -0,0 +1,75 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_ctrl + +import ( + "testing" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/stretchr/testify/require" +) + +// meshChannelType mirrors controller/raft/mesh.ChannelTypeMesh without importing that package. +const meshChannelType = "ctrl.mesh" + +func helloWithType(chType string) *channel.Hello { + h := channel.Headers{} + if chType != "" { + h.PutStringHeader(channel.TypeHeader, chType) + } + return &channel.Hello{Headers: h} +} + +// Test_ConnectHandler_isSeparatelyValidated covers the rule that only channel types dispatched to a +// dedicated, self-validating acceptor are skipped; every other type - including unrecognized types and +// the mesh type on a non-clustered controller - routes to the router control acceptor and must be +// validated here. +func Test_ConnectHandler_isSeparatelyValidated(t *testing.T) { + // Clustered controller: only the mesh type has a dedicated acceptor. + clustered := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}}} + require.True(t, clustered.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh is validated by its own acceptor") + require.False(t, clustered.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeDefault)), "router ctrl type is validated here") + require.False(t, clustered.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeHighPriority))) + require.False(t, clustered.isSeparatelyValidated(helloWithType("bogus")), "unrecognized types route to the router acceptor and must be validated") + require.False(t, clustered.isSeparatelyValidated(helloWithType("")), "legacy (no type) connections are validated here") + + // Non-clustered controller: no dedicated acceptors, so even a mesh-typed connection routes to the + // router acceptor and must be validated. + standalone := &ConnectHandler{separatelyValidatedTypes: map[string]struct{}{}} + require.False(t, standalone.isSeparatelyValidated(helloWithType(meshChannelType)), "mesh on a non-clustered controller must be validated") + require.False(t, standalone.isSeparatelyValidated(helloWithType(ctrlchan.ChannelTypeDefault))) +} + +func Test_isFirstCtrlConnection(t *testing.T) { + // Legacy / non-grouped dial: no grouped header -> treated as a new channel. + require.True(t, isFirstCtrlConnection(&channel.Hello{Headers: channel.Headers{}})) + + grouped := channel.Headers{} + grouped.PutBoolHeader(channel.IsGroupedHeader, true) + grouped.PutBoolHeader(channel.IsFirstGroupConnection, true) + require.True(t, isFirstCtrlConnection(&channel.Hello{Headers: grouped}), "grouped first connection") + + additional := channel.Headers{} + additional.PutBoolHeader(channel.IsGroupedHeader, true) + require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: additional}), "additional underlay (no first flag)") + + notFirst := channel.Headers{} + notFirst.PutBoolHeader(channel.IsGroupedHeader, true) + notFirst.PutBoolHeader(channel.IsFirstGroupConnection, false) + require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: notFirst}), "additional underlay (first=false)") +} diff --git a/router/env/ctrls.go b/router/env/ctrls.go index 4e73425a2..354e867cd 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -311,10 +311,12 @@ func (self *networkControllers) connectToController(endpoint string, addr transp logrus.Debugf("Using local interface %s to dial controller", config.Ctrl.LocalBinding) } - // Build headers for the initial dial, including grouped channel flags + // Base headers apply to every underlay of the grouped channel. IsFirstGroupConnection must NOT be + // set here: the dialer reuses these headers when creating additional (e.g. ctrl.high) underlays, and + // an inherited first-connection flag would make each additional underlay look like a new channel and + // trip the controller's already-connected guard. headers.PutBoolHeader(channel.IsGroupedHeader, true) headers.PutStringHeader(channel.TypeHeader, ctrlchan.ChannelTypeDefault) - headers.PutBoolHeader(channel.IsFirstGroupConnection, true) dialer := channel.NewClassicDialer(channel.DialerConfig{ Identity: config.Id, @@ -327,8 +329,15 @@ func (self *networkControllers) connectToController(endpoint string, addr transp }, }) + // The initial underlay, and only it, carries IsFirstGroupConnection. + firstDialHeaders := channel.Headers{} + for k, v := range headers { + firstDialHeaders[k] = v + } + firstDialHeaders.PutBoolHeader(channel.IsFirstGroupConnection, true) + // Dial initial underlay - underlay, err := dialer.CreateWithHeaders(config.Ctrl.Options.ConnectTimeout, headers) + underlay, err := dialer.CreateWithHeaders(config.Ctrl.Options.ConnectTimeout, firstDialHeaders) if err != nil { return fmt.Errorf("error connecting ctrl (%v)", err) } From d9ddd984238cf3635d5e732f51448935edc2031d Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 14:53:03 -0400 Subject: [PATCH 14/73] Add negative-path tests for control channel connection validation - adds tests asserting router control channel connections with an untrusted or self-signed leaf (including one backed by a scraped CA-chained filler cert) are rejected, and that separately-validated channel types are skipped - extracts a small header helper so the grouped-connection first-underlay scoping is unit-testable, and tests that the flag is not inherited by additional underlays --- controller/handler_ctrl/connect_test.go | 101 ++++++++++++++++++++++++ router/env/ctrls.go | 18 +++-- router/env/ctrls_test.go | 53 +++++++++++++ 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 router/env/ctrls_test.go diff --git a/controller/handler_ctrl/connect_test.go b/controller/handler_ctrl/connect_test.go index 7cd14c243..2586076a9 100644 --- a/controller/handler_ctrl/connect_test.go +++ b/controller/handler_ctrl/connect_test.go @@ -17,9 +17,17 @@ package handler_ctrl import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" "testing" + "time" "github.com/openziti/channel/v4" + "github.com/openziti/identity" "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/stretchr/testify/require" ) @@ -73,3 +81,96 @@ func Test_isFirstCtrlConnection(t *testing.T) { notFirst.PutBoolHeader(channel.IsFirstGroupConnection, false) require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: notFirst}), "additional underlay (first=false)") } + +// caPoolIdentity is a minimal identity.Identity whose only useful method is CaPool. The certificate +// verification path of HandleConnection consults nothing else, so the remaining interface methods are +// left to the embedded nil interface (never called on the paths exercised here). +type caPoolIdentity struct { + identity.Identity + pool *identity.CaPool +} + +func (f *caPoolIdentity) CaPool() *identity.CaPool { return f.pool } + +type ctCertAndKey struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +var ctSerial int64 + +func ctMkCert(t *testing.T, cn string, isCA bool, signer *ctCertAndKey) *ctCertAndKey { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + ctSerial++ + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(ctSerial), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + } + if isCA { + tmpl.IsCA = true + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + } else { + tmpl.KeyUsage = x509.KeyUsageDigitalSignature + tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + } + signParent, signKey := tmpl, key + if signer != nil { + signParent, signKey = signer.cert, signer.key + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, signParent, &key.PublicKey, signKey) + require.NoError(t, err) + c, err := x509.ParseCertificate(der) + require.NoError(t, err) + return &ctCertAndKey{cert: c, key: key} +} + +// Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf covers the router control-channel +// verification: a connection dispatched to the router acceptor must present a leaf that chains to the +// controller CA. In particular, presenting a self-signed leaf followed by a legitimate router's public +// certificate as filler must be rejected - the check is bound to the leaf, not to "some presented cert +// chains". These paths fail during certificate verification, before any network state is consulted. +func Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf(t *testing.T) { + req := require.New(t) + + root := ctMkCert(t, "root", true, nil) + inter := ctMkCert(t, "int", true, root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + handler := &ConnectHandler{ + identity: &caPoolIdentity{pool: pool}, + separatelyValidatedTypes: map[string]struct{}{}, + } + + // A legitimate, CA-chained certificate an attacker could scrape off the wire and present as filler. + legit := ctMkCert(t, "legit-router", false, inter) + // The attacker's own self-signed leaf - it does not chain to the CA. + forged := ctMkCert(t, "forged", false, nil) + + hello := &channel.Hello{IdToken: "router1", Headers: channel.Headers{}} + + req.Error(handler.HandleConnection(hello, nil), "no certificates must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert}), + "self-signed leaf that does not chain to the CA must be rejected") + req.Error(handler.HandleConnection(hello, []*x509.Certificate{forged.cert, legit.cert}), + "self-signed leaf backed by a scraped CA-chained filler cert must be rejected") +} + +// Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated verifies that connections whose type is +// handled by a separate acceptor (e.g. the raft mesh on a clustered controller) are not validated here, +// even when the presented certificate would fail the router control-channel check. +func Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated(t *testing.T) { + req := require.New(t) + + forged := ctMkCert(t, "forged", false, nil) + handler := &ConnectHandler{ + separatelyValidatedTypes: map[string]struct{}{meshChannelType: {}}, + } + + req.NoError(handler.HandleConnection(helloWithType(meshChannelType), []*x509.Certificate{forged.cert}), + "mesh-typed connection is validated by its own acceptor and must be skipped here") +} diff --git a/router/env/ctrls.go b/router/env/ctrls.go index 354e867cd..9f0369e13 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -299,6 +299,18 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C }() } +// firstUnderlayHeaders returns a copy of the grouped-channel base headers marked with +// IsFirstGroupConnection. Only the initial underlay carries this flag; additional underlays reuse the +// unmarked base headers so the controller does not treat each of them as a new channel. +func firstUnderlayHeaders(base channel.Headers) channel.Headers { + first := channel.Headers{} + for k, v := range base { + first[k] = v + } + first.PutBoolHeader(channel.IsFirstGroupConnection, true) + return first +} + func (self *networkControllers) connectToController(endpoint string, addr transport.Address) error { headers, err := self.dialEnv.GetChannelHeaders() if err != nil { @@ -330,11 +342,7 @@ func (self *networkControllers) connectToController(endpoint string, addr transp }) // The initial underlay, and only it, carries IsFirstGroupConnection. - firstDialHeaders := channel.Headers{} - for k, v := range headers { - firstDialHeaders[k] = v - } - firstDialHeaders.PutBoolHeader(channel.IsFirstGroupConnection, true) + firstDialHeaders := firstUnderlayHeaders(headers) // Dial initial underlay underlay, err := dialer.CreateWithHeaders(config.Ctrl.Options.ConnectTimeout, firstDialHeaders) diff --git a/router/env/ctrls_test.go b/router/env/ctrls_test.go new file mode 100644 index 000000000..4838c0a96 --- /dev/null +++ b/router/env/ctrls_test.go @@ -0,0 +1,53 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package env + +import ( + "testing" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/stretchr/testify/require" +) + +// Test_firstUnderlayHeaders verifies that only the initial underlay is flagged as the first grouped +// connection. If the flag leaked onto the base headers, every additional underlay (e.g. ctrl.high) +// would look like a new channel and trip the controller's already-connected churn guard. +func Test_firstUnderlayHeaders(t *testing.T) { + req := require.New(t) + + base := channel.Headers{} + base.PutBoolHeader(channel.IsGroupedHeader, true) + base.PutStringHeader(channel.TypeHeader, ctrlchan.ChannelTypeDefault) + + first := firstUnderlayHeaders(base) + + // The base headers, reused for additional underlays, must not carry the first-connection flag. + _, baseHasFirst := base[channel.IsFirstGroupConnection] + req.False(baseHasFirst, "base headers for additional underlays must not be flagged as first") + + // The initial underlay's headers must carry it. + firstFlag, ok := first.GetBoolHeader(channel.IsFirstGroupConnection) + req.True(ok, "initial underlay headers must contain the first-connection flag") + req.True(firstFlag) + + // The remaining base headers are preserved on the copy. + grouped, _ := first.GetBoolHeader(channel.IsGroupedHeader) + req.True(grouped, "grouped flag preserved on the initial underlay copy") + chType, _ := first.GetStringHeader(channel.TypeHeader) + req.Equal(ctrlchan.ChannelTypeDefault, chType, "channel type preserved on the initial underlay copy") +} From 7e0cfdf190efc0546ea82ae75c751da811e052b0 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 20 Jul 2026 22:46:21 -0400 Subject: [PATCH 15/73] Add server-auth certificate chain verification helper --- common/cert/verify.go | 36 ++++++++++++++++++++++++++---------- common/cert/verify_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/common/cert/verify.go b/common/cert/verify.go index 8021f1813..360f65e22 100644 --- a/common/cert/verify.go +++ b/common/cert/verify.go @@ -25,17 +25,33 @@ import ( ) // VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool, and returns that verified leaf. Only certs[0] is verified: it is the -// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a -// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate -// intermediates (added alongside the pool's intermediates), never as independent grounds for -// acceptance. +// root in the given CA pool with client-authentication extended key usage, and returns that verified +// leaf. Use this for inbound connections, where the peer's leaf is its TLS client certificate. See +// verifyCertChain for how the leaf and any certs[1:] are treated. // -// Client-authentication extended key usage is required. A certificate with no ExtKeyUsage extension -// is unrestricted and passes (the form ziti pki produces); a leaf whose EKU excludes client -// authentication is rejected. Callers that must accept server-auth-only client identities should -// verify with an explicit {ClientAuth, ServerAuth} usage set instead. +// A certificate with no ExtKeyUsage extension is unrestricted and passes (the form ziti pki produces); +// a leaf whose EKU excludes client authentication is rejected. func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + return verifyCertChain(pool, certs, x509.ExtKeyUsageClientAuth) +} + +// VerifyServerCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// root in the given CA pool with server-authentication extended key usage, and returns that verified +// leaf. Use this for outbound connections, where the peer's leaf is its TLS server certificate: an +// external PKI may issue distinct client-auth and server-auth certificates, and the peer of an +// outbound dial presents the latter. See verifyCertChain for how the leaf and any certs[1:] are +// treated. +func VerifyServerCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { + return verifyCertChain(pool, certs, x509.ExtKeyUsageServerAuth) +} + +// verifyCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted root in +// the given CA pool with the requested extended key usage, and returns that verified leaf. Only certs[0] +// is verified: it is the certificate whose private key the TLS handshake proved the peer holds, and it +// is the certificate a caller derives peer identity from. Any remaining certs[1:] are treated only as +// candidate intermediates (added alongside the pool's intermediates), never as independent grounds for +// acceptance. +func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage x509.ExtKeyUsage) (*x509.Certificate, error) { if pool == nil { return nil, errors.New("no ca pool provided") } @@ -52,7 +68,7 @@ func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x if _, err := certs[0].Verify(x509.VerifyOptions{ Roots: pool.RootsAsStdPool(), Intermediates: intermediates, - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsages: []x509.ExtKeyUsage{keyUsage}, }); err != nil { return nil, fmt.Errorf("leaf certificate not trusted: %w", err) } diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go index 27bfcdfbd..65b95abf4 100644 --- a/common/cert/verify_test.go +++ b/common/cert/verify_test.go @@ -170,3 +170,38 @@ func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { _, err = VerifyClientCertChain(pool, nil) req.Error(err, "no certs rejected") } + +// TestVerifyCertChain_DirectionAwareEKU verifies that the client-auth and server-auth variants each +// enforce their own extended key usage. An external PKI may issue separate client-auth and server-auth +// certificates; verifying with the wrong direction (e.g. requiring client auth of a peer's server +// certificate on an outbound connection) would reject a legitimate peer. A leaf carrying both usages, +// or none at all, satisfies either variant. +func TestVerifyCertChain_DirectionAwareEKU(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + + clientOnly := vMkLeaf(t, "client-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) + serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + both := vMkLeaf(t, "both", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter) + none := vMkLeaf(t, "none", "/identity/real", nil, inter) + + _, err := VerifyClientCertChain(pool, []*x509.Certificate{clientOnly.cert}) + req.NoError(err, "client-auth leaf accepted for inbound direction") + _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.Error(err, "server-auth-only leaf rejected for inbound direction") + + _, err = VerifyServerCertChain(pool, []*x509.Certificate{serverOnly.cert}) + req.NoError(err, "server-auth leaf accepted for outbound direction") + _, err = VerifyServerCertChain(pool, []*x509.Certificate{clientOnly.cert}) + req.Error(err, "client-auth-only leaf rejected for outbound direction") + + // A leaf carrying both usages, or none, satisfies either direction (the common ziti pki case). + for _, c := range []*x509.Certificate{both.cert, none.cert} { + _, err = VerifyClientCertChain(pool, []*x509.Certificate{c}) + req.NoError(err) + _, err = VerifyServerCertChain(pool, []*x509.Certificate{c}) + req.NoError(err) + } +} From 3e6f0f2a76a7865eeefb6a6a7b577d98a968d1ea Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 21 Jul 2026 16:55:11 -0400 Subject: [PATCH 16/73] Verify router leaf against full CA bundle without EKU restriction - verifies the control-channel peer leaf against the controller's full trusted-CA pool (identity.CA()) instead of only self-signed roots, honoring intermediate trust anchors and multi-root bundles - drops the client-auth extended-key-usage requirement so an externally managed PKI with arbitrary or absent EKUs is not rejected --- common/cert/verify.go | 49 +++----- common/cert/verify_test.go | 153 ++++++++++++------------ controller/handler_ctrl/connect.go | 2 +- controller/handler_ctrl/connect_test.go | 12 +- 4 files changed, 103 insertions(+), 113 deletions(-) diff --git a/common/cert/verify.go b/common/cert/verify.go index 360f65e22..c569fb4b7 100644 --- a/common/cert/verify.go +++ b/common/cert/verify.go @@ -20,39 +20,22 @@ import ( "crypto/x509" "errors" "fmt" - - "github.com/openziti/identity" ) -// VerifyClientCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool with client-authentication extended key usage, and returns that verified -// leaf. Use this for inbound connections, where the peer's leaf is its TLS client certificate. See -// verifyCertChain for how the leaf and any certs[1:] are treated. +// VerifyLeafCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted +// certificate in the given pool, and returns that verified leaf. Only certs[0] is verified: it is the +// certificate whose private key the TLS handshake proved the peer holds, and it is the certificate a +// caller derives peer identity from. Any remaining certs[1:] are treated only as candidate +// intermediates supplied by the peer, never as independent grounds for acceptance - so a peer cannot be +// admitted by presenting its own leaf alongside some other certificate that happens to chain. // -// A certificate with no ExtKeyUsage extension is unrestricted and passes (the form ziti pki produces); -// a leaf whose EKU excludes client authentication is rejected. -func VerifyClientCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { - return verifyCertChain(pool, certs, x509.ExtKeyUsageClientAuth) -} - -// VerifyServerCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted -// root in the given CA pool with server-authentication extended key usage, and returns that verified -// leaf. Use this for outbound connections, where the peer's leaf is its TLS server certificate: an -// external PKI may issue distinct client-auth and server-auth certificates, and the peer of an -// outbound dial presents the latter. See verifyCertChain for how the leaf and any certs[1:] are -// treated. -func VerifyServerCertChain(pool *identity.CaPool, certs []*x509.Certificate) (*x509.Certificate, error) { - return verifyCertChain(pool, certs, x509.ExtKeyUsageServerAuth) -} - -// verifyCertChain verifies that the presented leaf certificate (certs[0]) chains to a trusted root in -// the given CA pool with the requested extended key usage, and returns that verified leaf. Only certs[0] -// is verified: it is the certificate whose private key the TLS handshake proved the peer holds, and it -// is the certificate a caller derives peer identity from. Any remaining certs[1:] are treated only as -// candidate intermediates (added alongside the pool's intermediates), never as independent grounds for -// acceptance. -func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage x509.ExtKeyUsage) (*x509.Certificate, error) { - if pool == nil { +// roots is the verifying node's full trusted-CA pool (identity.CA()). Every certificate in it is a valid +// chain terminus, whether a self-signed root or an intermediate distributed as a trust anchor, matching +// how the node's TLS configuration establishes trust. No extended-key-usage restriction is applied: +// certificates issued by an external PKI with arbitrary or absent EKUs are accepted as long as the leaf +// chains to a trusted CA. +func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate) (*x509.Certificate, error) { + if roots == nil { return nil, errors.New("no ca pool provided") } @@ -60,15 +43,15 @@ func verifyCertChain(pool *identity.CaPool, certs []*x509.Certificate, keyUsage return nil, errors.New("no certificates presented") } - intermediates := pool.IntermediatesAsStdPool() + intermediates := x509.NewCertPool() for _, intermediate := range certs[1:] { intermediates.AddCert(intermediate) } if _, err := certs[0].Verify(x509.VerifyOptions{ - Roots: pool.RootsAsStdPool(), + Roots: roots, Intermediates: intermediates, - KeyUsages: []x509.ExtKeyUsage{keyUsage}, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, }); err != nil { return nil, fmt.Errorf("leaf certificate not trusted: %w", err) } diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go index 65b95abf4..7b64688b3 100644 --- a/common/cert/verify_test.go +++ b/common/cert/verify_test.go @@ -27,7 +27,6 @@ import ( "testing" "time" - "github.com/openziti/identity" "github.com/stretchr/testify/require" ) @@ -99,109 +98,115 @@ func vMkLeaf(t *testing.T, cn, spiffePath string, ekus []x509.ExtKeyUsage, signe return &vCertAndKey{cert: c, key: key} } -// TestVerifyClientCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to -// the CA is rejected even when another presented certificate does chain - i.e. verification is bound -// to the leaf, not to "some presented certificate verifies". -func TestVerifyClientCertChain_RejectsUnchainedLeaf(t *testing.T) { - req := require.New(t) - root := vMkCA(t, "root", nil) - inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) - - // A trust-domain-signed cert (chains to the CA) presented as an extra certificate. - extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - // The leaf itself is self-signed and does NOT chain to the CA. - unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) - - _, err := VerifyClientCertChain(pool, []*x509.Certificate{unchained.cert, extra.cert}) - req.Error(err, "leaf not chaining to the CA must be rejected even alongside a chaining extra cert") +func vPoolOf(certs ...*x509.Certificate) *x509.CertPool { + pool := x509.NewCertPool() + for _, c := range certs { + pool.AddCert(c) + } + return pool } -func TestVerifyClientCertChain_AcceptsLegitLeaf(t *testing.T) { +// TestVerifyLeafCertChain_RejectsUnchainedLeaf verifies that a leaf which does not itself chain to a +// trusted CA is rejected even when another presented certificate does chain - i.e. verification is bound +// to the leaf, not to "some presented certificate verifies". +func TestVerifyLeafCertChain_RejectsUnchainedLeaf(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + roots := vPoolOf(root.cert, inter.cert) + + // A trust-anchored cert (chains to the pool) presented as an extra certificate. + extra := vMkLeaf(t, "extra", "", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) + // The leaf itself is self-signed and does NOT chain to the pool. + unchained := vMkLeaf(t, "unchained", "/identity/other", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, nil) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{unchained.cert, extra.cert}) + req.Error(err, "leaf not chaining to the pool must be rejected even alongside a chaining extra cert") +} + +func TestVerifyLeafCertChain_AcceptsLegitLeaf(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + inter := vMkCA(t, "int", root) + roots := vPoolOf(root.cert, inter.cert) legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - leaf, err := VerifyClientCertChain(pool, []*x509.Certificate{legit.cert}) + leaf, err := VerifyLeafCertChain(roots, []*x509.Certificate{legit.cert}) req.NoError(err) req.Equal(legit.cert, leaf, "returns the verified leaf (certs[0]) for identity use") } -func TestVerifyClientCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { +func TestVerifyLeafCertChain_AcceptsPeerSuppliedIntermediate(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) legit := vMkLeaf(t, "legit", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) // Pool holds ONLY the root; the intermediate must be supplied on the wire (certs[1:]). - rootOnly := identity.NewCaPool([]*x509.Certificate{root.cert}) - _, err := VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert}) + rootOnly := vPoolOf(root.cert) + _, err := VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert}) req.Error(err, "without the intermediate anywhere the leaf cannot be verified") - _, err = VerifyClientCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) + _, err = VerifyLeafCertChain(rootOnly, []*x509.Certificate{legit.cert, inter.cert}) req.NoError(err, "peer-supplied intermediate lets a valid peer verify") } -func TestVerifyClientCertChain_EKUCompat(t *testing.T) { +// TestVerifyLeafCertChain_MultipleRoots covers a trust bundle with more than one self-signed root (as a +// quickstart deployment produces, concatenating a controller root and a signer root). A leaf chaining to +// either root must verify. +func TestVerifyLeafCertChain_MultipleRoots(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signerRoot := vMkCA(t, "signer-root", nil) + signerInter := vMkCA(t, "signer-int", signerRoot) + roots := vPoolOf(ctrlRoot.cert, signerRoot.cert) + + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signerInter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signerInter.cert}) + req.NoError(err, "a leaf chaining to one of several trusted roots must verify") +} + +// TestVerifyLeafCertChain_IntermediateAsTrustAnchor covers a self-managed PKI that distributes a +// (non-self-signed) intermediate as the trust anchor without its root. Every certificate in the pool is +// a valid chain terminus, so a leaf chaining directly to that intermediate must verify. +func TestVerifyLeafCertChain_IntermediateAsTrustAnchor(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + leaf := vMkLeaf(t, "leaf", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - // No ExtKeyUsage extension (the form ziti pki produces) is unrestricted -> accepted. - noEKU := vMkLeaf(t, "ziti-pki", "/identity/real", nil, inter) - _, err := VerifyClientCertChain(pool, []*x509.Certificate{noEKU.cert}) - req.NoError(err, "no-EKU leaf accepted (ziti pki backward compat)") - - // EKU present but excludes client auth (server-auth only) -> rejected. - serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.Error(err, "server-auth-only leaf rejected by the client-auth requirement") + interAnchored := vPoolOf(inter.cert) // only the intermediate distributed, no self-signed root + _, err := VerifyLeafCertChain(interAnchored, []*x509.Certificate{leaf.cert}) + req.NoError(err, "an intermediate distributed as a trust anchor must be accepted as a chain terminus") } -func TestVerifyClientCertChain_EmptyInputs(t *testing.T) { - req := require.New(t) - root := vMkCA(t, "root", nil) - pool := identity.NewCaPool([]*x509.Certificate{root.cert}) - - _, err := VerifyClientCertChain(nil, []*x509.Certificate{root.cert}) - req.Error(err, "nil pool rejected") - _, err = VerifyClientCertChain(pool, nil) - req.Error(err, "no certs rejected") -} - -// TestVerifyCertChain_DirectionAwareEKU verifies that the client-auth and server-auth variants each -// enforce their own extended key usage. An external PKI may issue separate client-auth and server-auth -// certificates; verifying with the wrong direction (e.g. requiring client auth of a peer's server -// certificate on an outbound connection) would reject a legitimate peer. A leaf carrying both usages, -// or none at all, satisfies either variant. -func TestVerifyCertChain_DirectionAwareEKU(t *testing.T) { +// TestVerifyLeafCertChain_ArbitraryEKU covers external PKIs whose certificates carry arbitrary or absent +// extended key usages. No EKU restriction is applied, so all of them verify as long as the chain is +// valid. +func TestVerifyLeafCertChain_ArbitraryEKU(t *testing.T) { req := require.New(t) root := vMkCA(t, "root", nil) inter := vMkCA(t, "int", root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + roots := vPoolOf(root.cert, inter.cert) - clientOnly := vMkLeaf(t, "client-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, inter) - serverOnly := vMkLeaf(t, "server-only", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, inter) - both := vMkLeaf(t, "both", "/identity/real", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, inter) - none := vMkLeaf(t, "none", "/identity/real", nil, inter) - - _, err := VerifyClientCertChain(pool, []*x509.Certificate{clientOnly.cert}) - req.NoError(err, "client-auth leaf accepted for inbound direction") - _, err = VerifyClientCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.Error(err, "server-auth-only leaf rejected for inbound direction") - - _, err = VerifyServerCertChain(pool, []*x509.Certificate{serverOnly.cert}) - req.NoError(err, "server-auth leaf accepted for outbound direction") - _, err = VerifyServerCertChain(pool, []*x509.Certificate{clientOnly.cert}) - req.Error(err, "client-auth-only leaf rejected for outbound direction") - - // A leaf carrying both usages, or none, satisfies either direction (the common ziti pki case). - for _, c := range []*x509.Certificate{both.cert, none.cert} { - _, err = VerifyClientCertChain(pool, []*x509.Certificate{c}) - req.NoError(err) - _, err = VerifyServerCertChain(pool, []*x509.Certificate{c}) - req.NoError(err) + for _, ekus := range [][]x509.ExtKeyUsage{ + nil, + {x509.ExtKeyUsageClientAuth}, + {x509.ExtKeyUsageServerAuth}, + {x509.ExtKeyUsageEmailProtection}, + } { + leaf := vMkLeaf(t, "leaf", "/identity/real", ekus, inter) + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}) + req.NoError(err, "leaf with EKU %v must be accepted", ekus) } } + +func TestVerifyLeafCertChain_EmptyInputs(t *testing.T) { + req := require.New(t) + root := vMkCA(t, "root", nil) + roots := vPoolOf(root.cert) + + _, err := VerifyLeafCertChain(nil, []*x509.Certificate{root.cert}) + req.Error(err, "nil pool rejected") + _, err = VerifyLeafCertChain(roots, nil) + req.Error(err, "no certs rejected") +} diff --git a/controller/handler_ctrl/connect.go b/controller/handler_ctrl/connect.go index 2f04850f8..d53a1ea68 100644 --- a/controller/handler_ctrl/connect.go +++ b/controller/handler_ctrl/connect.go @@ -99,7 +99,7 @@ func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates // verified leaf. Matching the enrolled fingerprint against any presented certificate would let a // peer present its own leaf followed by a target router's public certificate and pass without that // router's private key. - leaf, err := cert.VerifyClientCertChain(self.identity.CaPool(), certificates) + leaf, err := cert.VerifyLeafCertChain(self.identity.CA(), certificates) if err != nil { return fmt.Errorf("unable to verify dialer, routerId: %v: %w", id, err) } diff --git a/controller/handler_ctrl/connect_test.go b/controller/handler_ctrl/connect_test.go index 2586076a9..749a3d074 100644 --- a/controller/handler_ctrl/connect_test.go +++ b/controller/handler_ctrl/connect_test.go @@ -82,15 +82,15 @@ func Test_isFirstCtrlConnection(t *testing.T) { require.False(t, isFirstCtrlConnection(&channel.Hello{Headers: notFirst}), "additional underlay (first=false)") } -// caPoolIdentity is a minimal identity.Identity whose only useful method is CaPool. The certificate +// caPoolIdentity is a minimal identity.Identity whose only useful method is CA. The certificate // verification path of HandleConnection consults nothing else, so the remaining interface methods are // left to the embedded nil interface (never called on the paths exercised here). type caPoolIdentity struct { identity.Identity - pool *identity.CaPool + roots *x509.CertPool } -func (f *caPoolIdentity) CaPool() *identity.CaPool { return f.pool } +func (f *caPoolIdentity) CA() *x509.CertPool { return f.roots } type ctCertAndKey struct { cert *x509.Certificate @@ -139,10 +139,12 @@ func Test_ConnectHandler_HandleConnection_RejectsUntrustedLeaf(t *testing.T) { root := ctMkCert(t, "root", true, nil) inter := ctMkCert(t, "int", true, root) - pool := identity.NewCaPool([]*x509.Certificate{root.cert, inter.cert}) + roots := x509.NewCertPool() + roots.AddCert(root.cert) + roots.AddCert(inter.cert) handler := &ConnectHandler{ - identity: &caPoolIdentity{pool: pool}, + identity: &caPoolIdentity{roots: roots}, separatelyValidatedTypes: map[string]struct{}{}, } From d6104e00f42309ae066f745a19eae807b1bcc401 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 27 Jul 2026 14:06:44 -0400 Subject: [PATCH 17/73] Add security advisories to 2.0.2 release notes - adds the two control-plane security advisories (GHSA-mrpr-756c-xm47 critical, GHSA-cc5m-7mhm-xh9f medium) and the bbolt memory-corruption fix (#4108) to the existing 2.0.2 release notes --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 485a72397..be2b9a15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,31 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes +## Security Advisories + +This release addresses two control-plane certificate and identity validation vulnerabilities. See the linked +GitHub Security Advisories for full details, impact, and affected versions. + +* [GHSA-mrpr-756c-xm47](https://github.com/openziti/ziti/security/advisories/GHSA-mrpr-756c-xm47) (CVE pending) (Critical) - Improper peer certificate validation on the controller + cluster mesh, router links, and metrics endpoint. TLS peer checks accepted a connection when any presented + certificate chained to the trusted CA while taking the peer identity from the leaf certificate, allowing a + peer to be admitted under a forged identity without possessing a trusted key. On HA/clustered controllers + this allows joining the controller cluster as an arbitrary controller. +* [GHSA-cc5m-7mhm-xh9f](https://github.com/openziti/ziti/security/advisories/GHSA-cc5m-7mhm-xh9f) (CVE pending) (Medium) - Control-channel connections carrying a channel-type header + bypassed router certificate and identity verification, allowing an attacker that can reach the controller + control port to be admitted as an arbitrary router identity and manipulate that router's fabric terminators, + faults, and circuit routing. Impact is limited to router data model metadata (service and identity names) and + control-plane manipulation; it does not by itself grant access to the services the network protects. + ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.0...v2.0.1) * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user + * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory # Release 2.0.1 From 215eec7f09d655ebf66f3ad1e1325adbec17e241 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 27 May 2026 09:45:45 -0400 Subject: [PATCH 18/73] Drop release-build wait gate from promote-downstreams workflow - removes the wait_for_release job that waited on every check on the release commit; unrelated checks sharing the commit (e.g. POST Webhook with Python, add-to-project) could conclude failure and block promotion - makes parse_version the entry job and drops its needs: wait_for_release - relies on artifact existence as the success signal: packages and the :version image only exist if the release build and tests passed, and the promote steps already fail closed on missing artifacts (jf rt copy --fail-no-op=true, docker buildx imagetools create) --- .github/workflows/promote-downstreams.yml | 41 ----------------------- 1 file changed, 41 deletions(-) diff --git a/.github/workflows/promote-downstreams.yml b/.github/workflows/promote-downstreams.yml index 0d8dacc5e..55052da64 100644 --- a/.github/workflows/promote-downstreams.yml +++ b/.github/workflows/promote-downstreams.yml @@ -13,49 +13,8 @@ concurrency: cancel-in-progress: true jobs: - wait_for_release: - name: Wait for Release Builds to Succeed - runs-on: ubuntu-24.04 - steps: - - name: Debug action - uses: hmarr/debug-action@v3 - - - name: Wait for all checks on this rev - uses: lewagon/wait-on-check-action@v1.5.0 - with: - ref: ${{ github.ref_name }} - repo-token: ${{ secrets.GITHUB_TOKEN }} - # seconds between polling the checks api for job statuses - wait-interval: 30 - # confusingly, this means "pause this step until all jobs from all workflows in same run have completed" - running-workflow-name: Wait for Release Builds to Succeed - # comma-separated list of check names (job..name) to ignore - ignore-checks: SDK Terminator Validation,Fablab HA Smoketest,POST Webhook,Release Quickstart Job,Parse Tag Regex - - - name: Git Checkout - if: failure() - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Diagnose Failed "Wait for Release Builds to Succeed" - if: failure() - shell: bash - run: | - set -o pipefail - set -o xtrace - - COMMIT_SHA=$(git rev-parse ${GITHUB_REF_NAME}^{commit}) - for STATUS in cancelled failure - do - gh run list --repo "${GITHUB_REPOSITORY}" --status "${STATUS}" --commit "${COMMIT_SHA}" - done - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # the purpose of this job is to enforce that the Git ref promoted is a semver eligible for stable release, i.e., not having a semver pre-release suffix; the extracted version without the leading 'v' is passed to the docker job as the container image tag parse_version: - needs: wait_for_release name: Parse Tag Regex runs-on: ubuntu-24.04 outputs: From d56f7981a3513a8c343eade72f659e77ff78bb5f Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 27 May 2026 10:05:45 -0400 Subject: [PATCH 19/73] Add optional tag input to promote-downstreams workflow - adds an optional 'tag' workflow_dispatch input so the promote can be dispatched from a branch (e.g. main) while targeting a specific release tag, instead of only running against the ref it fires on - resolves the promoted ref as github.event.inputs.tag || github.ref_name in the validate and compare steps, so the release-event path is unchanged - includes the resolved tag in the concurrency group so promoting different tags from a branch no longer cancel each other --- .github/workflows/promote-downstreams.yml | 28 +++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/promote-downstreams.yml b/.github/workflows/promote-downstreams.yml index 55052da64..fe9df9393 100644 --- a/.github/workflows/promote-downstreams.yml +++ b/.github/workflows/promote-downstreams.yml @@ -1,15 +1,21 @@ name: Promote Downstream Releases -on: - # may be triggered manually on a release tag that represents a prerelease to promote it to a release in the downstream package repositories and Docker Hub +on: + # may be triggered manually to promote a release tag to stable in the downstream package repositories and Docker Hub. + # Dispatch from the tag itself, or from any branch and set the 'tag' input to the release tag to promote. workflow_dispatch: + inputs: + tag: + description: Release tag to promote, e.g. v2.0.0. Defaults to the ref the run was dispatched from; set this to promote a specific tag when dispatching from a branch. + required: false + type: string # GitHub release is marked stable, i.e., isPrerelease: false release: types: [released] # this release event activity type excludes prereleases -# cancel older, redundant runs of same workflow on same branch +# cancel older, redundant runs of same workflow for the same tag (or dispatched ref) concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.head_ref || github.ref_name }} cancel-in-progress: true jobs: @@ -24,11 +30,14 @@ jobs: - name: Validate the Release Tag is a Stable Release Ref id: validate shell: bash + env: + # the 'tag' input when dispatched from a branch, otherwise the ref the run fired on (the tag) + PROMOTE_REF: ${{ github.event.inputs.tag || github.ref_name }} run: | - if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "version=${GITHUB_REF_NAME#v}" | tee -a $GITHUB_OUTPUT + if [[ "${PROMOTE_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "version=${PROMOTE_REF#v}" | tee -a $GITHUB_OUTPUT else - echo "${GITHUB_REF_NAME} is not a semver stable release ref" >&2 + echo "${PROMOTE_REF} is not a semver stable release ref" >&2 exit 1 fi @@ -55,14 +64,15 @@ jobs: | sort -V \ | tail -1 ) - CURRENT_VERSION="${GITHUB_REF_NAME}" - + CURRENT_VERSION="${PROMOTE_REF}" + if [[ "$CURRENT_VERSION" == "$HIGHEST_VERSION" ]]; then echo "highest=true" | tee -a $GITHUB_OUTPUT else echo "highest=false" | tee -a $GITHUB_OUTPUT fi env: + PROMOTE_REF: ${{ github.event.inputs.tag || github.ref_name }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} promote_docker: From c04ef14698ce7cc7ae2e6a1226c229c9b9c9deb4 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:25:26 -0400 Subject: [PATCH 20/73] backport to 2.0.x - notification success should not block a release --- .github/workflows/mattermost-channel-posts.yml | 8 ++++++++ .github/workflows/mattermost-webhook.yml | 3 +++ .github/workflows/promote-downstreams.yml | 6 ++++-- .github/workflows/publish-linux-packages.yml | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mattermost-channel-posts.yml b/.github/workflows/mattermost-channel-posts.yml index e707585f4..4a88c8d01 100644 --- a/.github/workflows/mattermost-channel-posts.yml +++ b/.github/workflows/mattermost-channel-posts.yml @@ -21,9 +21,16 @@ jobs: send-notifications: runs-on: ubuntu-24.04 name: POST Webhook with Python + # a chat notification must never fail the release: this job's status is reported as success even when the + # Mattermost webhook is unreachable, so workflows that gate on the checks for a revision aren't blocked by it + continue-on-error: true + # the webhook is dialed over the ziti overlay with no client-side connect timeout, so cap the job instead + timeout-minutes: 5 if: github.actor != 'dependabot[bot]' steps: + # per-step too, so an unreachable webhook in the first post doesn't skip the second - uses: openziti/ziti-mattermost-action-py@v1 + continue-on-error: true if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') @@ -35,6 +42,7 @@ jobs: senderUsername: "GitHubZ" - uses: openziti/ziti-mattermost-action-py@v1 + continue-on-error: true if: | github.repository_owner == 'openziti' && ((github.event_name != 'pull_request_review') diff --git a/.github/workflows/mattermost-webhook.yml b/.github/workflows/mattermost-webhook.yml index dfa1cbd8b..7379b19d1 100644 --- a/.github/workflows/mattermost-webhook.yml +++ b/.github/workflows/mattermost-webhook.yml @@ -14,6 +14,9 @@ on: jobs: mattermost-ziti-nodejs-webhook: continue-on-error: true + # the webhook is dialed over the ziti overlay with no client-side connect timeout; when the service is down this + # job has sat for hours, stalling anything that waits on the checks for a revision + timeout-minutes: 5 runs-on: ubuntu-24.04 name: POST Webhook with NodeJS if: github.repository_owner == 'openziti' && github.actor != 'dependabot[bot]' diff --git a/.github/workflows/promote-downstreams.yml b/.github/workflows/promote-downstreams.yml index fe9df9393..0a17bdb6a 100644 --- a/.github/workflows/promote-downstreams.yml +++ b/.github/workflows/promote-downstreams.yml @@ -106,7 +106,9 @@ jobs: name: Promote ${{ matrix.package_name }}-${{ matrix.arch.rpm }}.${{ matrix.packager }} needs: parse_version strategy: - fail-fast: true + # each copy is independent and idempotent, so let the rest finish and report exactly which one failed instead of + # cancelling siblings and leaving an arbitrary subset of the packages promoted + fail-fast: false matrix: package_name: - openziti @@ -130,7 +132,7 @@ jobs: ZITI_RPM_PROD_REPO: ${{ vars.ZITI_RPM_PROD_REPO || 'zitipax-openziti-rpm-stable' }} steps: - name: Configure jFrog CLI - uses: jfrog/setup-jfrog-cli@v4 + uses: jfrog/setup-jfrog-cli@v5 env: JF_ENV_1: ${{ secrets.ZITI_ARTIFACTORY_CLI_CONFIG_PACKAGE_UPLOAD }} diff --git a/.github/workflows/publish-linux-packages.yml b/.github/workflows/publish-linux-packages.yml index d4bdc6300..0059ab098 100644 --- a/.github/workflows/publish-linux-packages.yml +++ b/.github/workflows/publish-linux-packages.yml @@ -88,7 +88,7 @@ jobs: if-no-files-found: error - name: Configure jFrog CLI - uses: jfrog/setup-jfrog-cli@v4 + uses: jfrog/setup-jfrog-cli@v5 env: JF_ENV_1: ${{ secrets.ZITI_ARTIFACTORY_CLI_CONFIG_PACKAGE_UPLOAD }} From 8dcc60d3194e1c17aa9bce43045d730fa25a6155 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Thu, 30 Jul 2026 10:58:05 -0400 Subject: [PATCH 21/73] Add tests for opaque service session token loading - verifies loadFromBolt accepts a legacy opaque service session token for its owning api session - verifies loadFromBolt rejects an opaque service session token presented under a different api session with an InvalidSessionError --- controller/handler_edge_ctrl/common_test.go | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 controller/handler_edge_ctrl/common_test.go diff --git a/controller/handler_edge_ctrl/common_test.go b/controller/handler_edge_ctrl/common_test.go new file mode 100644 index 000000000..035bad4bd --- /dev/null +++ b/controller/handler_edge_ctrl/common_test.go @@ -0,0 +1,156 @@ +package handler_edge_ctrl + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/env" + "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/network" + "github.com/stretchr/testify/require" +) + +type legacySessionTestHandler struct { + appEnv *env.AppEnv +} + +func (self *legacySessionTestHandler) getAppEnv() *env.AppEnv { + return self.appEnv +} + +func (*legacySessionTestHandler) getNetwork() *network.Network { + return nil +} + +func (*legacySessionTestHandler) getChannel() channel.Channel { + return nil +} + +func (*legacySessionTestHandler) Label() string { + return "legacy-session-test" +} + +func newLegacySessionTestEntities(t *testing.T, testCtx *model.TestContext) (*model.Managers, *model.Identity, *model.EdgeService) { + managers := testCtx.GetManagers() + identity := &model.Identity{ + Name: uuid.NewString(), + IdentityTypeId: db.DefaultIdentityType, + } + require.NoError(t, managers.Identity.Create(identity, change.New())) + + service := &model.EdgeService{Name: uuid.NewString()} + require.NoError(t, managers.EdgeService.Create(service, change.New())) + + edgeRouter := &model.EdgeRouter{Name: uuid.NewString()} + require.NoError(t, managers.EdgeRouter.Create(edgeRouter, change.New())) + + servicePolicy := &model.ServicePolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + IdentityRoles: []string{"#all"}, + ServiceRoles: []string{"#all"}, + PolicyType: db.PolicyTypeDialName, + } + require.NoError(t, managers.ServicePolicy.Create(servicePolicy, change.New())) + + edgeRouterPolicy := &model.EdgeRouterPolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + IdentityRoles: []string{"#all"}, + EdgeRouterRoles: []string{"#all"}, + } + require.NoError(t, managers.EdgeRouterPolicy.Create(edgeRouterPolicy, change.New())) + + serviceEdgeRouterPolicy := &model.ServiceEdgeRouterPolicy{ + Name: uuid.NewString(), + Semantic: db.SemanticAllOf, + ServiceRoles: []string{"#all"}, + EdgeRouterRoles: []string{"#all"}, + } + require.NoError(t, managers.ServiceEdgeRouterPolicy.Create(serviceEdgeRouterPolicy, change.New())) + + return managers, identity, service +} + +func TestLoadFromBoltSupportsOpaqueServiceSessionTokens(t *testing.T) { + testCtx := model.NewTestContext(t) + defer testCtx.Cleanup() + testCtx.Init() + + managers, identity, service := newLegacySessionTestEntities(t, testCtx) + + apiSession := &model.ApiSession{ + Token: uuid.NewString(), + IdentityId: identity.Id, + Identity: identity, + LastActivityAt: time.Now(), + } + _, err := managers.ApiSession.Create(nil, apiSession, nil) + require.NoError(t, err) + + legacySession := &model.Session{ + Token: uuid.NewString(), + IdentityId: identity.Id, + ApiSessionId: apiSession.Id, + ServiceId: service.Id, + Type: db.SessionTypeDial, + } + _, err = managers.Session.Create(legacySession, change.New()) + require.NoError(t, err) + + handler := &legacySessionTestHandler{ + appEnv: &env.AppEnv{Managers: managers}, + } + requestCtx := &baseSessionRequestContext{handler: handler} + + requestCtx.loadFromBolt(legacySession.Token, apiSession.Token) + + require.NoError(t, requestCtx.err) + require.Equal(t, legacySession.Id, requestCtx.session.Id) + require.Equal(t, apiSession.Id, requestCtx.apiSession.Id) +} + +func TestLoadFromBoltRejectsOpaqueServiceSessionForDifferentApiSession(t *testing.T) { + testCtx := model.NewTestContext(t) + defer testCtx.Cleanup() + testCtx.Init() + + managers, identity, service := newLegacySessionTestEntities(t, testCtx) + + newApiSession := func() *model.ApiSession { + apiSession := &model.ApiSession{ + Token: uuid.NewString(), + IdentityId: identity.Id, + Identity: identity, + LastActivityAt: time.Now(), + } + _, err := managers.ApiSession.Create(nil, apiSession, nil) + require.NoError(t, err) + return apiSession + } + + ownerApiSession := newApiSession() + otherApiSession := newApiSession() + legacySession := &model.Session{ + Token: uuid.NewString(), + IdentityId: identity.Id, + ApiSessionId: ownerApiSession.Id, + ServiceId: service.Id, + Type: db.SessionTypeDial, + } + _, err := managers.Session.Create(legacySession, change.New()) + require.NoError(t, err) + + handler := &legacySessionTestHandler{ + appEnv: &env.AppEnv{Managers: managers}, + } + requestCtx := &baseSessionRequestContext{handler: handler} + + requestCtx.loadFromBolt(legacySession.Token, otherApiSession.Token) + + require.IsType(t, InvalidSessionError{}, requestCtx.err) +} From d3fa41a6fc6683ac386b0130ffe1983caf7ec3c0 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 31 Jul 2026 14:08:34 -0400 Subject: [PATCH 22/73] Fix lock order inversion in ConnectionTracker. Fixes #4207 - releases the per-identity lock before acquiring the cmap shard lock when the scan loop reaps an entry, establishing a single shard-lock-then-value-lock order - extracts the entry removal into removeIfEmpty, which decides based on the value currently in the map, so an identity that reconnects after the scan decided to remove it is left alone - takes the per-identity lock in the removal check, fixing an unsynchronized read of the router map - documents the lock ordering invariant on identityConnections - adds tests covering concurrent scanning and connect/disconnect handling, entry reaping, and reconnection between the scan's decision and the removal (cherry picked from commit 0451ca7ebc90762507c614ccb61d37961fbc223d) --- controller/model/connection_tracker_test.go | 214 ++++++++++++++++++++ controller/model/identity_manager.go | 43 +++- 2 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 controller/model/connection_tracker_test.go diff --git a/controller/model/connection_tracker_test.go b/controller/model/connection_tracker_test.go new file mode 100644 index 000000000..50e1ef852 --- /dev/null +++ b/controller/model/connection_tracker_test.go @@ -0,0 +1,214 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" + + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/controller/event" +) + +// fakeCtrlChannel implements the part of ctrlchan.CtrlChannel that the connection tracker +// uses. The embedded interface is left nil so that any other method panics rather than +// quietly returning a zero value. +type fakeCtrlChannel struct { + ctrlchan.CtrlChannel + id string + closed atomic.Bool +} + +func (self *fakeCtrlChannel) PeerId() string { + return self.id +} + +func (self *fakeCtrlChannel) IsClosed() bool { + return self.closed.Load() +} + +func newTestConnectionTracker(t *testing.T) *ConnectionTracker { + closeNotify := make(chan struct{}) + t.Cleanup(func() { + close(closeNotify) + }) + + return &ConnectionTracker{ + connections: cmap.New[*identityConnections](), + eventDispatcher: event.DispatcherMock{}, + scanInterval: time.Millisecond, + unknownTimeout: time.Minute, + closeNotify: closeNotify, + } +} + +// TestConnectionTrackerLockOrdering ensures that the scan loop and connect/disconnect +// handling can run concurrently against the same identities without deadlocking. +// +// The tracker uses two locks, the cmap shard lock and the per-identity lock. Acquiring +// them in opposite orders in different code paths deadlocks permanently, and because the +// shard lock is then never released, every identity read in the controller blocks behind +// it. +func TestConnectionTrackerLockOrdering(t *testing.T) { + tracker := newTestConnectionTracker(t) + + // A small identity set keeps the scan loop and the connect/disconnect handling + // contending on the same entries and the same cmap shards. + identityIds := []string{"identity-1", "identity-2", "identity-3", "identity-4"} + + var stop atomic.Bool + var wg sync.WaitGroup + + for _, identityId := range identityIds { + wg.Add(1) + go func(identityId string) { + defer wg.Done() + ch := &fakeCtrlChannel{id: "router-1"} + for !stop.Load() { + // leaves the entry with no routers, making it a candidate for the scan + // loop to reap while this goroutine is still touching it + tracker.MarkConnected(identityId, ch) + tracker.MarkDisconnected(identityId, ch) + } + }(identityId) + } + + // An identity no other goroutine touches, so that the scan loop is the only thing + // that could take it offline. Nothing may drop an entry that has a live router + // connection, so if the scan loop removes it based on a view taken before the + // reconnect, this sees it. + var sawUnexpectedState atomic.Bool + wg.Add(1) + go func() { + defer wg.Done() + ch := &fakeCtrlChannel{id: "router-2"} + for !stop.Load() { + tracker.MarkConnected("identity-reconnecting", ch) + if tracker.GetIdentityOnlineState("identity-reconnecting") != IdentityStateOnline { + sawUnexpectedState.Store(true) + return + } + tracker.MarkDisconnected("identity-reconnecting", ch) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for !stop.Load() { + tracker.ScanForDisconnectedRouters() + } + }() + + time.AfterFunc(2*time.Second, func() { + stop.Store(true) + }) + + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(30 * time.Second): + buf := make([]byte, 4*1024*1024) + n := runtime.Stack(buf, true) + t.Fatalf("connection tracker deadlocked, goroutine dump follows:\n%s", buf[:n]) + } + + require.False(t, sawUnexpectedState.Load(), + "identity reported as not online while it had a live router connection") +} + +// TestConnectionTrackerReapsDisconnectedIdentities covers the entry removal path that the +// scan loop uses, since that is where the lock ordering has to be respected. +func TestConnectionTrackerReapsDisconnectedIdentities(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + req.Equal(IdentityStateOnline, tracker.GetIdentityOnlineState("identity-1")) + + tracker.ScanForDisconnectedRouters() + req.Equal(1, tracker.connections.Count(), "identity with a connected router should not be reaped") + + tracker.MarkDisconnected("identity-1", ch) + req.Equal(IdentityStateOffline, tracker.GetIdentityOnlineState("identity-1")) + + tracker.ScanForDisconnectedRouters() + req.Equal(0, tracker.connections.Count(), "identity with no connected routers should be reaped") +} + +// TestConnectionTrackerRemoveIfEmptyRechecksMapValue covers the window between the scan +// loop deciding an entry is empty and the entry actually being removed. Nothing is locked +// across that window, so an identity can reconnect inside it and the removal has to +// notice. +// +// The scan loop is driven a step at a time here because that window cannot be held open +// from outside; removeIfEmpty is exactly the step that runs after the decision. +func TestConnectionTrackerRemoveIfEmptyRechecksMapValue(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + tracker.MarkDisconnected("identity-1", ch) + + // the state the scan loop observes, and acts on, before removing the entry + entry, found := tracker.connections.Get("identity-1") + req.True(found) + entry.RLock() + empty := len(entry.routers) == 0 + entry.RUnlock() + req.True(empty, "scan loop would decide to remove this entry") + + // the identity reconnects before the removal runs + tracker.MarkConnected("identity-1", ch) + + tracker.removeIfEmpty("identity-1") + + req.Equal(1, tracker.connections.Count(), "reconnected identity should not be removed") + req.Equal(IdentityStateOnline, tracker.GetIdentityOnlineState("identity-1")) +} + +// TestConnectionTrackerRemoveIfEmptyRemovesEmptyEntry is the other half of +// TestConnectionTrackerRemoveIfEmptyRechecksMapValue, so that the recheck cannot be +// satisfied by simply never removing anything. +func TestConnectionTrackerRemoveIfEmptyRemovesEmptyEntry(t *testing.T) { + req := require.New(t) + tracker := newTestConnectionTracker(t) + + ch := &fakeCtrlChannel{id: "router-1"} + + tracker.MarkConnected("identity-1", ch) + tracker.MarkDisconnected("identity-1", ch) + req.Equal(1, tracker.connections.Count()) + + tracker.removeIfEmpty("identity-1") + req.Equal(0, tracker.connections.Count()) +} diff --git a/controller/model/identity_manager.go b/controller/model/identity_manager.go index c264938de..609de7236 100644 --- a/controller/model/identity_manager.go +++ b/controller/model/identity_manager.go @@ -1094,6 +1094,14 @@ const ( IdentityStateUnknown IdentityOnlineState = 2 ) +// identityConnections tracks which routers an identity is currently connected to, along +// with the last connectivity state reported for it. +// +// Two locks protect the connection tracker: the shard lock inside the ConcurrentMap of +// these, and the lock on each of these. They must always be acquired in that order, +// shard lock first. The cmap Upsert and RemoveCb callbacks run with the shard lock held, +// so taking this lock inside one of them is fine; taking the shard lock while already +// holding this one is not, and will deadlock. type identityConnections struct { sync.RWMutex routers map[string]ctrlchan.CtrlChannel @@ -1186,19 +1194,36 @@ func (self *ConnectionTracker) ScanForDisconnectedRouters() { } } - entry.Val.Lock() - if len(entry.Val.routers) == 0 { - self.connections.RemoveCb(entry.Key, func(key string, v *identityConnections, exists bool) bool { - if v != nil { - return len(v.routers) == 0 - } - return true - }) + // This check is only a filter, so that we don't take the shard write lock for + // every identity on every scan. The entry lock must be released before calling + // removeIfEmpty, which takes the shard lock. + entry.Val.RLock() + empty := len(entry.Val.routers) == 0 + entry.Val.RUnlock() + + if empty { + self.removeIfEmpty(entry.Key) } - entry.Val.Unlock() } } +// removeIfEmpty drops an identity's connection entry if it has no router connections. +// The identity may have reconnected since the caller decided the entry was empty, so the +// value currently in the map is what decides, not whatever the caller was looking at. +// +// This takes the shard lock, so it must not be called while holding an +// identityConnections lock. See identityConnections for the lock ordering rules. +func (self *ConnectionTracker) removeIfEmpty(identityId string) { + self.connections.RemoveCb(identityId, func(key string, v *identityConnections, exists bool) bool { + if v == nil { + return true + } + v.RLock() + defer v.RUnlock() + return len(v.routers) == 0 + }) +} + func (self *ConnectionTracker) MarkConnected(identityId string, ch ctrlchan.CtrlChannel) { pfxlog.Logger().WithField("identityId", identityId).WithField("routerId", ch.PeerId()).Trace("marking identity connected to router") var postUpsertCallback func() From 47689bba2e22445f3eae5749c7a0c93d3017b6d5 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 3 Aug 2026 16:12:53 -0400 Subject: [PATCH 23/73] Add 2.0.3 release notes to the CHANGELOG - adds a Release 2.0.3 section covering the two user-facing fixes backported to release-v2.0.x since v2.0.2: the ConnectionTracker lock order inversion (#4207) and the fabric terminator remove ownership check (#4166) - corrects the 2.0.2 compare link, which pointed at the v2.0.0...v2.0.1 range --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2b9a15f..7cff1cf96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Release 2.0.3 + +## What's New + +* Bug fixes + +## Component Updates and Bug Fixes + +* github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) + * [Issue #4207](https://github.com/openziti/ziti/issues/4207) - [Backport-2.0] Lock order inversion in ConnectionTracker deadlocks the controller + * [Issue #4166](https://github.com/openziti/ziti/issues/4166) - [Backport-2.0] Fabric terminator remove handlers don't verify the terminator belongs to the requesting router + # Release 2.0.2 ## What's New @@ -23,7 +35,7 @@ GitHub Security Advisories for full details, impact, and affected versions. ## Component Updates and Bug Fixes -* github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.0...v2.0.1) +* github.com/openziti/ziti/v2: [v2.0.1 -> v2.0.2](https://github.com/openziti/ziti/compare/v2.0.1...v2.0.2) * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory From e46a5c6c21d20cc27afac1968b02361018c590d6 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 6 Aug 2026 15:42:47 +0100 Subject: [PATCH 24/73] =?UTF-8?q?backport=20openziti/ziti#4063=20to=20rele?= =?UTF-8?q?ase-v2.0.x=20enroll=20with=20empty=20roles=E2=80=A6=20(#4065)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * backport openziti/ziti#4063 to release-v2.0.x enroll with empty roles when ext-jwt attribute claim is absent - treats an absent enrollment attribute claim as no role attributes rather than rejecting the token; a failed jsonPointer.Get means the claim is not present, since pointer syntax is already validated at jsonpointer.New - matches the existing unset-selector and empty-claim cases, which already enroll with an empty attribute set; a present-but-wrong-type claim still errors - adds unit coverage for resolveStringSliceClaimProperty - moves the two ext-jwt enrollment integration subtests that asserted the old failure behavior to success cases asserting an empty role-attribute set * backport openziti/ziti#4063 to v2.0.x treat a null attribute claim as no role attributes - treats an attribute claim that resolves to a null value as no role attributes instead of rejecting the enrollment with InvalidEnrollmentToken - logs at DEBUG when the attribute claim selector does not resolve and at WARN when it resolves to a null claim - formats the resolved value in the wrong-type error rather than the nil result of the failed array assertion, which always printed an empty list - corrects the godoc and the inline comment to describe the unset, non-resolving, null, and wrong-type cases as they actually behave - adds unit coverage for flat and nested null claims and tightens the wrong-type case to assert on the message - adds enrollment tests covering a null attribute claim enrolling with no role attributes and a null name claim still rejecting --- controller/model/token_provider_cache.go | 22 ++- controller/model/token_provider_cache_test.go | 110 +++++++++++++ tests/enrollment_token_to_cert_test.go | 155 +++++++++++++++--- tests/enrollment_token_to_token_test.go | 144 +++++++++++++--- 4 files changed, 378 insertions(+), 53 deletions(-) create mode 100644 controller/model/token_provider_cache_test.go diff --git a/controller/model/token_provider_cache.go b/controller/model/token_provider_cache.go index e8cb0cb52..c59c33a0e 100644 --- a/controller/model/token_provider_cache.go +++ b/controller/model/token_provider_cache.go @@ -32,10 +32,10 @@ import ( nfPem "github.com/openziti/foundation/v2/pem" "github.com/openziti/foundation/v2/stringz" "github.com/openziti/jwks" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" cmap "github.com/orcaman/concurrent-map/v2" "go.etcd.io/bbolt" ) @@ -629,7 +629,9 @@ func (r *TokenIssuerExtJwt) VerifyToken(token string) *common.TokenVerificationR } // resolveStringSliceClaimProperty extracts a string or string array from JWT claims using a JSON pointer. -// Returns a string slice even if the claim is a single string value. +// Returns a string slice even if the claim is a single string value. An unset selector, a pointer that does +// not resolve against the claims, and a null claim all resolve to no values without error. A claim that is +// present but neither a string nor an array of strings errors. func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]string, error) { if property == "" { return nil, nil @@ -648,7 +650,19 @@ func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]s val, _, err := jsonPointer.Get(claims) if err != nil { - return nil, fmt.Errorf("could not resolve json pointer: %s: %w", property, err) + // The role attributes claim is optional, so a pointer that does not resolve against this + // token's claims yields no attributes rather than an error, matching the unset-selector and + // empty-claim cases. This covers an absent key as well as a traversal failure, such as + // indexing into a scalar. + pfxlog.Logger().WithError(err).WithField("selector", property). + Debug("attribute claim selector did not resolve, enrolling with no role attributes") + return nil, nil + } + + if val == nil { + pfxlog.Logger().WithField("selector", property). + Warn("attribute claim selector resolved to a null claim, enrolling with no role attributes") + return nil, nil } strVal, ok := val.(string) @@ -663,7 +677,7 @@ func resolveStringSliceClaimProperty(claims jwt.MapClaims, property string) ([]s arrVals, ok := val.([]any) if !ok { - return nil, fmt.Errorf("could not resolve json pointer: %s: value is not a string or an array of strings, got: %v", property, arrVals) + return nil, fmt.Errorf("could not resolve json pointer: %s: value is not a string or an array of strings, got: %v", property, val) } var attributes []string diff --git a/controller/model/token_provider_cache_test.go b/controller/model/token_provider_cache_test.go new file mode 100644 index 000000000..73e39d3d3 --- /dev/null +++ b/controller/model/token_provider_cache_test.go @@ -0,0 +1,110 @@ +package model + +import ( + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/require" +) + +func Test_resolveStringSliceClaimProperty(t *testing.T) { + t.Run("returns empty when the selector is unset", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": "admin"}, "") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is absent at the selected path", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"name": "bob"}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when a nested claim is absent at the selected path", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"other": "x"}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is present but null", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": nil}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when a nested claim is present but null", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"ziti": map[string]any{"roles": nil}}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns empty when the claim is present but an empty string", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": ""}, "/roles") + + req.NoError(err) + req.Empty(vals) + }) + + t.Run("returns a single value when the claim is a string", func(t *testing.T) { + req := require.New(t) + + vals, err := resolveStringSliceClaimProperty(jwt.MapClaims{"roles": "admin"}, "/roles") + + req.NoError(err) + req.Equal([]string{"admin"}, vals) + }) + + t.Run("returns all values when the claim is a string array", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"roles": []any{"admin", "support"}} + + vals, err := resolveStringSliceClaimProperty(claims, "/roles") + + req.NoError(err) + req.Equal([]string{"admin", "support"}, vals) + }) + + t.Run("resolves a nested claim that is present", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"resource_access": map[string]any{"ziti": map[string]any{"roles": []any{"admin"}}}} + + vals, err := resolveStringSliceClaimProperty(claims, "/resource_access/ziti/roles") + + req.NoError(err) + req.Equal([]string{"admin"}, vals) + }) + + t.Run("errors when the claim is present but not a string or array of strings", func(t *testing.T) { + req := require.New(t) + + claims := jwt.MapClaims{"roles": map[string]any{"unexpected": "object"}} + + _, err := resolveStringSliceClaimProperty(claims, "/roles") + + req.Error(err) + req.ErrorContains(err, "map[unexpected:object]") + }) +} diff --git a/tests/enrollment_token_to_cert_test.go b/tests/enrollment_token_to_cert_test.go index 47bc36f90..7e7d27f6b 100644 --- a/tests/enrollment_token_to_cert_test.go +++ b/tests/enrollment_token_to_cert_test.go @@ -123,6 +123,104 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { }) }) + t.Run("when the attribute selector is set but the claim is absent", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorAbsent := createExtJwtComponents("enroll-to-cert-attr-selector-absent") + extJwtSingerAttrSelectorAbsent.Create.EnrollToCertEnabled = true + extJwtSingerAttrSelectorAbsent.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerAttrSelectorAbsent.Create.EnrollAttributeClaimsSelector = "absent-attr-selector" + extJwtSingerAttrSelectorAbsent.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorAbsent.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorAbsent.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorAbsent.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorAbsent.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorAbsent, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim absent from the token enrolls with no role attributes rather than failing + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + ctx.Req.NoError(err) + ctx.Req.NotNil(creds) + ctx.Req.NotNil(creds.Key) + ctx.Req.NotEmpty(creds.Certs) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + + t.Run("when the attribute selector is set but the claim is null", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorNull := createExtJwtComponents("enroll-to-cert-attr-selector-null") + extJwtSingerAttrSelectorNull.Create.EnrollToCertEnabled = true + extJwtSingerAttrSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerAttrSelectorNull.Create.EnrollAttributeClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerAttrSelectorNull.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorNull.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim sent as null enrolls with no role attributes rather than failing + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + ctx.Req.NoError(err) + ctx.Req.NotNil(creds) + ctx.Req.NotNil(creds.Key) + ctx.Req.NotEmpty(creds.Certs) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + t.Run("when all selectors set with multiple attributes", func(t *testing.T) { ctx.testContextChanged(t) @@ -318,6 +416,33 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { ctx.Req.Nil(creds) }) + t.Run("if the name claim selector resolves to a null claim", func(t *testing.T) { + extJwtSingerNameSelectorNull := createExtJwtComponents("enroll-to-cert-name-selector-null") + extJwtSingerNameSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID + extJwtSingerNameSelectorNull.Create.Enabled = ToPtr(true) + extJwtSingerNameSelectorNull.Create.EnrollToCertEnabled = true + extJwtSingerNameSelectorNull.Create.EnrollNameClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerNameSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerNameSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerNameSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerNameSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an identity cannot be created without a name, so unlike the optional attribute claim a + // null name claim rejects the enrollment + creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) + ctx.Req.Nil(creds) + }) + t.Run("if the name claim selector resolves to a non-string", func(t *testing.T) { extJwtSingerNameIsNumberSelectorFails := createExtJwtComponents("enroll-to-cert-name-selector-is-number-fails") extJwtSingerNameIsNumberSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID @@ -343,31 +468,6 @@ func Test_EnrollmentToken_ToCertificate(t *testing.T) { ctx.Req.Nil(creds) }) - t.Run("if the attribute claim selector does not resolve", func(t *testing.T) { - extJwtSingerAttrSelectorFails := createExtJwtComponents("enroll-to-cert-attr-selector-fails") - extJwtSingerAttrSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID - extJwtSingerAttrSelectorFails.Create.Enabled = ToPtr(true) - extJwtSingerAttrSelectorFails.Create.EnrollToCertEnabled = true - extJwtSingerAttrSelectorFails.Create.EnrollAttributeClaimsSelector = "invalid-attr-selector" - extJwtSingerAttrSelectorFails.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorFails.Create) - ctx.Req.NoError(err) - ctx.Req.NotNil(extJwtSingerAttrSelectorFails.Detail) - - enrollClaims := &claimsWithAttributes{} - enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorFails, enrollClaims) - ctx.Req.NoError(err) - ctx.Req.NotEmpty(enrollmentJwt) - - clientApi := ctx.NewEdgeClientApi(nil) - ctx.Req.NotNil(clientApi) - - creds, err := clientApi.CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt) - - ctx.Req.Error(err) - ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) - ctx.Req.Nil(creds) - }) - t.Run("if the attribute claim selector resolves to a non-string or non-string-array", func(t *testing.T) { extJwtSingerAttrIsNumberSelectorNotString := createExtJwtComponents("enroll-to-cert-attr-selector-is-number-fails") extJwtSingerAttrIsNumberSelectorNotString.Create.EnrollAuthPolicyID = *authPolicyOnlyCerts.Detail.ID @@ -695,6 +795,7 @@ const ( ClaimsWithAttributesAttributeStringPropertyName = "attributeString" ClaimsWithAttributesCustomIdPropertyName = "customId" ClaimsWithAttributesCustomNamePropertyName = "customName" + ClaimsWithAttributesNullClaimPropertyName = "nullClaim" ) type claimsWithAttributes struct { @@ -704,4 +805,8 @@ type claimsWithAttributes struct { CustomId string `json:"customId,omitempty"` CustomName string `json:"customName,omitempty"` NumberValue int64 `json:"numberValue,omitempty"` + + // NullClaim has no omitempty and is never populated, so it always serializes as an explicit JSON + // null. It models an IdP that emits an optional claim as null rather than omitting it. + NullClaim *string `json:"nullClaim"` } diff --git a/tests/enrollment_token_to_token_test.go b/tests/enrollment_token_to_token_test.go index 55062d29e..ddb253b20 100644 --- a/tests/enrollment_token_to_token_test.go +++ b/tests/enrollment_token_to_token_test.go @@ -116,6 +116,100 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { }) }) + t.Run("when the attribute selector is set but the claim is absent", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorAbsent := createExtJwtComponents("enroll-to-token-attr-selector-absent") + extJwtSingerAttrSelectorAbsent.Create.EnrollToTokenEnabled = true + extJwtSingerAttrSelectorAbsent.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerAttrSelectorAbsent.Create.EnrollAttributeClaimsSelector = "absent-attr-selector" + extJwtSingerAttrSelectorAbsent.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorAbsent.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorAbsent.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorAbsent.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorAbsent.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorAbsent, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim absent from the token enrolls with no role attributes rather than failing + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + ctx.Req.NoError(err) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + creds := edgeApis.NewJwtCredentials(enrollmentJwt) + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + + t.Run("when the attribute selector is set but the claim is null", func(t *testing.T) { + ctx.testContextChanged(t) + + extJwtSingerAttrSelectorNull := createExtJwtComponents("enroll-to-token-attr-selector-null") + extJwtSingerAttrSelectorNull.Create.EnrollToTokenEnabled = true + extJwtSingerAttrSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerAttrSelectorNull.Create.EnrollAttributeClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerAttrSelectorNull.Create.ClaimsProperty = nil + extJwtSingerAttrSelectorNull.Create.EnrollNameClaimsSelector = "" + extJwtSingerAttrSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerAttrSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an optional attribute claim sent as null enrolls with no role attributes rather than failing + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + ctx.Req.NoError(err) + + t.Run("the identity has no role attributes", func(t *testing.T) { + ctx.testContextChanged(t) + + creds := edgeApis.NewJwtCredentials(enrollmentJwt) + apiSession, err := clientApi.Authenticate(creds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + queryApiSession, err := clientApi.QueryCurrentApiSession() + ctx.Req.NoError(err) + ctx.Req.NotNil(queryApiSession) + ctx.Req.NotNil(queryApiSession.Identity) + ctx.Req.NotEmpty(queryApiSession.Identity.ID) + + createdIdentity, err := adminManClient.GetIdentity(queryApiSession.Identity.ID) + ctx.Req.NoError(err) + ctx.Req.NotNil(createdIdentity) + + ctx.Req.Empty(*createdIdentity.RoleAttributes) + }) + }) + t.Run("when all selectors set with multiple attributes", func(t *testing.T) { ctx.testContextChanged(t) @@ -305,6 +399,32 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { ctx.Req.Nil(creds) }) + t.Run("if the name claim selector resolves to a null claim", func(t *testing.T) { + extJwtSingerNameSelectorNull := createExtJwtComponents("enroll-to-token-name-selector-null") + extJwtSingerNameSelectorNull.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID + extJwtSingerNameSelectorNull.Create.Enabled = ToPtr(true) + extJwtSingerNameSelectorNull.Create.EnrollToTokenEnabled = true + extJwtSingerNameSelectorNull.Create.EnrollNameClaimsSelector = ClaimsWithAttributesNullClaimPropertyName + extJwtSingerNameSelectorNull.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerNameSelectorNull.Create) + ctx.Req.NoError(err) + ctx.Req.NotNil(extJwtSingerNameSelectorNull.Detail) + + enrollClaims := &claimsWithAttributes{} + enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerNameSelectorNull, enrollClaims) + ctx.Req.NoError(err) + ctx.Req.NotEmpty(enrollmentJwt) + + clientApi := ctx.NewEdgeClientApi(nil) + ctx.Req.NotNil(clientApi) + + // an identity cannot be created without a name, so unlike the optional attribute claim a + // null name claim rejects the enrollment + err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) + }) + t.Run("if the name claim selector resolves to a non-string", func(t *testing.T) { extJwtSingerNameIsNumberSelectorFails := createExtJwtComponents("enroll-to-token-name-selector-is-number-fails") extJwtSingerNameIsNumberSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID @@ -330,30 +450,6 @@ func Test_EnrollmentToken_ToToken(t *testing.T) { ctx.Req.Nil(creds) }) - t.Run("if the attribute claim selector does not resolve", func(t *testing.T) { - extJwtSingerAttrSelectorFails := createExtJwtComponents("enroll-to-token-attr-selector-fails") - extJwtSingerAttrSelectorFails.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID - extJwtSingerAttrSelectorFails.Create.Enabled = ToPtr(true) - extJwtSingerAttrSelectorFails.Create.EnrollToTokenEnabled = true - extJwtSingerAttrSelectorFails.Create.EnrollAttributeClaimsSelector = "invalid-attr-selector" - extJwtSingerAttrSelectorFails.Detail, err = adminManClient.CreateExtJwtSigner(extJwtSingerAttrSelectorFails.Create) - ctx.Req.NoError(err) - ctx.Req.NotNil(extJwtSingerAttrSelectorFails.Detail) - - enrollClaims := &claimsWithAttributes{} - enrollmentJwt, err := newJwtForExtJwtSigner(extJwtSingerAttrSelectorFails, enrollClaims) - ctx.Req.NoError(err) - ctx.Req.NotEmpty(enrollmentJwt) - - clientApi := ctx.NewEdgeClientApi(nil) - ctx.Req.NotNil(clientApi) - - err = clientApi.CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt) - - ctx.Req.Error(err) - ctx.Req.ApiErrorWithCode(err, apierror.InvalidEnrollmentTokenCode) - }) - t.Run("if the attribute claim selector resolves to a non-string or non-string-array", func(t *testing.T) { extJwtSingerAttrIsNumberSelectorNotString := createExtJwtComponents("enroll-to-token-attr-selector-is-number-fails") extJwtSingerAttrIsNumberSelectorNotString.Create.EnrollAuthPolicyID = *authPolicyOnlyExtJwtCreate.Detail.ID From 1764bf06b74ba563ed661510e40e6d035440f792 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 14 Jul 2026 22:25:16 -0400 Subject: [PATCH 25/73] Load cluster id before starting raft and mesh. For #4104 - loads the persisted cluster id in raft Init, right after the FSM db is opened and before raft (and therefore the mesh transport) starts - prevents an already-initialized node from briefly presenting and validating an empty cluster id, which the mesh empty-id bypass would let accept a peer from a different cluster; that connection would then persist since cluster-id validation only runs once at channel bind - drops the now-redundant load from InitEnv, leaving only command registration (cherry picked from commit 5437d7f728107272f8f58df3a87190dde9b2fef4) --- controller/raft/raft.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 0f7df0618..43ffcfdcd 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -41,7 +41,6 @@ import ( "github.com/openziti/foundation/v2/versions" "github.com/openziti/identity" "github.com/openziti/metrics" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" "github.com/openziti/ziti/v2/controller/apierror" @@ -53,6 +52,7 @@ import ( "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/peermsg" "github.com/openziti/ziti/v2/controller/raft/mesh" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" "github.com/teris-io/shortid" ) @@ -215,12 +215,8 @@ func (self *Controller) RegisterClusterEventHandler(f func(event ClusterEvent, s } func (self *Controller) InitEnv(env model.Env) error { + // The cluster id is loaded earlier, in Init, so it is set before raft and the mesh start. model.RegisterCommand(env, &InitClusterIdCmd{}, &cmd_pb.InitClusterIdCommand{}) - clusterId, err := db.LoadClusterId(env.GetDb()) - if err != nil { - return err - } - self.clusterId.Store(clusterId) return nil } @@ -640,6 +636,14 @@ func (self *Controller) Init() error { return fmt.Errorf("failed to init FSM (%w)", err) } + // Load the cluster id before raft (and the mesh) start, so this node never presents an empty id + // that the mesh empty-id bypass would let pair with a different cluster. + clusterId, err := db.LoadClusterId(self.Fsm.GetDb()) + if err != nil { + return fmt.Errorf("failed to load cluster id (%w)", err) + } + self.clusterId.Store(clusterId) + raftTransport := raft.NewNetworkTransportWithLogger(self.Mesh, 3, 10*time.Second, raftConfig.Logger) if raftConfig.Recover { From 70f97e5b0d2b437e134bf3ff97981eca447c768d Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 14 Jul 2026 22:34:21 -0400 Subject: [PATCH 26/73] Validate migration source db has data before restore. For #4104 - adds validateMigrationSourceDb, a read-only check that the migration source bolt db contains at least one identity, and calls it in RaftRestoreFromBoltDb before any streaming or bootstrap - rejects an empty, stray, or partially-written db that db.Open would otherwise open (creating the root bucket) and restore into an empty single-node cluster, which os.Stat alone could not catch - adds a unit test covering the empty and populated cases (cherry picked from commit 450b98b9a159394b89a6995b9c35645263713cf8) --- controller/controller.go | 31 ++++++++++++++++ controller/controller_test.go | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 controller/controller_test.go diff --git a/controller/controller.go b/controller/controller.go index 762188648..c440c8469 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -68,6 +68,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/teris-io/shortid" + "go.etcd.io/bbolt" ) type Controller struct { @@ -852,6 +853,32 @@ func (c *Controller) InitializeRaftFromBoltDb(sourceDbPath string) error { return c.RaftRestoreFromBoltDb(sourceDbPath) } +// validateMigrationSourceDb rejects a migration source that is not an initialized controller db. +// db.Open creates the root bucket on any file, so existence is not enough; it checks for a default +// admin identity, which an initialized controller always has. The check is read-only. +func validateMigrationSourceDb(sourceDb boltz.Db) error { + hasDefaultAdmin := false + err := sourceDb.View(func(tx *bbolt.Tx) error { + identities := boltz.Path(tx, db.RootBucket, db.EntityTypeIdentities) + if identities == nil { + return nil + } + return identities.ForEachTypedBucket(func(_ string, identity *boltz.TypedBucket) error { + if identity.GetBoolWithDefault(db.FieldIdentityIsDefaultAdmin, false) { + hasDefaultAdmin = true + } + return nil + }) + }) + if err != nil { + return errors.Wrap(err, "unable to read identities from source db") + } + if !hasDefaultAdmin { + return errors.New("source db has no default admin identity; it is empty or was never a fully initialized controller") + } + return nil +} + func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { log := pfxlog.Logger() @@ -876,6 +903,10 @@ func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { } }() + if err = validateMigrationSourceDb(sourceDb); err != nil { + return errors.Wrapf(err, "migration source db [%v] is not a valid initialized controller database", sourceDbPath) + } + timelineId, err := sourceDb.GetTimelineId(boltz.TimelineModeForceReset, shortid.Generate) if err != nil { return err diff --git a/controller/controller_test.go b/controller/controller_test.go new file mode 100644 index 000000000..0e696cd3e --- /dev/null +++ b/controller/controller_test.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package controller + +import ( + "path/filepath" + "testing" + + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" + "github.com/stretchr/testify/require" +) + +func TestValidateMigrationSourceDb(t *testing.T) { + t.Run("rejects a db with no identities", func(t *testing.T) { + // db.Open creates the root bucket but no identities, mimicking an empty or stray file. + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "empty.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.Error(t, validateMigrationSourceDb(sourceDb)) + }) + + t.Run("rejects a db whose identities include no default admin", func(t *testing.T) { + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "no-admin.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.NoError(t, addIdentity(sourceDb, "regular-identity", false)) + require.Error(t, validateMigrationSourceDb(sourceDb)) + }) + + t.Run("accepts a db that has a default admin identity", func(t *testing.T) { + sourceDb, err := db.Open(filepath.Join(t.TempDir(), "populated.db")) + require.NoError(t, err) + defer func() { _ = sourceDb.Close() }() + + require.NoError(t, addIdentity(sourceDb, "regular-identity", false)) + require.NoError(t, addIdentity(sourceDb, "admin-identity", true)) + require.NoError(t, validateMigrationSourceDb(sourceDb)) + }) +} + +// addIdentity writes an identity bucket with the isDefaultAdmin field set, using boltz so the +// stored value is read back the same way the controller reads it. +func addIdentity(sourceDb boltz.Db, id string, isDefaultAdmin bool) error { + return sourceDb.Update(nil, func(ctx boltz.MutateContext) error { + idBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.EntityTypeIdentities, id) + idBucket.SetBool(db.FieldIdentityIsDefaultAdmin, isDefaultAdmin, nil) + return idBucket.GetError() + }) +} From 5b5165d700bc8c84c4d63a1e614293c857b4699d Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 14 Jul 2026 22:38:00 -0400 Subject: [PATCH 27/73] Warn when db is set on an already-initialized cluster. For #4104 - warns when 'db' is present on a clustered controller that is already bootstrapped, where the setting is silently ignored and should be removed - leaves the first-boot migration path unchanged, since 'db' legitimately seeds a new cluster from a legacy database before the cluster is initialized (cherry picked from commit 3c12fe9dd44741dcd3c4cf4ada516c833442f339) --- controller/controller.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/controller/controller.go b/controller/controller.go index c440c8469..faba542c5 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -312,8 +312,15 @@ func NewController(cfg *config.Config, versionProvider versions.VersionProvider) c.initWeb() // need to init web before bootstrapping, so we can provide our endpoints to peers - if c.raftController != nil && !c.raftController.IsBootstrapped() { - if err = c.TryInitializeRaftFromBoltDb(); err != nil { + if c.raftController != nil { + _, dbConfigured := c.config.Src["db"] + if c.raftController.IsBootstrapped() { + // On a clustered config 'db' only seeds a new cluster on first bootstrap; once + // initialized it is dead config, so warn rather than let a stale setting sit unnoticed. + if dbConfigured { + log.Warn("'db' is set but this clustered controller is already initialized; the 'db' setting is ignored and should be removed from the configuration") + } + } else if err = c.TryInitializeRaftFromBoltDb(); err != nil { log.WithError(err).Panic("error bootstrapping raft") } } From 38c58ee8a961cdb1a9bf410255e5482e2d820e54 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 14 Jul 2026 22:50:18 -0400 Subject: [PATCH 28/73] Drop divergent mesh peers on cluster-id change; refuse forked bootstrap. For #4104 - captures each peer's advertised cluster id at channel bind and adds RevalidatePeerClusterIds, which drops connected peers whose cluster id no longer matches the local one; the mesh cluster-id check otherwise runs only once at bind and permits empty ids, so a node that connects while blank and later acquires a different id would keep that cross-cluster connection - triggers revalidation when the local cluster id is established, in InitClusterIdCmd.Apply, off the raft apply path - refuses to self-bootstrap a blank node that is already connected to cluster peers, since it should join the existing cluster rather than fork a new one - adds a unit test for the peer cluster-id mismatch selection (cherry picked from commit 63b9dd6e7967f432324afedea6c06247882b827c) --- controller/raft/mesh/mesh.go | 60 ++++++++++++++++++++-- controller/raft/mesh/mesh_test.go | 82 +++++++++++++++++++++++++++++++ controller/raft/raft.go | 17 +++++++ 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index 4c201ddb3..cfe489136 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -71,11 +71,12 @@ type Peer struct { Id raft.ServerID Address string Channel channel.Channel - RaftConns concurrenz.CopyOnWriteMap[uint32, *raftPeerConn] + ClusterId string Version *versions.VersionInfo SigningCerts []*x509.Certificate ApiAddresses map[string][]event.ApiAddress PreferredLeader bool + RaftConns concurrenz.CopyOnWriteMap[uint32, *raftPeerConn] raftPeerIdGen uint32 } @@ -324,6 +325,10 @@ type Mesh interface { RegisterClusterStateHandler(f func(state ClusterState)) Init(bindHandler channel.BindHandler) CleanupDialRecords() + + // RevalidatePeerClusterIds drops connected peers whose cluster id no longer matches the local + // cluster id. It is called when the local node acquires or changes its cluster id. + RevalidatePeerClusterIds() } func New(env Env, raftAddr raft.ServerAddress, helloHeaderProviders []HeaderProvider) Mesh { @@ -515,6 +520,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer if err = self.validateConnection(peer.Channel); err != nil { return err } + peer.ClusterId = getPeerClusterId(peer.Channel) underlay := binding.GetChannel().Underlay() id, err := self.extractPeerId(underlay.GetRemoteAddr().String(), underlay.Certificates()) @@ -590,14 +596,55 @@ func (self *impl) validateConnection(ch channel.Channel) error { } func (self *impl) checkClusterIds(ch channel.Channel) error { - clusterIdBytes, _ := headerWithFallback(ch.Underlay().Headers(), ClusterIdHeader, LegacyClusterIdHeader) - clusterId := string(clusterIdBytes) + clusterId := getPeerClusterId(ch) if clusterId != "" && self.env.GetClusterId() != "" && clusterId != self.env.GetClusterId() { return fmt.Errorf("local cluster id %s doesn't match peer cluster id %s", self.env.GetClusterId(), clusterId) } return nil } +// getPeerClusterId returns the cluster id the peer advertised, or "" if none (a blank node not yet +// joined or bootstrapped). +func getPeerClusterId(ch channel.Channel) string { + clusterIdBytes, _ := headerWithFallback(ch.Underlay().Headers(), ClusterIdHeader, LegacyClusterIdHeader) + return string(clusterIdBytes) +} + +// RevalidatePeerClusterIds drops connected peers whose known cluster id differs from the local one. +// The bind-time check runs once and skips empty ids, so a node that connected while blank and later +// acquired a different id would otherwise stay cross-connected. Called when the local id is set. +func (self *impl) RevalidatePeerClusterIds() { + localId := self.env.GetClusterId() + for _, peer := range peersWithMismatchedClusterId(localId, self.GetPeers()) { + pfxlog.Logger(). + WithField("peerId", string(peer.Id)). + WithField("peerAddress", peer.Address). + WithField("peerClusterId", peer.ClusterId). + WithField("localClusterId", localId). + Error("dropping peer connection with mismatched cluster id") + if err := peer.Channel.Close(); err != nil { + pfxlog.Logger().WithError(err). + WithField("peerId", string(peer.Id)). + Error("error closing peer channel with mismatched cluster id") + } + } +} + +// peersWithMismatchedClusterId returns peers whose known cluster id differs from localId. Peers with +// an empty id (legitimate blank joiners) are never returned, nor is anything when localId is empty. +func peersWithMismatchedClusterId(localId string, peers map[string]*Peer) []*Peer { + if localId == "" { + return nil + } + var mismatched []*Peer + for _, peer := range peers { + if peer.ClusterId != "" && peer.ClusterId != localId { + mismatched = append(mismatched, peer) + } + } + return mismatched +} + func (self *impl) checkCerts(ch channel.Channel) error { // Peer identity is taken from certs[0] via ExtractSpiffeId, so certs[0] is the certificate that must // chain to a trusted CA; VerifyLeafCertChain verifies that leaf specifically against the node's full @@ -697,6 +744,12 @@ func ExtractSpiffeId(certs []*x509.Certificate) (string, error) { func (self *impl) PeerConnected(peer *Peer, dial bool) error { self.lock.Lock() + // Re-check the cluster id under the lock: the bind-time check can race a local-id change that + // lands before the peer is registered, which RevalidatePeerClusterIds would then miss. + if localId := self.env.GetClusterId(); localId != "" && peer.ClusterId != "" && peer.ClusterId != localId { + self.lock.Unlock() + return fmt.Errorf("peer %v cluster id %s does not match local cluster id %s", peer.Id, peer.ClusterId, localId) + } if self.Peers[peer.Address] != nil { defer self.lock.Unlock() return fmt.Errorf("connection from peer %v @ %v already present", peer.Id, peer.Address) @@ -875,6 +928,7 @@ func (self *impl) AcceptUnderlay(underlay channel.Underlay) error { if err = self.validateConnection(peer.Channel); err != nil { return err } + peer.ClusterId = getPeerClusterId(peer.Channel) peer.Version = versionInfo if certHeader, found := headerWithFallback(ch.Underlay().Headers(), SigningCertHeader, LegacySigningCertHeader); found { diff --git a/controller/raft/mesh/mesh_test.go b/controller/raft/mesh/mesh_test.go index 49e46b96b..7b1b65790 100644 --- a/controller/raft/mesh/mesh_test.go +++ b/controller/raft/mesh/mesh_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/versions" "github.com/openziti/ziti/v2/controller/event" @@ -56,6 +57,7 @@ func Test_AddPeer_PassesReadonlyWhenVersionsMatch(t *testing.T) { Peers: map[string]*Peer{}, version: NewVersionProviderTest(), eventDispatcher: event.DispatcherMock{}, + env: &clusterIdEnv{}, } p := &Peer{Version: testVersion("1")} @@ -69,6 +71,7 @@ func Test_AddPeer_TurnsReadonlyWhenVersionsDoNotMatch(t *testing.T) { Peers: map[string]*Peer{}, version: NewVersionProviderTest(), eventDispatcher: event.DispatcherMock{}, + env: &clusterIdEnv{}, } p := &Peer{Version: testVersion("dne")} @@ -125,6 +128,85 @@ func testVersion(v string) *versions.VersionInfo { return &versions.VersionInfo{Version: v} } +func Test_peersWithMismatchedClusterId(t *testing.T) { + t.Run("returns nothing when local cluster id is empty", func(t *testing.T) { + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: "cluster-1"}, + } + assert.Empty(t, peersWithMismatchedClusterId("", peers)) + }) + + t.Run("returns nothing when all peers match", func(t *testing.T) { + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: "cluster-1"}, + "b": {Id: "b", ClusterId: "cluster-1"}, + } + assert.Empty(t, peersWithMismatchedClusterId("cluster-1", peers)) + }) + + t.Run("ignores a peer with an empty cluster id", func(t *testing.T) { + // A blank peer is a legitimate joiner that has not yet adopted a cluster id. + peers := map[string]*Peer{ + "a": {Id: "a", ClusterId: ""}, + } + assert.Empty(t, peersWithMismatchedClusterId("cluster-1", peers)) + }) + + t.Run("returns only the peers whose cluster id differs", func(t *testing.T) { + mismatch := &Peer{Id: "mismatch", ClusterId: "cluster-2"} + peers := map[string]*Peer{ + "match": {Id: "match", ClusterId: "cluster-1"}, + "blank": {Id: "blank", ClusterId: ""}, + "mismatch": mismatch, + } + assert.Equal(t, []*Peer{mismatch}, peersWithMismatchedClusterId("cluster-1", peers)) + }) +} + +// closeRecordingChannel is a channel.Channel that records whether Close was called. Only Close is +// exercised by RevalidatePeerClusterIds; the embedded nil interface satisfies the rest. +type closeRecordingChannel struct { + channel.Channel + closed bool +} + +func (self *closeRecordingChannel) Close() error { + self.closed = true + return nil +} + +// clusterIdEnv is a mesh Env that reports a fixed cluster id. Only GetClusterId is exercised by +// RevalidatePeerClusterIds; the embedded nil interface satisfies the rest. +type clusterIdEnv struct { + Env + clusterId string +} + +func (self *clusterIdEnv) GetClusterId() string { + return self.clusterId +} + +func Test_RevalidatePeerClusterIds_ClosesOnlyMismatchedPeers(t *testing.T) { + matchCh := &closeRecordingChannel{} + blankCh := &closeRecordingChannel{} + mismatchCh := &closeRecordingChannel{} + + m := &impl{ + env: &clusterIdEnv{clusterId: "cluster-1"}, + Peers: map[string]*Peer{ + "match": {Id: "match", ClusterId: "cluster-1", Channel: matchCh}, + "blank": {Id: "blank", ClusterId: "", Channel: blankCh}, + "mismatch": {Id: "mismatch", ClusterId: "cluster-2", Channel: mismatchCh}, + }, + } + + m.RevalidatePeerClusterIds() + + assert.False(t, matchCh.closed, "peer with matching cluster id should not be closed") + assert.False(t, blankCh.closed, "peer with an empty cluster id should not be closed") + assert.True(t, mismatchCh.closed, "peer with a mismatched cluster id should be closed") +} + type VersionProviderTest struct { } diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 43ffcfdcd..97b60d8f9 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -908,6 +908,17 @@ func (self *Controller) Bootstrap() error { logrus.Info("raft already bootstrapped") self.bootstrapped.Store(true) } else { + // Already connected to peers means this node belongs to an existing cluster; founding a new + // one here would fork a divergent cluster. + if peers := self.Mesh.GetPeers(); len(peers) > 0 { + addrs := make([]string, 0, len(peers)) + for addr := range peers { + addrs = append(addrs, addr) + } + return fmt.Errorf("refusing to bootstrap a new cluster: node is already connected to %d cluster peer(s) %v; "+ + "this node should join the existing cluster (e.g. 'ziti agent cluster add' from a current member), not initialize a new one", len(peers), addrs) + } + if err := self.migrationMgr.ValidateMigrationEnvironment(); err != nil { return err } @@ -1109,6 +1120,12 @@ type InitClusterIdCmd struct { func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { self.raftController.clusterId.Store(self.ClusterId) + if mesh := self.raftController.Mesh; mesh != nil { + // The local cluster id just changed; drop any peer that connected while this node was blank + // (accepted via the empty-id bypass) and now belongs to a different cluster. Done off the + // apply path so channel teardown does not stall raft. + go mesh.RevalidatePeerClusterIds() + } _, err := self.raftController.Fsm.GetDb().GetTimelineId(boltz.TimelineModeForceReset, func() (string, error) { return self.TimelineId, nil }) From 40845be94214a159a46493756af5f67d97b88fa3 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 14 Jul 2026 23:02:08 -0400 Subject: [PATCH 29/73] Halt on critical-command apply failure instead of advancing. For #4104 - adds a CriticalCommand marker interface for commands that establish base state, and marks SyncSnapshotCommand (a full snapshot restore) as critical - makes BoltDbFsm.Apply halt when a critical command fails to apply, rather than logging the error and persisting the advanced raft index; the failed apply's in-tx index update is rolled back and left unpersisted, so raft replays and retries the command on restart instead of the node running caught-up-on-index but empty-on-data - attaches the command type to all apply log lines so a failure is self-contained (cherry picked from commit 11e049a452b56cd6589bd5932893d0defb2b0899) --- controller/command/command.go | 10 ++++++++- controller/command/generic_cmds.go | 7 +++++- controller/network/network.go | 36 ++++++++++++++++++++---------- controller/raft/fsm.go | 12 +++++++--- controller/raft/raft.go | 30 ++++++++++++++++++------- 5 files changed, 70 insertions(+), 25 deletions(-) diff --git a/controller/command/command.go b/controller/command/command.go index cd842cfe3..843e0a135 100644 --- a/controller/command/command.go +++ b/controller/command/command.go @@ -23,9 +23,9 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/debugz" "github.com/openziti/foundation/v2/rate" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" ) @@ -42,6 +42,14 @@ type Command interface { Encode() ([]byte, error) } +// CriticalCommand marks commands that establish base state (e.g. a snapshot restore). A failed apply +// of one must halt the node rather than log-and-advance the raft index, which would leave the node +// caught up on index but missing data. Ordinary commands are logged and skipped on failure. +type CriticalCommand interface { + Command + IsCriticalCommand() +} + // Validatable instances can be validated. Command instances which implement Validable will be validated // before Command.Apply is called type Validatable interface { diff --git a/controller/command/generic_cmds.go b/controller/command/generic_cmds.go index e2d31b14e..b35ae6303 100644 --- a/controller/command/generic_cmds.go +++ b/controller/command/generic_cmds.go @@ -1,11 +1,11 @@ package command import ( - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/pkg/errors" ) @@ -144,6 +144,8 @@ func (self *DeleteEntityCommand) GetChangeContext() *change.Context { return self.Context } +var _ CriticalCommand = (*SyncSnapshotCommand)(nil) + type SyncSnapshotCommand struct { TimelineId string Snapshot []byte @@ -155,6 +157,9 @@ func (self *SyncSnapshotCommand) Apply(ctx boltz.MutateContext) error { return self.SnapshotSink(self, changeCtx.RaftIndex) } +// IsCriticalCommand marks SyncSnapshotCommand as base state: a failed apply halts rather than advances. +func (self *SyncSnapshotCommand) IsCriticalCommand() {} + func (self *SyncSnapshotCommand) Encode() ([]byte, error) { return cmd_pb.EncodeProtobuf(&cmd_pb.SyncSnapshotCommand{ SnapshotId: self.TimelineId, diff --git a/controller/network/network.go b/controller/network/network.go index 267b30c39..cb6a19593 100644 --- a/controller/network/network.go +++ b/controller/network/network.go @@ -1360,13 +1360,15 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } if currentTimelineId != "" && currentTimelineId == cmd.TimelineId { log.WithField("timelineId", cmd.TimelineId).Info("snapshot already current, skipping reload") + // The DB is already the restored snapshot, but a prior apply may have halted after the + // restore and before the raft index was recorded. Ensure the index is persisted so the node + // does not stay caught up in raft with a stale stored index. + if err = network.ensureRaftIndex(index); err != nil { + return fmt.Errorf("failed to set raft index for already-current snapshot (%w)", err) + } return nil } - if err != nil { - log.WithError(err).Error("unable to read current raft index before DB restore") - } - buf := bytes.NewBuffer(cmd.Snapshot) reader, err := gzip.NewReader(buf) if err != nil { @@ -1374,14 +1376,11 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } network.GetDb().RestoreFromReader(reader) - err = network.GetDb().Update(nil, func(ctx boltz.MutateContext) error { - raftBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.MetadataBucket) - raftBucket.SetInt64(db.FieldRaftIndex, int64(index), nil) - return nil - }) - - if err != nil { - log.WithError(err).Errorf("failed to set index after restore") + if err = network.ensureRaftIndex(index); err != nil { + // The database was restored but its raft index was not recorded. Return the error so the + // command apply fails loudly (SyncSnapshotCommand is a critical command) rather than the + // restore being treated as successful with an unrecorded index. + return fmt.Errorf("failed to set raft index after db restore (%w)", err) } time.AfterFunc(5*time.Second, func() { @@ -1392,6 +1391,19 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index return nil } +// ensureRaftIndex records the raft index if the stored one is behind it, returning an error on +// failure so a missed index update surfaces rather than being treated as success. +func (network *Network) ensureRaftIndex(index uint64) error { + return network.GetDb().Update(nil, func(ctx boltz.MutateContext) error { + if db.LoadCurrentRaftIndex(ctx.Tx()) >= index { + return nil + } + raftBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.MetadataBucket) + raftBucket.SetInt64(db.FieldRaftIndex, int64(index), nil) + return raftBucket.GetError() + }) +} + func (network *Network) AddInspectTarget(target InspectTarget) { network.inspectionTargets.Append(target) } diff --git a/controller/raft/fsm.go b/controller/raft/fsm.go index 4d14257cb..5bbe3f922 100644 --- a/controller/raft/fsm.go +++ b/controller/raft/fsm.go @@ -30,11 +30,11 @@ import ( "github.com/hashicorp/raft" "github.com/michaelquigley/pfxlog" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/event" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/sirupsen/logrus" "go.etcd.io/bbolt" bbolterrors "go.etcd.io/bbolt/errors" @@ -306,7 +306,8 @@ func (self *BoltDbFsm) Apply(log *raft.Log) interface{} { return err } - logger.Infof("apply log with type %T", cmd) + logger = logger.WithField("cmdType", fmt.Sprintf("%T", cmd)) + logger.Info("applying log") changeCtx := cmd.GetChangeContext() if changeCtx == nil { changeCtx = change.New().SetSourceType("unattributed").SetChangeAuthorType(change.AuthorTypeUnattributed) @@ -319,8 +320,13 @@ func (self *BoltDbFsm) Apply(log *raft.Log) interface{} { }) if err = cmd.Apply(ctx); err != nil { + if _, critical := cmd.(command.CriticalCommand); critical { + // Base-state command failed; halt instead of advancing over incomplete state. The + // in-tx index update rolled back with the apply, so raft replays it on restart. + logger.WithError(err).Fatal("failed to apply critical base-state command; halting rather than advancing over incomplete state") + } logger.WithError(err).Error("applying log resulted in error") - // if this errored, assume that we haven't updated the index in the db + // apply rolled back the in-tx index update; persist it here since raft advances regardless self.updateIndex(log.Index) } diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 97b60d8f9..37ab5733e 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -1112,20 +1112,19 @@ type MigrationManager interface { InitializeRaftFromBoltDb(srcDb string) error } +var _ command.CriticalCommand = (*InitClusterIdCmd)(nil) + type InitClusterIdCmd struct { ClusterId string `json:"clusterId"` TimelineId string `json:"timelineId"` raftController *Controller } +// IsCriticalCommand marks InitClusterIdCmd as base state: it establishes the cluster id, which is +// not otherwise replayed, so a failed apply must halt rather than advance. Its writes are idempotent. +func (self *InitClusterIdCmd) IsCriticalCommand() {} + func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { - self.raftController.clusterId.Store(self.ClusterId) - if mesh := self.raftController.Mesh; mesh != nil { - // The local cluster id just changed; drop any peer that connected while this node was blank - // (accepted via the empty-id bypass) and now belongs to a different cluster. Done off the - // apply path so channel teardown does not stall raft. - go mesh.RevalidatePeerClusterIds() - } _, err := self.raftController.Fsm.GetDb().GetTimelineId(boltz.TimelineModeForceReset, func() (string, error) { return self.TimelineId, nil }) @@ -1136,7 +1135,22 @@ func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { if self.raftController.env.TimelineId() != self.TimelineId { self.raftController.env.InitTimelineId(self.TimelineId) } - return db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId) + + // Persist the cluster id before publishing it in memory. db.InitClusterId commits within this + // call, so once it returns successfully the id is durable. + if err = db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId); err != nil { + return err + } + + // Only after the id is persisted, publish it in memory and revalidate peers, so memory, peer + // state, and on-disk state cannot diverge if the write fails. Revalidation drops any peer that + // connected while this node was blank (accepted via the empty-id bypass) and now belongs to a + // different cluster; it runs off the apply path so channel teardown does not stall raft. + self.raftController.clusterId.Store(self.ClusterId) + if mesh := self.raftController.Mesh; mesh != nil { + go mesh.RevalidatePeerClusterIds() + } + return nil } func (self *InitClusterIdCmd) Encode() ([]byte, error) { From 1a582480693b9ad4a156c90b2f857002e9b8307c Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 17 Jul 2026 11:34:39 -0400 Subject: [PATCH 30/73] Persist cluster id across migration snapshot restore. For #4104 - adds a clusterId field to SyncSnapshotCommand and writes it into the database after the migration snapshot restore, so a controller bootstrapped by migrating a database ends up with a durable cluster id instead of an empty one - the snapshot restore replaces the whole FSM database with the migration source, which carries no cluster id, so without this the id written during bootstrap was silently wiped and the node came up with an empty, non-durable cluster id, defeating the mesh cluster-id validation - persists the raft index after the cluster id in RestoreSnapshot so the index remains the completion gate: a failure before it halts (SyncSnapshotCommand is a critical command) and replays/retries on restart rather than skipping the command with a blank cluster id - fails RaftRestoreFromBoltDb when the cluster id is blank after bootstrap - regenerates cmd.pb.go for the new field (cherry picked from commit d8cedeb8ea1813221808f3da556a8fc59efae431) --- common/pb/cmd_pb/cmd.pb.go | 316 +++++++++++++++-------------- common/pb/cmd_pb/cmd.proto | 1 + controller/command/generic_cmds.go | 2 + controller/controller.go | 7 + controller/network/network.go | 27 ++- 5 files changed, 194 insertions(+), 159 deletions(-) diff --git a/common/pb/cmd_pb/cmd.pb.go b/common/pb/cmd_pb/cmd.pb.go index fdd5496ae..770b10477 100644 --- a/common/pb/cmd_pb/cmd.pb.go +++ b/common/pb/cmd_pb/cmd.pb.go @@ -599,6 +599,7 @@ type SyncSnapshotCommand struct { SnapshotId string `protobuf:"bytes,1,opt,name=snapshotId,proto3" json:"snapshotId,omitempty"` Snapshot []byte `protobuf:"bytes,2,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + ClusterId string `protobuf:"bytes,3,opt,name=clusterId,proto3" json:"clusterId,omitempty"` } func (x *SyncSnapshotCommand) Reset() { @@ -647,6 +648,13 @@ func (x *SyncSnapshotCommand) GetSnapshot() []byte { return nil } +func (x *SyncSnapshotCommand) GetClusterId() string { + if x != nil { + return x.ClusterId + } + return "" +} + type InitClusterIdCommand struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1413,164 +1421,166 @@ var file_cmd_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0x51, + 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0x6f, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x22, 0x54, 0x0a, 0x14, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x49, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, - 0x69, 0x6e, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, - 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x49, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, - 0x03, 0x63, 0x74, 0x78, 0x22, 0x91, 0x01, 0x0a, 0x08, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x1e, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x22, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x83, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x32, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, - 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, - 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x4e, - 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, - 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x30, - 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x22, 0xa0, 0x04, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, - 0x72, 0x73, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, - 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x7a, 0x69, 0x74, - 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x58, - 0x0a, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x7a, 0x69, 0x74, 0x69, - 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, - 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, - 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, - 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x8b, 0x05, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, - 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, - 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, - 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x08, - 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, - 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, - 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, - 0x35, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1a, - 0x0a, 0x08, 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x61, - 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x73, 0x61, 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, - 0x72, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x43, 0x74, 0x72, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0xa5, 0x01, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x61, - 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x6d, 0x74, 0x75, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, - 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x2a, 0xc3, 0x01, 0x0a, 0x0b, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, - 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, - 0x65, 0x10, 0x82, 0x10, 0x12, 0x16, 0x0a, 0x11, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0x83, 0x10, 0x12, 0x18, 0x0a, 0x13, - 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x10, 0x84, 0x10, 0x12, 0x17, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x85, 0x10, 0x12, - 0x1a, 0x0a, 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x86, 0x10, 0x12, 0x22, 0x0a, 0x1d, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x68, 0x69, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x87, 0x10, 0x2a, - 0x9e, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x01, 0x12, - 0x14, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, - 0x79, 0x70, 0x65, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, - 0x79, 0x6e, 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x0a, 0x12, 0x11, 0x0a, - 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x10, 0x0b, - 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, - 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x2f, 0x70, - 0x62, 0x2f, 0x63, 0x6d, 0x64, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x22, + 0x54, 0x0a, 0x14, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, 0x69, 0x6e, + 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x54, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x49, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x49, 0x64, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, + 0x74, 0x78, 0x22, 0x91, 0x01, 0x0a, 0x08, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1e, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x22, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x07, 0x66, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x1c, 0x0a, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x48, 0x00, 0x52, 0x08, 0x6e, 0x69, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x83, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x32, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, + 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, + 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x4e, 0x0a, 0x09, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, + 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x30, 0x0a, 0x16, + 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0xa0, + 0x04, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, + 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, + 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, + 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x31, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, + 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, + 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x11, + 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, + 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x43, 0x74, 0x72, + 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x11, 0x63, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, + 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x16, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, + 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x23, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x8b, 0x05, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1e, + 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, + 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, + 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, + 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x08, 0x70, 0x65, + 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x7a, + 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, + 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, + 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x7a, 0x69, + 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x6f, 0x73, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x69, 0x73, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x61, 0x76, 0x65, + 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0f, 0x73, 0x61, 0x76, 0x65, 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, 0x72, 0x6c, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x74, + 0x72, 0x6c, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x4e, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0xa5, 0x01, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x61, 0x72, 0x64, + 0x77, 0x61, 0x72, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6d, + 0x74, 0x75, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6d, 0x74, 0x75, 0x12, 0x14, 0x0a, + 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x2a, 0xc3, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x0f, + 0x4e, 0x65, 0x77, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, + 0x82, 0x10, 0x12, 0x16, 0x0a, 0x11, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0x83, 0x10, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x10, 0x84, 0x10, 0x12, 0x17, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x85, 0x10, 0x12, 0x1a, 0x0a, + 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x86, 0x10, 0x12, 0x22, 0x0a, 0x1d, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0x87, 0x10, 0x2a, 0x9e, 0x01, + 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, + 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x01, 0x12, 0x14, 0x0a, + 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, + 0x65, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x79, 0x6e, + 0x63, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, 0x49, + 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x10, 0x0b, 0x42, 0x26, + 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x2f, + 0x63, 0x6d, 0x64, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/common/pb/cmd_pb/cmd.proto b/common/pb/cmd_pb/cmd.proto index 070d36c37..26ba2f026 100644 --- a/common/pb/cmd_pb/cmd.proto +++ b/common/pb/cmd_pb/cmd.proto @@ -75,6 +75,7 @@ message DeleteEntityCommand { message SyncSnapshotCommand { string snapshotId = 1; bytes snapshot = 2; + string clusterId = 3; } message InitClusterIdCommand { diff --git a/controller/command/generic_cmds.go b/controller/command/generic_cmds.go index b35ae6303..8b29d0e42 100644 --- a/controller/command/generic_cmds.go +++ b/controller/command/generic_cmds.go @@ -149,6 +149,7 @@ var _ CriticalCommand = (*SyncSnapshotCommand)(nil) type SyncSnapshotCommand struct { TimelineId string Snapshot []byte + ClusterId string SnapshotSink func(cmd *SyncSnapshotCommand, index uint64) error } @@ -164,6 +165,7 @@ func (self *SyncSnapshotCommand) Encode() ([]byte, error) { return cmd_pb.EncodeProtobuf(&cmd_pb.SyncSnapshotCommand{ SnapshotId: self.TimelineId, Snapshot: self.Snapshot, + ClusterId: self.ClusterId, }) } diff --git a/controller/controller.go b/controller/controller.go index faba542c5..e1cd5c119 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -943,6 +943,13 @@ func (c *Controller) RaftRestoreFromBoltDb(sourceDbPath string) error { return fmt.Errorf("unable to bootstrap cluster (%w)", err) } + // Carry the cluster id Bootstrap established so RestoreSnapshot can write it back after the + // restore (the migration source has none). Blank here means a bug in Bootstrap, so fail. + cmd.ClusterId = c.raftController.GetClusterId() + if cmd.ClusterId == "" { + return errors.New("cluster id is blank after bootstrap; refusing to restore without a durable cluster id") + } + return c.raftController.Dispatch(cmd) } diff --git a/controller/network/network.go b/controller/network/network.go index cb6a19593..ebcd0f801 100644 --- a/controller/network/network.go +++ b/controller/network/network.go @@ -190,6 +190,7 @@ func (self *Network) decodeSyncSnapshotCommand(_ int32, data []byte) (command.Co cmd := &command.SyncSnapshotCommand{ TimelineId: msg.SnapshotId, Snapshot: msg.Snapshot, + ClusterId: msg.ClusterId, SnapshotSink: self.RestoreSnapshot, } @@ -1360,9 +1361,10 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } if currentTimelineId != "" && currentTimelineId == cmd.TimelineId { log.WithField("timelineId", cmd.TimelineId).Info("snapshot already current, skipping reload") - // The DB is already the restored snapshot, but a prior apply may have halted after the - // restore and before the raft index was recorded. Ensure the index is persisted so the node - // does not stay caught up in raft with a stale stored index. + // DB already restored; ensure cluster id then raft index (index last, see main path). + if err = network.ensureClusterId(cmd.ClusterId); err != nil { + return fmt.Errorf("failed to set cluster id for already-current snapshot (%w)", err) + } if err = network.ensureRaftIndex(index); err != nil { return fmt.Errorf("failed to set raft index for already-current snapshot (%w)", err) } @@ -1376,10 +1378,14 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index } network.GetDb().RestoreFromReader(reader) + + // Write the cluster id before the raft index. The index is the completion gate on restart (the + // FSM skips entries at or below the stored index), so persist it last: any earlier failure then + // replays and retries instead of skipping the command with a blank cluster id. + if err = network.ensureClusterId(cmd.ClusterId); err != nil { + return fmt.Errorf("failed to set cluster id after db restore (%w)", err) + } if err = network.ensureRaftIndex(index); err != nil { - // The database was restored but its raft index was not recorded. Return the error so the - // command apply fails loudly (SyncSnapshotCommand is a critical command) rather than the - // restore being treated as successful with an unrecorded index. return fmt.Errorf("failed to set raft index after db restore (%w)", err) } @@ -1391,6 +1397,15 @@ func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index return nil } +// ensureClusterId writes the cluster id if one is not already set (no-op when empty or matching). +// The snapshot restore replaces the whole db, which carries no cluster id, so it must be set here. +func (network *Network) ensureClusterId(clusterId string) error { + if clusterId == "" { + return nil + } + return db.InitClusterId(network.GetDb(), nil, clusterId) +} + // ensureRaftIndex records the raft index if the stored one is behind it, returning an error on // failure so a missed index update surfaces rather than being treated as success. func (network *Network) ensureRaftIndex(index uint64) error { From 5d368308e4c5869fdb30a902f3fc06396f981f72 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 17 Jul 2026 11:55:53 -0400 Subject: [PATCH 31/73] Backfill cluster id on leadership; make InitClusterId idempotent. For #4104 - adds a leadership-gained handler that establishes a cluster id when a bootstrapped cluster has none, so clusters migrated on older builds (which came up with an empty, non-durable cluster id) self-heal without operator action - makes db.InitClusterId set-once and idempotent: it keeps an existing cluster id and returns the effective value instead of erroring on a different one, so redundant or racing backfills cannot fail the critical InitClusterIdCmd or diverge memory from disk - passes the current timeline id through the backfill so it is left unchanged - publishes the effective cluster id in memory after persisting it (cherry picked from commit 0c3015190faa637a0bc62fac111015b52da83ae2) --- controller/db/db.go | 22 +++++++++----- controller/network/network.go | 3 +- controller/raft/raft.go | 57 ++++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/controller/db/db.go b/controller/db/db.go index 8a448d83d..9a06bfd9a 100644 --- a/controller/db/db.go +++ b/controller/db/db.go @@ -17,8 +17,7 @@ package db import ( - "fmt" - + "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/controller/storage/boltz" "go.etcd.io/bbolt" ) @@ -74,20 +73,29 @@ func LoadClusterId(db boltz.Db) (string, error) { return result, err } -func InitClusterId(db boltz.Db, ctx boltz.MutateContext, clusterId string) error { - return db.Update(ctx, func(ctx boltz.MutateContext) error { +// InitClusterId sets the cluster id if unset and returns the effective id (an existing id wins). It +// is set-once: a differing id is kept with a warning rather than an error, so redundant or racing +// writes (e.g. backfill across leadership changes) cannot fail. +func InitClusterId(db boltz.Db, ctx boltz.MutateContext, clusterId string) (string, error) { + effective := clusterId + err := db.Update(ctx, func(ctx boltz.MutateContext) error { raftBucket := boltz.GetOrCreatePath(ctx.Tx(), RootBucket, MetadataBucket) if raftBucket.HasError() { return raftBucket.Err } currentId := raftBucket.GetStringWithDefault(FieldClusterId, "") if currentId != "" { - if currentId == clusterId { - return nil + effective = currentId + if currentId != clusterId { + pfxlog.Logger(). + WithField("existingClusterId", currentId). + WithField("ignoredClusterId", clusterId). + Warn("cluster id already set; keeping existing value and ignoring the new one") } - return fmt.Errorf("cluster id already initialized to %s", currentId) + return nil } raftBucket.SetString(FieldClusterId, clusterId, nil) return raftBucket.Err }) + return effective, err } diff --git a/controller/network/network.go b/controller/network/network.go index ebcd0f801..42969f570 100644 --- a/controller/network/network.go +++ b/controller/network/network.go @@ -1403,7 +1403,8 @@ func (network *Network) ensureClusterId(clusterId string) error { if clusterId == "" { return nil } - return db.InitClusterId(network.GetDb(), nil, clusterId) + _, err := db.InitClusterId(network.GetDb(), nil, clusterId) + return err } // ensureRaftIndex records the raft index if the stored one is behind it, returning an error on diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 37ab5733e..52f75945e 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -157,6 +157,7 @@ type Controller struct { indexTracker IndexTracker migrationMgr MigrationManager clusterStateChangeHandlers concurrenz.CopyOnWriteSlice[func(event ClusterEvent, state ClusterState, leaderId string)] + clusterStateChangeLock sync.Mutex isLeader atomic.Bool clusterEvents chan raft.Observation raftRateLimiter rate.AdaptiveRateLimitTracker @@ -208,6 +209,10 @@ func (self *Controller) initErrorMappers() { } func (self *Controller) RegisterClusterEventHandler(f func(event ClusterEvent, state ClusterState, leaderId string)) { + // Hold the lock across the leader check and append so a leadership transition cannot slip + // between them, which would leave the handler seeing neither the immediate call nor the event. + self.clusterStateChangeLock.Lock() + defer self.clusterStateChangeLock.Unlock() if self.isLeader.Load() { f(ClusterEventLeadershipGained, newClusterState(true, !self.Mesh.IsReadOnly()), self.env.GetId().Token) } @@ -686,6 +691,7 @@ func (self *Controller) StartEventGeneration() { self.addEventsHandlers() go self.eventLoop() self.setupPreferredLeaderTransfer() + self.setupClusterIdBackfill() } func (self *Controller) setupPreferredLeaderTransfer() { @@ -766,6 +772,38 @@ func (self *Controller) transferToPreferredLeader() { log.Warn("no preferred leader peers are connected, retaining leadership") } +// setupClusterIdBackfill backfills a cluster id on leadership when the cluster has none, letting a +// cluster migrated on an older build (which came up with no cluster id) self-heal. If a cluster +// hasn't been bootstrapped yet, this isn't needed +func (self *Controller) setupClusterIdBackfill() { + if self.Raft.LastIndex() == 0 { + return + } + self.RegisterClusterEventHandler(func(evt ClusterEvent, state ClusterState, leaderId string) { + if evt == ClusterEventLeadershipGained { + go self.backfillClusterId() + } + }) +} + +// backfillClusterId sets a cluster id if this leader finds none; a no-op otherwise, so it is safe to +// run on every leadership change. The current timeline id is passed through unchanged. +func (self *Controller) backfillClusterId() { + if !self.IsLeader() || self.GetClusterId() != "" { + return + } + clusterId := uuid.NewString() + log := pfxlog.Logger().WithField("clusterId", clusterId) + log.Info("cluster has no cluster id; backfilling one on leadership acquisition") + if err := self.Dispatch(&InitClusterIdCmd{ + ClusterId: clusterId, + TimelineId: self.env.TimelineId(), + raftController: self, + }); err != nil { + log.WithError(err).Error("failed to backfill cluster id") + } +} + func (self *Controller) Configure(ctrlConfig *config.RaftConfig, conf *raft.Config) { conf.SnapshotThreshold = uint64(ctrlConfig.SnapshotThreshold) conf.SnapshotInterval = ctrlConfig.SnapshotInterval @@ -859,6 +897,9 @@ func (self *Controller) processRaftObservation(observation raft.Observation, eve pfxlog.Logger().Tracef("raft observation received: isLeader: %v, isReadWrite: %v", self.isLeader.Load(), eventState.isReadWrite) if raftState, ok := observation.Data.(raft.RaftState); ok { + // Serialize the leadership swap and dispatch with RegisterClusterEventHandler so a handler + // registered during a transition is not missed by both the immediate call and the event. + self.clusterStateChangeLock.Lock() if raftState == raft.Leader { if wasLeader := self.isLeader.Swap(true); !wasLeader { self.handleClusterStateChange(ClusterEventLeadershipGained, eventState) @@ -866,6 +907,7 @@ func (self *Controller) processRaftObservation(observation raft.Observation, eve } else if wasLeader := self.isLeader.Swap(false); wasLeader { self.handleClusterStateChange(ClusterEventLeadershipLost, eventState) } + self.clusterStateChangeLock.Unlock() } if state, ok := observation.Data.(mesh.ClusterState); ok { @@ -1136,17 +1178,16 @@ func (self *InitClusterIdCmd) Apply(ctx boltz.MutateContext) error { self.raftController.env.InitTimelineId(self.TimelineId) } - // Persist the cluster id before publishing it in memory. db.InitClusterId commits within this - // call, so once it returns successfully the id is durable. - if err = db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId); err != nil { + // Persist before publishing in memory. InitClusterId returns the effective id (an existing id + // wins), so a redundant command cannot diverge memory from disk. + effectiveClusterId, err := db.InitClusterId(self.raftController.Fsm.GetDb(), ctx, self.ClusterId) + if err != nil { return err } - // Only after the id is persisted, publish it in memory and revalidate peers, so memory, peer - // state, and on-disk state cannot diverge if the write fails. Revalidation drops any peer that - // connected while this node was blank (accepted via the empty-id bypass) and now belongs to a - // different cluster; it runs off the apply path so channel teardown does not stall raft. - self.raftController.clusterId.Store(self.ClusterId) + // Publish and revalidate only after persisting. Revalidation drops peers from a different + // cluster that connected while this node was blank; off the apply path so it does not stall raft. + self.raftController.clusterId.Store(effectiveClusterId) if mesh := self.raftController.Mesh; mesh != nil { go mesh.RevalidatePeerClusterIds() } From e0158107f1dc2415b9466e0128f1528151fdfd4d Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Thu, 6 Aug 2026 16:08:05 -0400 Subject: [PATCH 32/73] Add 2.0.3 CHANGELOG entry for cluster bootstrapping backport. For #4203 - notes the controller cluster bootstrapping fixes in the pending 2.0.3 section --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cff1cf96..374712386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) + * [Issue #4203](https://github.com/openziti/ziti/issues/4203) - [Backport-2.0] Controller cluster bootstrapping fixes * [Issue #4207](https://github.com/openziti/ziti/issues/4207) - [Backport-2.0] Lock order inversion in ConnectionTracker deadlocks the controller * [Issue #4166](https://github.com/openziti/ziti/issues/4166) - [Backport-2.0] Fabric terminator remove handlers don't verify the terminator belongs to the requesting router From 455bb79a946213b65ba37e388330b71e1f1a49d2 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 7 Aug 2026 14:50:29 -0400 Subject: [PATCH 33/73] Scope terminator operations to the requesting router. Fixes #4236 - rejects a remove or update request whose terminator is owned by a different router, closing the two fabric handlers the batch fix did not cover - adds a unit test for the single-terminator ownership check - adds an end-to-end test that drives the fabric control channel from an enrolled router against a second router's terminator, covering single remove, batch remove, and re-weight, plus a control that a router can still remove its own --- controller/handler_ctrl/base.go | 7 + controller/handler_ctrl/base_test.go | 21 +++ controller/handler_ctrl/remove_terminator.go | 10 ++ controller/handler_ctrl/update_terminator.go | 10 ++ tests/terminator_ownership_test.go | 131 +++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 tests/terminator_ownership_test.go diff --git a/controller/handler_ctrl/base.go b/controller/handler_ctrl/base.go index fe962dd18..da180f2f0 100644 --- a/controller/handler_ctrl/base.go +++ b/controller/handler_ctrl/base.go @@ -34,6 +34,13 @@ func (self *baseHandler) newChangeContext(ch channel.Channel, method string) *ch return change.NewControlChannelChange(self.router.Id, self.router.Name, method, ch) } +// ownsTerminator reports whether terminator belongs to the router on the other end of this handler's +// control channel. Terminator operations arriving on the fabric control channel are scoped to the +// requesting router, mirroring the edge control channel's verifyTerminator. +func (self *baseHandler) ownsTerminator(terminator *model.Terminator) bool { + return terminator.Router == self.router.Id +} + // lookupTerminatorOwner returns the id of the router that owns terminator id and whether the // terminator currently exists. A not-found terminator returns ("", false, nil); other read errors // are returned so the caller can default to keeping the id rather than acting on incomplete state. diff --git a/controller/handler_ctrl/base_test.go b/controller/handler_ctrl/base_test.go index dfa5310b8..43fed9c9f 100644 --- a/controller/handler_ctrl/base_test.go +++ b/controller/handler_ctrl/base_test.go @@ -20,6 +20,8 @@ import ( "errors" "testing" + "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/models" "github.com/stretchr/testify/require" ) @@ -69,3 +71,22 @@ func Test_filterOwnedTerminators(t *testing.T) { req.Equal(1, rejected) }) } + +// Test_ownsTerminator covers the single-terminator check used by the remove and update handlers, +// which reject outright rather than filtering. +func Test_ownsTerminator(t *testing.T) { + handler := &baseHandler{router: &model.Router{BaseEntity: models.BaseEntity{Id: "router-me"}}} + + t.Run("owned by the requesting router", func(t *testing.T) { + require.True(t, handler.ownsTerminator(&model.Terminator{Router: "router-me"})) + }) + + t.Run("owned by a different router", func(t *testing.T) { + require.False(t, handler.ownsTerminator(&model.Terminator{Router: "router-other"})) + }) + + t.Run("unset owner", func(t *testing.T) { + require.False(t, handler.ownsTerminator(&model.Terminator{}), + "a terminator with no owner must not be treated as owned by the requester") + }) +} diff --git a/controller/handler_ctrl/remove_terminator.go b/controller/handler_ctrl/remove_terminator.go index 18b9b9e81..39a34c2c9 100644 --- a/controller/handler_ctrl/remove_terminator.go +++ b/controller/handler_ctrl/remove_terminator.go @@ -64,6 +64,16 @@ func (self *removeTerminatorHandler) handleRemoveTerminator(msg *channel.Message return } + if !self.ownsTerminator(terminator) { + log. + WithField("routerId", self.router.Id). + WithField("terminator", request.TerminatorId). + WithField("terminatorRouterId", terminator.Router). + Warn("router attempted to remove a terminator it does not own; rejected") + handler_common.SendFailure(msg, ch, "terminator not owned by requesting router") + return + } + if err := self.network.Terminator.Delete(request.TerminatorId, self.newChangeContext(ch, "fabric.remove.terminator")); err == nil { log. WithField("routerId", ch.Id()). diff --git a/controller/handler_ctrl/update_terminator.go b/controller/handler_ctrl/update_terminator.go index d4a87392b..ef132d55b 100644 --- a/controller/handler_ctrl/update_terminator.go +++ b/controller/handler_ctrl/update_terminator.go @@ -66,6 +66,16 @@ func (self *updateTerminatorHandler) handleUpdateTerminator(msg *channel.Message return } + if !self.ownsTerminator(terminator) { + log. + WithField("routerId", self.router.Id). + WithField("terminator", request.TerminatorId). + WithField("terminatorRouterId", terminator.Router). + Warn("router attempted to update a terminator it does not own; rejected") + handler_common.SendFailure(msg, ch, "terminator not owned by requesting router") + return + } + if !request.UpdateCost && !request.UpdatePrecedence { // nothing to do handler_common.SendSuccess(msg, ch, "") diff --git a/tests/terminator_ownership_test.go b/tests/terminator_ownership_test.go new file mode 100644 index 000000000..4addf719c --- /dev/null +++ b/tests/terminator_ownership_test.go @@ -0,0 +1,131 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + "github.com/openziti/ziti/v2/controller/xt_smartrouting" + "google.golang.org/protobuf/proto" +) + +// Test_TerminatorOwnership drives the fabric control channel directly to confirm that terminator +// operations are scoped to the requesting router: a router may only remove or update terminators it +// owns. Without that scoping any enrolled router can delete another router's terminators (taking +// down services hosted elsewhere) or re-weight them to steer traffic. +func Test_TerminatorOwnership(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + svc := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll(xt_smartrouting.Name) + + // The requesting router: real, enrolled, and connected, so we can send on its control channel. + requestingRouter := ctx.CreateEnrollAndStartEdgeRouter() + + // The victim router only needs to exist in the model to own a terminator; it is never started. + victimRouter := ctx.AdminManagementSession.requireNewEdgeRouter() + + ctrlCh := requestingRouter.GetNetworkControllers().AnyCtrlChannel() + ctx.Req.NotNil(ctrlCh) + + // sendForResult sends a fabric ctrl_pb request and returns the controller's Result. + sendForResult := func(contentType int32, body proto.Message) *channel.Result { + bodyBytes, err := proto.Marshal(body) + ctx.Req.NoError(err) + + msg := channel.NewMessage(contentType, bodyBytes) + reply, err := msg.WithTimeout(5 * time.Second).SendForReply(ctrlCh.GetDefaultSender()) + ctx.Req.NoError(err) + + return channel.UnmarshalResult(reply) + } + + terminatorExists := func(id string) bool { + return ctx.AdminManagementSession.requireQuery("terminators/"+id) != nil + } + + terminatorPrecedence := func(id string) string { + entity := ctx.AdminManagementSession.requireQuery("terminators/" + id) + return entity.Path("data.precedence").Data().(string) + } + + newVictimTerminator := func() string { + term := ctx.AdminManagementSession.requireNewTerminator(svc.Id, victimRouter.id, "transport", "tcp:localhost:1234") + return term.id + } + + t.Run("cannot remove a terminator owned by another router", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + + result := sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorRequestType), + &ctrl_pb.RemoveTerminatorRequest{TerminatorId: terminatorId}) + + ctx.Req.False(result.Success, "removing another router's terminator must be rejected") + ctx.Req.True(terminatorExists(terminatorId), "the victim's terminator must survive the rejected removal") + }) + + t.Run("cannot remove another router's terminator in a batch", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + + // The batch handler drops ids it does not own rather than failing the whole request, so the + // assertion that matters is that the terminator survives. + sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorsRequestType), + &ctrl_pb.RemoveTerminatorsRequest{TerminatorIds: []string{terminatorId}}) + + ctx.Req.True(terminatorExists(terminatorId), "the victim's terminator must survive the batch removal") + }) + + t.Run("cannot re-weight a terminator owned by another router", func(t *testing.T) { + ctx.NextTest(t) + + terminatorId := newVictimTerminator() + ctx.Req.Equal("default", terminatorPrecedence(terminatorId)) + + result := sendForResult(int32(ctrl_pb.ContentType_UpdateTerminatorRequestType), + &ctrl_pb.UpdateTerminatorRequest{ + TerminatorId: terminatorId, + UpdatePrecedence: true, + Precedence: ctrl_pb.TerminatorPrecedence_Failed, + }) + + ctx.Req.False(result.Success, "updating another router's terminator must be rejected") + ctx.Req.Equal("default", terminatorPrecedence(terminatorId), + "the victim's terminator precedence must be unchanged") + }) + + t.Run("can remove a terminator it owns", func(t *testing.T) { + ctx.NextTest(t) + + term := ctx.AdminManagementSession.requireNewTerminator(svc.Id, requestingRouter.GetRouterId().Token, "transport", "tcp:localhost:1234") + + result := sendForResult(int32(ctrl_pb.ContentType_RemoveTerminatorRequestType), + &ctrl_pb.RemoveTerminatorRequest{TerminatorId: term.id}) + + ctx.Req.True(result.Success, "a router must still be able to remove its own terminator: %v", result.Message) + }) +} From d231be36b06ead58c644c6d451fe622bc5297b80 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 7 Aug 2026 15:30:40 -0400 Subject: [PATCH 34/73] Set env on the v1 create-circuit request context. Fixes #4242 - passes appEnv into the legacy v1 handler's request context, so token validation no longer makes a nil-interface call and panics the controller - adds a regression test driving the v1 handler over the control channel with JWT-prefixed, opaque, and empty tokens, asserting an error reply comes back and the controller keeps serving --- .../handler_edge_ctrl/create_circuit.go | 2 +- tests/create_circuit_v1_test.go | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/create_circuit_v1_test.go diff --git a/controller/handler_edge_ctrl/create_circuit.go b/controller/handler_edge_ctrl/create_circuit.go index 10cd999d9..03a60f09c 100644 --- a/controller/handler_edge_ctrl/create_circuit.go +++ b/controller/handler_edge_ctrl/create_circuit.go @@ -58,7 +58,7 @@ func (self *createCircuitHandler) HandleReceiveCreateCircuitV1(msg *channel.Mess } ctx := &CreateCircuitRequestContext{ - baseSessionRequestContext: baseSessionRequestContext{handler: self, msg: msg}, + baseSessionRequestContext: baseSessionRequestContext{handler: self, msg: msg, env: self.appEnv}, req: req, } diff --git a/tests/create_circuit_v1_test.go b/tests/create_circuit_v1_test.go new file mode 100644 index 000000000..e63377b9b --- /dev/null +++ b/tests/create_circuit_v1_test.go @@ -0,0 +1,99 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/sdk-golang/ziti/edge" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "google.golang.org/protobuf/proto" +) + +// Test_CreateCircuitV1_InvalidTokens drives the legacy v1 create-circuit handler with tokens that fail +// validation. Token validation runs against the request context's env, so a context built without it +// takes a nil-interface method call. That panic happens on the control channel's async dispatch +// goroutine, which has no recover, and would therefore terminate the whole controller rather than +// producing an error reply. +// +// The JWT-prefixed case is the one that matters: a token without that prefix is looked up in bolt and +// fails on the missing api-session before any env call, so it never reaches the vulnerable line. +func Test_CreateCircuitV1_InvalidTokens(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + edgeRouter := ctx.CreateEnrollAndStartEdgeRouter() + + ctrlCh := edgeRouter.GetNetworkControllers().AnyCtrlChannel() + ctx.Req.NotNil(ctrlCh) + + sendCreateCircuitV1 := func(apiSessionToken, sessionToken string) *channel.Message { + req := &edge_ctrl_pb.CreateCircuitRequest{ + ApiSessionToken: apiSessionToken, + SessionToken: sessionToken, + } + + body, err := proto.Marshal(req) + ctx.Req.NoError(err) + + msg := channel.NewMessage(int32(edge_ctrl_pb.ContentType_CreateCircuitRequestType), body) + reply, err := msg.WithTimeout(5 * time.Second).SendForReply(ctrlCh.GetDefaultSender()) + ctx.Req.NoError(err, "the controller must reply rather than die") + + return reply + } + + requireErrorReply := func(reply *channel.Message) { + ctx.Req.Equal(int32(edge_ctrl_pb.ContentType_ErrorType), reply.ContentType, + "expected an error reply, got content type %v", reply.ContentType) + } + + // "ey" is the OIDC access-token prefix, which routes validation through the token path. + t.Run("jwt-prefixed api session token", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("eyJhbGciOiJIUzI1NiJ9.bogus.bogus", "eyJhbGciOiJIUzI1NiJ9.bogus.bogus")) + }) + + t.Run("opaque api session token", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("not-a-real-api-session-token", "not-a-real-session-token")) + }) + + t.Run("empty tokens", func(t *testing.T) { + ctx.NextTest(t) + requireErrorReply(sendCreateCircuitV1("", "")) + }) + + // The controller must still be serving after the above; a panic on the dispatch goroutine would + // have taken the process down rather than let this run. + t.Run("controller still serving", func(t *testing.T) { + ctx.NextTest(t) + + reply := sendCreateCircuitV1("eyJhbGciOiJIUzI1NiJ9.bogus.bogus", "eyJhbGciOiJIUzI1NiJ9.bogus.bogus") + requireErrorReply(reply) + + code, found := reply.GetUint32Header(edge.ErrorCodeHeader) + ctx.Req.True(found, "error replies carry an edge error code") + ctx.Req.NotZero(code) + }) +} From ecb805491f266b16c95dd1313564a76f70aaa6a3 Mon Sep 17 00:00:00 2001 From: Shawn Carey Date: Mon, 10 Aug 2026 11:11:39 -0400 Subject: [PATCH 35/73] add l2 configuration types [v2.0.x] (#4134) * add l2 configuration types * update changelog * typo --- CHANGELOG.md | 2 + controller/db/migration_initialize.go | 167 +++++++++++++++++--------- controller/db/migrations.go | 16 ++- 3 files changed, 129 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 374712386..050a62c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ GitHub Security Advisories for full details, impact, and affected versions. * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory + * [Issue #4139](https://github.com/openziti/ziti/issues/4139) - [Backport-2.0] Add l2 service configuration types + # Release 2.0.1 diff --git a/controller/db/migration_initialize.go b/controller/db/migration_initialize.go index 58630a639..97fef1b86 100644 --- a/controller/db/migration_initialize.go +++ b/controller/db/migration_initialize.go @@ -45,6 +45,8 @@ func (m *Migrations) initialize(step *boltz.MigrationStep) int { m.addSystemAuthPolicies(step) m.createConfigType(step, interfacesConfigTypeV1) m.createConfigType(step, proxyConfigTypeV1) + m.createConfigType(step, l2HostV1ConfigType) + m.createConfigType(step, l2InterceptV1ConfigType) return CurrentDbVersion } @@ -321,6 +323,65 @@ var tunnelDefinitions = map[string]interface{}{ }, } +var listenOptions = map[string]interface{}{ + "listenOptions": map[string]interface{}{ + "type": "object", + "additionalProperties": false, + "properties": map[string]interface{}{ + "connectTimeoutSeconds": map[string]interface{}{ + "$ref": "#/definitions/timeoutSeconds", + "description": "Timeout when making outbound connections. Defaults to 5. If both connectTimeoutSeconds and connectTimeout are specified, connectTimeout will be used.", + "deprecated": true, + }, + "connectTimeout": map[string]interface{}{ + "$ref": "#/definitions/duration", + "description": "Timeout when making outbound connections. Defaults to '5s'. If both connectTimeoutSeconds and connectTimeout are specified, connectTimeout will be used.", + }, + "maxConnections": map[string]interface{}{ + "type": "integer", + "minimum": 1, + "description": "defaults to 3", + }, + "identity": map[string]interface{}{ + "type": "string", + "description": "Associate the hosting terminator with the specified identity. '$tunneler_id.name' resolves to the name of the hosting tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the hosting tunneler's identity.", + }, + "bindUsingEdgeIdentity": map[string]interface{}{ + "type": "boolean", + "description": "Associate the hosting terminator with the name of the hosting tunneler's identity. Setting this to 'true' is equivalent to setting 'identiy=$tunneler_id.name'", + }, + "cost": map[string]interface{}{ + "type": "integer", + "minimum": 0, + "maximum": 65535, + "description": "defaults to 0", + }, + "precedence": map[string]interface{}{ + "type": "string", + "enum": []interface{}{"default", "required", "failed"}, + "description": "defaults to 'default'", + }, + }, + }, +} + +var dialOptions = map[string]interface{}{ + "dialOptions": map[string]interface{}{ + "type": "object", + "additionalProperties": false, + "properties": map[string]interface{}{ + "identity": map[string]interface{}{ + "type": "string", + "description": "Dial a terminator with the specified identity. '$dst_protocol', '$dst_ip', '$dst_port are resolved to the corresponding value of the destination address.", + }, + "connectTimeoutSeconds": map[string]interface{}{ + "$ref": "#/definitions/timeoutSeconds", + "description": "defaults to 5 seconds if no dialOptions are defined. defaults to 15 if dialOptions are defined but connectTimeoutSeconds is not specified.", + }, + }, + }, +} + // hostV1 schema with ["$id"] and ["definitions"] excluded var hostV1SchemaSansDefs = map[string]interface{}{ "type": "object", @@ -388,50 +449,12 @@ var hostV1SchemaSansDefs = map[string]interface{}{ }, "description": "hosting tunnelers establish local routes for the specified source addresses so binding will succeed", }, - "listenOptions": map[string]interface{}{ - "type": "object", - "additionalProperties": false, - "properties": map[string]interface{}{ - "connectTimeoutSeconds": map[string]interface{}{ - "$ref": "#/definitions/timeoutSeconds", - "description": "Timeout when making outbound connections. Defaults to 5. If both connectTimoutSeconds and connectTimeout are specified, connectTimeout will be used.", - "deprecated": true, - }, - "connectTimeout": map[string]interface{}{ - "$ref": "#/definitions/duration", - "description": "Timeout when making outbound connections. Defaults to '5s'. If both connectTimoutSeconds and connectTimeout are specified, connectTimeout will be used.", - }, - "maxConnections": map[string]interface{}{ - "type": "integer", - "minimum": 1, - "description": "defaults to 3", - }, - "identity": map[string]interface{}{ - "type": "string", - "description": "Associate the hosting terminator with the specified identity. '$tunneler_id.name' resolves to the name of the hosting tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the hosting tunneler's identity.", - }, - "bindUsingEdgeIdentity": map[string]interface{}{ - "type": "boolean", - "description": "Associate the hosting terminator with the name of the hosting tunneler's identity. Setting this to 'true' is equivalent to setting 'identiy=$tunneler_id.name'", - }, - "cost": map[string]interface{}{ - "type": "integer", - "minimum": 0, - "maximum": 65535, - "description": "defaults to 0", - }, - "precedence": map[string]interface{}{ - "type": "string", - "enum": []interface{}{"default", "required", "failed"}, - "description": "defaults to 'default'", - }, - }, - }, "proxy": map[string]interface{}{ "$ref": "#/definitions/proxyConfiguration", "description": "If defined, outgoing connections will be send through this proxy server", }, }, + listenOptions, ), "additionalProperties": false, "allOf": []interface{}{ @@ -532,7 +555,7 @@ var interceptV1ConfigType = &ConfigType{ "type": "object", "additionalProperties": false, "definitions": tunnelDefinitions, - "properties": map[string]interface{}{ + "properties": combine(dialOptions, map[string]interface{}{ "protocols": map[string]interface{}{ "allOf": []interface{}{ map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, @@ -551,20 +574,6 @@ var interceptV1ConfigType = &ConfigType{ map[string]interface{}{"items": map[string]interface{}{"$ref": "#/definitions/portRange"}}, }, }, - "dialOptions": map[string]interface{}{ - "type": "object", - "additionalProperties": false, - "properties": map[string]interface{}{ - "identity": map[string]interface{}{ - "type": "string", - "description": "Dial a terminator with the specified identity. '$dst_protocol', '$dst_ip', '$dst_port are resolved to the corresponding value of the destination address.", - }, - "connectTimeoutSeconds": map[string]interface{}{ - "$ref": "#/definitions/timeoutSeconds", - "description": "defaults to 5 seconds if no dialOptions are defined. defaults to 15 if dialOptions are defined but connectTimeoutSeconds is not specified.", - }, - }, - }, "sourceIp": map[string]interface{}{ "type": "string", "description": "The source IP (and optional :port) to spoof when the connection is egressed from the hosting tunneler. '$tunneler_id.name' resolves to the name of the client tunneler's identity. '$tunneler_id.tag[tagName]' resolves to the value of the 'tagName' tag on the client tunneler's identity. '$src_ip' and '$src_port' resolve to the source IP / port of the originating client. '$dst_port' resolves to the port that the client is trying to connect.", @@ -576,7 +585,7 @@ var interceptV1ConfigType = &ConfigType{ }, "description": "white list of source ips/cidrs that can be intercepted. all ips can be intercepted if this is not set.", }, - }, + }), "required": []interface{}{ "protocols", "addresses", @@ -585,6 +594,54 @@ var interceptV1ConfigType = &ConfigType{ }, } +var l2HostV1ConfigType = &ConfigType{ + BaseExtEntity: boltz.BaseExtEntity{Id: "l2.host.v1"}, + Name: "l2.host.v1", + Schema: map[string]interface{}{ + "$id": "https://ziti-edge.netfoundry.io/schemas/l2.host.v1.schema.json", + "definitions": combine(healthCheckSchema["definitions"].(map[string]interface{}), tunnelDefinitions), + "type": "object", + "properties": combine(listenOptions, map[string]interface{}{ + "bridgeIfs": map[string]interface{}{ + "allOf": []interface{}{ + map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, + map[string]interface{}{"items": map[string]interface{}{"type": "string"}}, + }, + "description": "Bridge the provided network interfaces with the tunneler's tap interface.", + }, + }), + "additionalProperties": false, + }, +} + +var l2InterceptV1ConfigType = &ConfigType{ + BaseExtEntity: boltz.BaseExtEntity{Id: "l2.intercept.v1"}, + Name: "l2.intercept.v1", + Schema: map[string]interface{}{ + "$id": "https://ziti-edge.netfoundry.io/schemas/l2.intercept.v1.schema.json", + "definitions": combine(tunnelDefinitions, map[string]interface{}{ + "ethType": map[string]interface{}{ + "type": "string", + "pattern": "^0[xX][0-9a-fA-F]{4}$", + }, + }), + "type": "object", + "properties": combine(dialOptions, map[string]interface{}{ + "ethTypes": map[string]interface{}{ + "allOf": []interface{}{ + map[string]interface{}{"$ref": "#/definitions/inhabitedSet"}, + map[string]interface{}{"items": map[string]interface{}{"$ref": "#/definitions/ethType"}}, + }, + "description": "list of EtherTypes to forward. frames with an EtherType that is not in this list will be dropped.", + }, + }), + "required": []interface{}{ + "ethTypes", + }, + "additionalProperties": false, + }, +} + var InterfacesV1TypeId = "interfaces.v1" var interfacesConfigTypeV1 = &ConfigType{ diff --git a/controller/db/migrations.go b/controller/db/migrations.go index e031375d9..e14ff8bc7 100644 --- a/controller/db/migrations.go +++ b/controller/db/migrations.go @@ -41,7 +41,21 @@ func RunMigrations(db boltz.Db, stores *Stores, signingCert *x509.Certificate) e } mm := boltz.NewMigratorManager(db) - return mm.Migrate("edge", CurrentDbVersion, migrations.migrate) + if err := mm.Migrate("edge", CurrentDbVersion, migrations.migrate); err != nil { + return err + } + + // l2 config types are needed in 2.0.x, but we don't want to increment the db version to add them, + // since doing so risks colliding with the db version range used by other branches. Since bumping + // the db version is what would normally trigger the migrator to invoke migrate() again, we instead + // ensure these config types exist on every startup, independent of the stored db version. + return db.Update(nil, func(ctx boltz.MutateContext) error { + step := &boltz.MigrationStep{Component: "edge", Ctx: ctx} + for _, cfgType := range []*ConfigType{l2HostV1ConfigType, l2InterceptV1ConfigType} { + migrations.createConfigType(step, cfgType) + } + return step.GetError() + }) } func (m *Migrations) migrate(step *boltz.MigrationStep) int { From 0b87da0f4d1fa19a4cf7306c62466181355f8b1a Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:49:10 -0400 Subject: [PATCH 36/73] update access tokens automatically from ziti cli, add tests (cherry picked from commit ba3b70939e13290328466286655dc72fab60b4aa) --- ziti/cmd/cmd.go | 1 + ziti/cmd/edge/login.go | 16 ++++- ziti/util/identities.go | 91 ++++++++++++++++++++++++++ ziti/util/identities_token_test.go | 100 +++++++++++++++++++++++++++++ ziti/util/rest.go | 49 ++++++++++---- ziti/util/rest_error_test.go | 66 +++++++++++++++++++ 6 files changed, 308 insertions(+), 15 deletions(-) create mode 100644 ziti/util/identities_token_test.go create mode 100644 ziti/util/rest_error_test.go diff --git a/ziti/cmd/cmd.go b/ziti/cmd/cmd.go index f352e69dc..e656e5718 100644 --- a/ziti/cmd/cmd.go +++ b/ziti/cmd/cmd.go @@ -72,6 +72,7 @@ var rootCommand = RootCmd{ cobraCommand: &cobra.Command{ Use: "ziti", Short: "ziti is a CLI for working with Ziti", + SilenceErrors: true, // errors are printed by exitWithError, this prevents cobra from also printing them PersistentPreRun: func(cmd *cobra.Command, args []string) { cmd.SilenceUsage = true }, diff --git a/ziti/cmd/edge/login.go b/ziti/cmd/edge/login.go index 172ea59b5..9725e8e7c 100644 --- a/ziti/cmd/edge/login.go +++ b/ziti/cmd/edge/login.go @@ -757,7 +757,21 @@ func (o *LoginOptions) Login() (edge_apis.ApiSession, error) { return edge_apis.NewApiSessionLegacy(o.Token), nil } } else if o.ApiSession != nil { - return o.ApiSession, nil + if o.ApiSession.GetType() != edge_apis.ApiSessionTypeOidc || !util.OidcAccessTokenExpired(o.ApiSession) { + return o.ApiSession, nil + } + // The cached OIDC access token is expired. It can only be refreshed if the cached refresh + // token is itself still valid, otherwise the user must authenticate from scratch. + if !util.OidcRefreshTokenValid(o.ApiSession) { + return nil, fmt.Errorf("the cached access token has expired and the refresh token can no longer be used to re-authenticate, please login again") + } + o.Println("Access token has expired, refreshing it using the cached refresh token...") + refreshed, refreshErr := o.mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, o.ApiSession) + if refreshErr != nil || refreshed == nil { + return nil, fmt.Errorf("failed to refresh the access token using the cached refresh token, please login again: %w", refreshErr) + } + o.Println("Successfully refreshed the access token") + return refreshed, nil } else if o.Username != "" && o.Password != "" { authCreds = edge_apis.NewUpdbCredentials(o.Username, o.Password) } else if o.ClientCert != "" || o.ClientKey != "" { diff --git a/ziti/util/identities.go b/ziti/util/identities.go index ff867181d..3047dc90e 100644 --- a/ziti/util/identities.go +++ b/ziti/util/identities.go @@ -16,6 +16,7 @@ import ( "time" httptransport "github.com/go-openapi/runtime/client" + "github.com/golang-jwt/jwt/v5" "github.com/openziti/edge-api/rest_management_api_client" edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/sdk-golang/ziti" @@ -189,6 +190,86 @@ func (self *RestClientEdgeIdentity) NewRequest(client *resty.Client) *resty.Requ return r } +// oidcTokenLeeway skews the OIDC token expiration slightly before its true expiry so it is +// used on the border, requests won't fail 401s. +const oidcTokenLeeway = 30 * time.Second + +// OidcAccessTokenExpired reports whether sess is an OIDC session whose access token is expired (or +// will expire within oidcTokenLeeway). Non-OIDC sessions always report false. An unreadable token is +// treated as expired so the caller refreshes rather than sending a token it cannot validate. +func OidcAccessTokenExpired(sess edge_apis.ApiSession) bool { + oidcSess, ok := sess.(*edge_apis.ApiSessionOidc) + if !ok { + return false + } + claims, err := oidcSess.GetAccessClaims() + if err != nil { + return true + } + exp, err := claims.GetExpirationTime() + if err != nil || exp == nil { + return true + } + return time.Now().Add(oidcTokenLeeway).After(exp.Time) +} + +// OidcRefreshTokenValid reports whether sess carries an OIDC refresh token that is present and not +// yet expired. A missing, unreadable, or expired refresh token returns false, indicating the user +// must authenticate again rather than refresh. +func OidcRefreshTokenValid(sess edge_apis.ApiSession) bool { + oidcSess, ok := sess.(*edge_apis.ApiSessionOidc) + if !ok || oidcSess.OidcTokens == nil || oidcSess.OidcTokens.RefreshToken == "" { + return false + } + claims := &jwt.RegisteredClaims{} + if _, _, err := jwt.NewParser().ParseUnverified(oidcSess.OidcTokens.RefreshToken, claims); err != nil { + return false + } + exp, err := claims.GetExpirationTime() + if err != nil || exp == nil { + return false + } + return time.Now().Before(exp.Time) +} + +// refreshOidcTokenIfExpired refreshes the cached OIDC access token when it has expired and the +// refresh token is still valid. It returns true when the session was refreshed so the caller can +// persist the updated config. Non-OIDC sessions and still-valid access tokens are no-ops. An expired +// refresh token, or a failed refresh, returns an error telling the user to log in again. +func (self *RestClientEdgeIdentity) refreshOidcTokenIfExpired() (bool, error) { + if self.ApiSession == nil || self.ApiSession.ApiSession == nil { + return false, nil + } + if !OidcAccessTokenExpired(self.ApiSession.ApiSession) { + return false, nil + } + if !OidcRefreshTokenValid(self.ApiSession.ApiSession) { + return false, errors.New("the cached access token has expired and the refresh token can no longer be used to re-authenticate, please login again") + } + + ctrlUrl, err := url.Parse(self.Url) + if err != nil { + return false, errors.Wrapf(err, "could not parse controller url %v while refreshing token", self.Url) + } + + tlsClientConfig, err := self.NewTlsClientConfig() + if err != nil { + return false, err + } + + _, _ = fmt.Fprintln(os.Stderr, "Access token has expired, refreshing it using the cached refresh token...") + + mgmtClient := edge_apis.NewManagementApiClient([]*url.URL{ctrlUrl}, tlsClientConfig.RootCAs, nil) + refreshed, refreshErr := mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, self.ApiSession.ApiSession) + if refreshErr != nil || refreshed == nil { + return false, errors.Wrap(refreshErr, "failed to refresh the access token using the cached refresh token, please login again") + } + + self.ApiSession = &edge_apis.ApiSessionJsonWrapper{ApiSession: refreshed} + _, _ = fmt.Fprintln(os.Stderr, "Successfully refreshed the access token") + return true, nil +} + func (self *RestClientEdgeIdentity) GetBaseUrlForApi(api API) (string, error) { if api == EdgeAPI { return self.Url, nil @@ -367,6 +448,16 @@ func LoadSelectedIdentity() (RestClientIdentity, error) { return nil, errors.Errorf("no identity '%v' found in CLI config %v. You can select an existing identity using 'ziti edge use '", id, configFile) } } + + // refresh an expired OIDC access token before any request uses it, persisting the new tokens + if refreshed, err := clientIdentity.refreshOidcTokenIfExpired(); err != nil { + return nil, err + } else if refreshed { + if persistErr := PersistRestClientConfig(config); persistErr != nil { + return nil, errors.Wrap(persistErr, "could not persist refreshed token to CLI config") + } + } + selectedIdentity = clientIdentity } return selectedIdentity, nil diff --git a/ziti/util/identities_token_test.go b/ziti/util/identities_token_test.go new file mode 100644 index 000000000..7e02afa82 --- /dev/null +++ b/ziti/util/identities_token_test.go @@ -0,0 +1,100 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/stretchr/testify/require" +) + +// makeJwtWithExp builds a signed JWT carrying only an exp claim. The token helpers parse it +// unverified, so the signing key is irrelevant. +func makeJwtWithExp(t *testing.T, exp time.Time) string { + tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(exp), + }) + s, err := tok.SignedString([]byte("test-signing-key")) + require.NoError(t, err) + return s +} + +func TestOidcAccessTokenExpired(t *testing.T) { + now := time.Now() + + t.Run("non-oidc session is never treated as expired", func(t *testing.T) { + legacy := edge_apis.NewApiSessionLegacy("zt-session-token") + require.False(t, OidcAccessTokenExpired(legacy)) + }) + + t.Run("token expiring well in the future is not expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "refresh") + require.False(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("already expired token is expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("token expiring inside the leeway window is treated as expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(5*time.Second)), "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("token expiring just past the leeway window is not expired", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(oidcTokenLeeway+time.Minute)), "refresh") + require.False(t, OidcAccessTokenExpired(sess)) + }) + + t.Run("unreadable token is treated as expired so the caller refreshes", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc("not-a-jwt", "refresh") + require.True(t, OidcAccessTokenExpired(sess)) + }) +} + +func TestOidcRefreshTokenValid(t *testing.T) { + now := time.Now() + + t.Run("non-oidc session has no refresh token", func(t *testing.T) { + legacy := edge_apis.NewApiSessionLegacy("zt-session-token") + require.False(t, OidcRefreshTokenValid(legacy)) + }) + + t.Run("missing refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "") + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("unreadable refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(time.Hour)), "not-a-jwt") + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("expired refresh token is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(-time.Minute))) + require.False(t, OidcRefreshTokenValid(sess)) + }) + + t.Run("unexpired refresh token is valid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(time.Hour))) + require.True(t, OidcRefreshTokenValid(sess)) + }) +} diff --git a/ziti/util/rest.go b/ziti/util/rest.go index 9de2025fe..d863cae49 100644 --- a/ziti/util/rest.go +++ b/ziti/util/rest.go @@ -103,8 +103,7 @@ func ControllerDetailEntity(api API, entityType, entityId string, logJSON bool, } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v", - queryUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("listing %v", queryUrl), resp) } if logJSON { @@ -165,8 +164,7 @@ func ControllerList(api API, path string, params url.Values, logJSON bool, out i } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v", - queryUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("listing %v", queryUrl), resp) } if logJSON { @@ -251,6 +249,34 @@ func WrapIfApiError(err error) error { return err } +// controllerResponseError builds a concise error for a non-success controller response. Authentication +// failures get an actionable hint instead of the raw server body, and other failures surface the API +// error's code and message rather than dumping the full JSON envelope and metadata. +func controllerResponseError(action string, resp *resty.Response) error { + return controllerResponseErrorFromParts(action, resp.StatusCode(), resp.Status(), resp.Body()) +} + +// controllerResponseErrorFromParts is the testable core of controllerResponseError. +func controllerResponseErrorFromParts(action string, statusCode int, status string, body []byte) error { + if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden { + return fmt.Errorf("not authorized: your session is invalid or has expired, please run 'ziti edge login' again (%s)", status) + } + if msg := parseApiErrorMessage(body); msg != "" { + return fmt.Errorf("error %s. Status code: %s, %s", action, status, msg) + } + return fmt.Errorf("error %s. Status code: %s", action, status) +} + +// parseApiErrorMessage extracts the "CODE - message" summary from a controller API error envelope, +// returning "" when the body is not a recognizable error envelope. +func parseApiErrorMessage(body []byte) string { + env := &rest_model.APIErrorEnvelope{} + if err := json.Unmarshal(body, env); err != nil || env.Error == nil { + return "" + } + return formatApiError(env.Error) +} + type ClientOpts interface { OutputRequestJson() bool OutputResponseJson() bool @@ -318,8 +344,7 @@ func ControllerCreate(api API, entityType string, body string, out io.Writer, lo } if resp.StatusCode() != http.StatusCreated { - return nil, fmt.Errorf("error creating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("creating %v instance", entityType), resp) } if logResponseJson { @@ -373,8 +398,7 @@ func ControllerDelete(api API, entityType string, id string, body string, out io if resp.StatusCode() != http.StatusOK { statusCode := resp.StatusCode() - return &statusCode, fmt.Errorf("error deleting %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityPath, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return &statusCode, controllerResponseError(fmt.Sprintf("deleting %v instance", entityPath), resp) } if logResponseJson { @@ -416,8 +440,7 @@ func ControllerUpdate(api API, entityType string, body string, out io.Writer, me } if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusAccepted { - return nil, fmt.Errorf("error updating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("updating %v instance", entityType), resp) } if logResponseJSON { @@ -466,8 +489,7 @@ func EdgeControllerVerify(entityType, id, body string, out io.Writer, logJSON bo } if resp.StatusCode() != http.StatusOK { - return fmt.Errorf("error verifying %v instance (%v) in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - entityType, id, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return controllerResponseError(fmt.Sprintf("verifying %v instance (%v)", entityType, id), resp) } if logJSON { @@ -500,8 +522,7 @@ func EdgeControllerRequest(entityType string, out io.Writer, logJSON bool, timeo } if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error performing request [%s] %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v", - request.Method, entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp)) + return nil, controllerResponseError(fmt.Sprintf("performing request [%s] %v instance", request.Method, entityType), resp) } if logJSON { diff --git a/ziti/util/rest_error_test.go b/ziti/util/rest_error_test.go new file mode 100644 index 000000000..d3ac319ab --- /dev/null +++ b/ziti/util/rest_error_test.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestControllerResponseErrorFromParts(t *testing.T) { + const unauthorizedBody = `{ + "error": {"code": "UNAUTHORIZED", "message": "The request could not be completed. The session is not authorized or the credentials are invalid", "requestId": "aszEJJLVr"}, + "meta": {"apiEnrollmentVersion": "0.0.1", "apiVersion": "0.0.1"} + }` + + t.Run("401 returns a clean login hint and no server body", func(t *testing.T) { + err := controllerResponseErrorFromParts("listing identities", http.StatusUnauthorized, "401 Unauthorized", []byte(unauthorizedBody)) + require.EqualError(t, err, "not authorized: your session is invalid or has expired, please run 'ziti edge login' again (401 Unauthorized)") + require.NotContains(t, err.Error(), "requestId") + require.NotContains(t, err.Error(), "apiVersion") + }) + + t.Run("403 is treated like 401", func(t *testing.T) { + err := controllerResponseErrorFromParts("deleting identity", http.StatusForbidden, "403 Forbidden", nil) + require.EqualError(t, err, "not authorized: your session is invalid or has expired, please run 'ziti edge login' again (403 Forbidden)") + }) + + t.Run("other errors surface the api error code and message, not the raw body", func(t *testing.T) { + body := `{"error": {"code": "NOT_FOUND", "message": "resource not found"}}` + err := controllerResponseErrorFromParts("getting identity", http.StatusNotFound, "404 Not Found", []byte(body)) + require.EqualError(t, err, "error getting identity. Status code: 404 Not Found, NOT_FOUND - resource not found") + }) + + t.Run("unparseable body falls back to status only", func(t *testing.T) { + err := controllerResponseErrorFromParts("creating identity", http.StatusInternalServerError, "500 Internal Server Error", []byte("boom")) + require.EqualError(t, err, "error creating identity. Status code: 500 Internal Server Error") + }) +} + +func TestParseApiErrorMessage(t *testing.T) { + t.Run("valid envelope", func(t *testing.T) { + require.Equal(t, "NOT_FOUND - missing", parseApiErrorMessage([]byte(`{"error":{"code":"NOT_FOUND","message":"missing"}}`))) + }) + t.Run("no error field", func(t *testing.T) { + require.Equal(t, "", parseApiErrorMessage([]byte(`{"data":{}}`))) + }) + t.Run("not json", func(t *testing.T) { + require.Equal(t, "", parseApiErrorMessage([]byte("nope"))) + }) +} From 6b3164526fe9785f596d759cb584d25bfd201ff9 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:59:17 -0400 Subject: [PATCH 37/73] guard against nil client (cherry picked from commit 6535c2353aa9f51621ba0474dbab06a4c07d6f3b) --- ziti/cmd/edge/login.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ziti/cmd/edge/login.go b/ziti/cmd/edge/login.go index 9725e8e7c..9ec0c1ffd 100644 --- a/ziti/cmd/edge/login.go +++ b/ziti/cmd/edge/login.go @@ -765,6 +765,9 @@ func (o *LoginOptions) Login() (edge_apis.ApiSession, error) { if !util.OidcRefreshTokenValid(o.ApiSession) { return nil, fmt.Errorf("the cached access token has expired and the refresh token can no longer be used to re-authenticate, please login again") } + if o.mgmtClient == nil { + return nil, fmt.Errorf("cannot refresh the access token, no management client is available") + } o.Println("Access token has expired, refreshing it using the cached refresh token...") refreshed, refreshErr := o.mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, o.ApiSession) if refreshErr != nil || refreshed == nil { From 05661a3e4d1eb3b477e6b039e4f5c9e272709186 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:37:20 -0400 Subject: [PATCH 38/73] self-review tightening (cherry picked from commit 1bccb9b9af11c2898654851ff34f833f9d996caf) --- ziti/util/identities.go | 6 ++++-- ziti/util/identities_token_test.go | 5 +++++ ziti/util/rest.go | 5 ++++- ziti/util/rest_error_test.go | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ziti/util/identities.go b/ziti/util/identities.go index 3047dc90e..4120f568e 100644 --- a/ziti/util/identities.go +++ b/ziti/util/identities.go @@ -229,7 +229,7 @@ func OidcRefreshTokenValid(sess edge_apis.ApiSession) bool { if err != nil || exp == nil { return false } - return time.Now().Before(exp.Time) + return time.Now().Add(oidcTokenLeeway).Before(exp.Time) } // refreshOidcTokenIfExpired refreshes the cached OIDC access token when it has expired and the @@ -453,8 +453,10 @@ func LoadSelectedIdentity() (RestClientIdentity, error) { if refreshed, err := clientIdentity.refreshOidcTokenIfExpired(); err != nil { return nil, err } else if refreshed { + // the refresh already succeeded server side, so use the new token even if saving fails, + // otherwise a write error would waste a single-use refresh token if persistErr := PersistRestClientConfig(config); persistErr != nil { - return nil, errors.Wrap(persistErr, "could not persist refreshed token to CLI config") + _, _ = fmt.Fprintf(os.Stderr, "warning: could not save refreshed token to CLI config: %v\n", persistErr) } } diff --git a/ziti/util/identities_token_test.go b/ziti/util/identities_token_test.go index 7e02afa82..912afa5f6 100644 --- a/ziti/util/identities_token_test.go +++ b/ziti/util/identities_token_test.go @@ -93,6 +93,11 @@ func TestOidcRefreshTokenValid(t *testing.T) { require.False(t, OidcRefreshTokenValid(sess)) }) + t.Run("refresh token expiring inside the leeway window is invalid", func(t *testing.T) { + sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(5*time.Second))) + require.False(t, OidcRefreshTokenValid(sess)) + }) + t.Run("unexpired refresh token is valid", func(t *testing.T) { sess := edge_apis.NewApiSessionOidc(makeJwtWithExp(t, now.Add(-time.Hour)), makeJwtWithExp(t, now.Add(time.Hour))) require.True(t, OidcRefreshTokenValid(sess)) diff --git a/ziti/util/rest.go b/ziti/util/rest.go index d863cae49..fd23fc60e 100644 --- a/ziti/util/rest.go +++ b/ziti/util/rest.go @@ -258,9 +258,12 @@ func controllerResponseError(action string, resp *resty.Response) error { // controllerResponseErrorFromParts is the testable core of controllerResponseError. func controllerResponseErrorFromParts(action string, statusCode int, status string, body []byte) error { - if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden { + if statusCode == http.StatusUnauthorized { return fmt.Errorf("not authorized: your session is invalid or has expired, please run 'ziti edge login' again (%s)", status) } + if statusCode == http.StatusForbidden { + return fmt.Errorf("not authorized: this identity does not have permission to perform this action (%s)", status) + } if msg := parseApiErrorMessage(body); msg != "" { return fmt.Errorf("error %s. Status code: %s, %s", action, status, msg) } diff --git a/ziti/util/rest_error_test.go b/ziti/util/rest_error_test.go index d3ac319ab..be27d1e6b 100644 --- a/ziti/util/rest_error_test.go +++ b/ziti/util/rest_error_test.go @@ -36,9 +36,9 @@ func TestControllerResponseErrorFromParts(t *testing.T) { require.NotContains(t, err.Error(), "apiVersion") }) - t.Run("403 is treated like 401", func(t *testing.T) { + t.Run("403 reports a permission problem, not an expired session", func(t *testing.T) { err := controllerResponseErrorFromParts("deleting identity", http.StatusForbidden, "403 Forbidden", nil) - require.EqualError(t, err, "not authorized: your session is invalid or has expired, please run 'ziti edge login' again (403 Forbidden)") + require.EqualError(t, err, "not authorized: this identity does not have permission to perform this action (403 Forbidden)") }) t.Run("other errors surface the api error code and message, not the raw body", func(t *testing.T) { From 16d1567b059140dc250887a5f98cba4253049a3a Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:13:39 -0400 Subject: [PATCH 39/73] spelling fix (cherry picked from commit 356ef963db64ed662521f0da46beee36d2588ac5) --- ziti/util/rest_error_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ziti/util/rest_error_test.go b/ziti/util/rest_error_test.go index be27d1e6b..46ced0f63 100644 --- a/ziti/util/rest_error_test.go +++ b/ziti/util/rest_error_test.go @@ -47,7 +47,7 @@ func TestControllerResponseErrorFromParts(t *testing.T) { require.EqualError(t, err, "error getting identity. Status code: 404 Not Found, NOT_FOUND - resource not found") }) - t.Run("unparseable body falls back to status only", func(t *testing.T) { + t.Run("unparsable body falls back to status only", func(t *testing.T) { err := controllerResponseErrorFromParts("creating identity", http.StatusInternalServerError, "500 Internal Server Error", []byte("boom")) require.EqualError(t, err, "error creating identity. Status code: 500 Internal Server Error") }) From 4bfcf3e85b4979211e71543e92535caeddcb7ad9 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:38:34 -0400 Subject: [PATCH 40/73] add backport changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050a62c2f..0ecafe125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [Issue #4203](https://github.com/openziti/ziti/issues/4203) - [Backport-2.0] Controller cluster bootstrapping fixes * [Issue #4207](https://github.com/openziti/ziti/issues/4207) - [Backport-2.0] Lock order inversion in ConnectionTracker deadlocks the controller * [Issue #4166](https://github.com/openziti/ziti/issues/4166) - [Backport-2.0] Fabric terminator remove handlers don't verify the terminator belongs to the requesting router + * [Issue #4052](https://github.com/openziti/ziti/issues/4052) - [Backport-2.0] The `ziti` CLI now refreshes an expired access token using the cached refresh token # Release 2.0.2 From 1af18b46943006df111238e13aa16c23bb325cd4 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 18 Aug 2026 10:34:39 -0400 Subject: [PATCH 41/73] Allow the adopter name Actieve in the codespell word list - adds actieve to the codespell ignore list, so the adopter entry in ADOPTERS.md isn't reported as a misspelling of active --- .github/workflows/codespell.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 752628259..166d5bc24 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,5 +14,5 @@ jobs: - name: Run code spelling check uses: codespell-project/actions-codespell@v2 with: - ignore_words_list: allos,ans,dne,noe,referr,ssudo,te,tranfer,ue + ignore_words_list: actieve,allos,ans,dne,noe,referr,ssudo,te,tranfer,ue skip: go.*,zititest/go.*,./controller/storage/zitiql/zitiql_parser.go From 7d462196a5b5065e9e9a79ba34ac333da24a2b88 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 18 Aug 2026 12:26:08 -0400 Subject: [PATCH 42/73] Wait for backgrounded sdk/env info updates in the OIDC auth test - replaces the read-once identity lookups in Test_Authenticate_OIDC_Auth with a helper that polls the management API until the identity reports the expected sdk info - removes the fixed one second sleep that previously stood in for that wait on the updb path --- tests/auth_oidc_test.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/auth_oidc_test.go b/tests/auth_oidc_test.go index 0a96453dc..db539e299 100644 --- a/tests/auth_oidc_test.go +++ b/tests/auth_oidc_test.go @@ -202,10 +202,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - time.Sleep(time.Second) - identityDetail, err := managementHelper.GetIdentity(accessClaims.Subject) - - ctx.Req.NoError(err) + identityDetail := requireIdentitySdkInfoUpdated(ctx, managementHelper, accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) @@ -320,9 +317,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - identityDetail, err := managementHelper.GetIdentity(accessClaims.Subject) - - ctx.Req.NoError(err) + identityDetail := requireIdentitySdkInfoUpdated(ctx, managementHelper, accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) @@ -699,3 +694,17 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { }) } + +// requireIdentitySdkInfoUpdated polls the management API until the identity reports the given SDK app id and +// returns the identity as read, failing the test if it doesn't appear within 10 seconds. The controller may apply +// sdk/env info updates on a background queue, so they are not guaranteed to be visible when authentication returns. +func requireIdentitySdkInfoUpdated(ctx *TestContext, managementHelper *ManagementHelperClient, identityId string, appId string) *rest_model.IdentityDetail { + var identityDetail *rest_model.IdentityDetail + ctx.Req.Eventually(func() bool { + var err error + identityDetail, err = managementHelper.GetIdentity(identityId) + return err == nil && identityDetail.SdkInfo != nil && identityDetail.SdkInfo.AppID == appId + }, 10*time.Second, 50*time.Millisecond, "identity %s sdk info was not updated", identityId) + + return identityDetail +} From 9f25798ca83fbc0b269988585eb9ce86d21bb251 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 00:23:20 -0400 Subject: [PATCH 43/73] Anchor the post-cutoff revocation test to a later iat second - waits until a whole second after an identity revocation before authenticating the session that is expected to survive it, so the fresh token's issued-at is unambiguously later than the cutoff - a token's iat carries whole seconds while the revocation cutoff keeps full precision, so enforcement cannot distinguish a session issued just before the cutoff from one issued just after it within the same second, and rejects both; the test passed only when the reaper's latency happened to push the re-authentication into the next second - returns immediately when the clock has already moved on, which is the usual case, so the wait costs nothing outside the race --- tests/revocation_enforcement_oidc_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/revocation_enforcement_oidc_test.go b/tests/revocation_enforcement_oidc_test.go index 070afeb37..08bde55c7 100644 --- a/tests/revocation_enforcement_oidc_test.go +++ b/tests/revocation_enforcement_oidc_test.go @@ -140,6 +140,18 @@ func echoServerHandler(conn *testServerConn) error { } } +// waitForSecondAfter blocks until the wall clock reaches a whole second later than the one containing +// ts, returning immediately when it already has. Call it before authenticating a session that has to +// be observably newer than a revocation created at ts: a token's iat claim carries whole seconds, so +// enforcement cannot tell a session issued just before a revocation from one issued just after it +// within the same second, and revokes both. +func waitForSecondAfter(ts time.Time) { + next := ts.Truncate(time.Second).Add(time.Second) + if remaining := time.Until(next); remaining > 0 { + time.Sleep(remaining + 10*time.Millisecond) + } +} + // freshAuthedContext creates and authenticates a new OIDC SDK context for an // existing identity, yielding a brand-new api-session (a fresh z_asid and issue // time). Authenticate on an already-authenticated context is a no-op, so a new @@ -258,6 +270,7 @@ func Test_Revocation_IdentityCutoff_PostCutoffSessionSurvives_OIDC(t *testing.T) // can still authenticate a new session below.) _, err := adminApi.CreateRevocation(dialIdentity.Id, rest_model.RevocationTypeEnumIDENTITY) ctx.Req.NoError(err) + revocationCreated := time.Now() requireConnClosedByReaper(ctx, clientConn) t.Run("a session authenticated after the cutoff survives", func(t *testing.T) { @@ -265,6 +278,7 @@ func Test_Revocation_IdentityCutoff_PostCutoffSessionSurvives_OIDC(t *testing.T) // A fresh session is issued after the cutoff, so it must not be revoked // even though the identity revocation still lingers. + waitForSecondAfter(revocationCreated) freshContext := freshAuthedContext(t, ctx, dialIdentity) newConn := ctx.WrapConn(freshContext.Dial(svc.Name)) requireConnSurvivesReaper(ctx, newConn) From 7b639eafc56c149dfe48457a91328f02c8a1ff07 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 17 Aug 2026 15:38:03 -0400 Subject: [PATCH 44/73] Close the xgress when a conn without half-close ends. Fixes #4269 - returns ErrPeerClosed rather than a bare io.EOF from XgressConn.ReadPayload when the connection has no half-close semantics, so Xgress.rx() runs its existing teardown - keeps the half-close path untouched, so a conn that can signal a write-side close still reports the fin headers followed by io.EOF - stops datagram flows from orphaning an xgress: their deadline setters are no-ops, so the liveness probe cannot detect the flow is gone, and the bare io.EOF read as a half-close left the xgress waiting on an end-of-circuit that could not arrive, holding its goroutines and buffers until the process exited - covers both ReadPayload branches, and the circuit teardown that depends on them - adds a contributors section to the release notes, along with the entries for backports already merged since v2.0.2 that had not been picked up --- CHANGELOG.md | 16 ++- router/xgress_common/connection.go | 9 ++ router/xgress_common/connection_test.go | 87 ++++++++++++++ router/xgress_test/xgress_teardown_test.go | 131 +++++++++++++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 router/xgress_common/connection_test.go create mode 100644 router/xgress_test/xgress_teardown_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ecafe125..d203cc2b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,26 @@ * Bug fixes +## Contributors + +Thanks to the community members who contributed to this release. + +* [@msbusk](https://github.com/msbusk) diagnosed the circuit leak in + [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a + production workload. + ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) - * [Issue #4203](https://github.com/openziti/ziti/issues/4203) - [Backport-2.0] Controller cluster bootstrapping fixes + * [Issue #4269](https://github.com/openziti/ziti/issues/4269) - [Backport-2.0] Router leaks LinkSendBuffer goroutines in `drainDeadlines()` — circuits accumulate until the router OOMs + * [Issue #4242](https://github.com/openziti/ziti/issues/4242) - [Backport-2.0] Legacy v1 create-circuit handler crashes the controller on JWT-prefixed tokens + * [Issue #4236](https://github.com/openziti/ziti/issues/4236) - [Backport-2.0] Ensure terminator operations are scoped by source router * [Issue #4207](https://github.com/openziti/ziti/issues/4207) - [Backport-2.0] Lock order inversion in ConnectionTracker deadlocks the controller + * [Issue #4203](https://github.com/openziti/ziti/issues/4203) - [Backport-2.0] Controller cluster bootstrapping fixes * [Issue #4166](https://github.com/openziti/ziti/issues/4166) - [Backport-2.0] Fabric terminator remove handlers don't verify the terminator belongs to the requesting router + * [Issue #4161](https://github.com/openziti/ziti/issues/4161) - [Backport-2.0] Leaderless controller strands terminator operations during cluster membership changes + * [Issue #4146](https://github.com/openziti/ziti/issues/4146) - [Backport-2.0] Upgraded controller rejects legacy clients' existing sessions and gives no recovery signal for invalid service tokens + * [Issue #4142](https://github.com/openziti/ziti/issues/4142) - [Backport-2.0] Service-policy enforcer deletes valid legacy sessions; type= queries use numeric id against the string-mapped symbol * [Issue #4052](https://github.com/openziti/ziti/issues/4052) - [Backport-2.0] The `ziti` CLI now refreshes an expired access token using the cached refresh token # Release 2.0.2 diff --git a/router/xgress_common/connection.go b/router/xgress_common/connection.go index b82770864..886f37599 100644 --- a/router/xgress_common/connection.go +++ b/router/xgress_common/connection.go @@ -253,6 +253,15 @@ func (self *XgressConn) ReadPayload() ([]byte, map[uint8][]byte, error) { // next time return EOF return nil, nil, io.EOF } + + // This conn has no half-close, so a read EOF means the local side is finished for + // good and will never signal anything further. Report a full close rather than a bare + // io.EOF, which Xgress.rx() would treat as a half-close, leaving the xgress waiting on + // an end-of-circuit that cannot arrive. + // + // The liveness probe above cannot catch this: conns without half-close include the + // datagram ones, and udp_vconn implements deadlines as no-ops that always succeed. + return nil, nil, xgress.ErrPeerClosed } return buffer, nil, err diff --git a/router/xgress_common/connection_test.go b/router/xgress_common/connection_test.go new file mode 100644 index 000000000..82099d6e2 --- /dev/null +++ b/router/xgress_common/connection_test.go @@ -0,0 +1,87 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xgress_common + +import ( + "io" + "net" + "testing" + "time" + + "github.com/openziti/sdk-golang/xgress" + "github.com/stretchr/testify/require" +) + +// eofConn is a connection whose reads have run out. Its deadline setters succeed, matching the +// datagram connections that implement them as no-ops, so the liveness probe in ReadPayload +// cannot tell that the peer is gone. +type eofConn struct { + net.Conn +} + +func (self *eofConn) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (self *eofConn) Write(b []byte) (int, error) { + return len(b), nil +} + +func (self *eofConn) Close() error { + return nil +} + +func (self *eofConn) SetReadDeadline(time.Time) error { return nil } +func (self *eofConn) SetWriteDeadline(time.Time) error { return nil } + +// TestReadPayloadEofWithoutHalfClose covers a connection with no half-close semantics reaching +// end of input. There is nothing further the local side can signal, so this has to be reported +// as a full close. Reporting a bare io.EOF instead reads as a half-close to Xgress.rx(), which +// leaves the xgress waiting on an end-of-circuit that will never arrive. +func TestReadPayloadEofWithoutHalfClose(t *testing.T) { + req := require.New(t) + + conn := NewXgressConn(&eofConn{}, false, ConnTypeTunnel) + + data, headers, err := conn.ReadPayload() + req.ErrorIs(err, xgress.ErrPeerClosed) + req.Nil(data) + req.Nil(headers) + + // still reported on a subsequent read, rather than reverting to a half-close + _, _, err = conn.ReadPayload() + req.ErrorIs(err, xgress.ErrPeerClosed) +} + +// TestReadPayloadEofWithHalfClose covers the half-close path, which must keep reporting the FIN +// headers followed by io.EOF so the write half can close while the read half stays open. +func TestReadPayloadEofWithHalfClose(t *testing.T) { + req := require.New(t) + + conn := NewXgressConn(&eofConn{}, true, ConnTypeTunnel) + + data, headers, err := conn.ReadPayload() + req.NoError(err) + req.Nil(data) + req.Equal(GetFinHeaders(), headers, "first read at end of input should carry the fin headers") + + data, headers, err = conn.ReadPayload() + req.ErrorIs(err, io.EOF) + req.Empty(data) + req.Nil(headers) + req.False(conn.IsClosed(), "half-close should leave the conn open for reading") +} diff --git a/router/xgress_test/xgress_teardown_test.go b/router/xgress_test/xgress_teardown_test.go new file mode 100644 index 000000000..542fdadac --- /dev/null +++ b/router/xgress_test/xgress_teardown_test.go @@ -0,0 +1,131 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package xgress_test + +import ( + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/openziti/sdk-golang/xgress" + "github.com/openziti/ziti/v2/router/xgress_common" + "github.com/stretchr/testify/require" +) + +// TestXgressClosesWhenPeerEndsWithoutHalfClose covers a circuit whose initiating connection has +// no half-close semantics, as datagram flows from an intercepting tunneler do. When such a +// connection reaches end of input it is finished for good, so the xgress must close and take the +// circuit with it. +// +// Treating that end of input as a half-close instead leaves the xgress waiting on an +// end-of-circuit its peer has no reason to send, so it is never closed, never reported to the +// controller, and holds its goroutines and buffers until the process exits. +func TestXgressClosesWhenPeerEndsWithoutHalfClose(t *testing.T) { + req := require.New(t) + + adapter := NewMockDataPlaneAdapter() + defer adapter.Close() + + circuitId := "teardown-circuit" + initAddr := xgress.Address("init") + termAddr := xgress.Address("term") + + options := xgress.DefaultOptions() + + // halfClose false: the flow has no way to signal a write-side close on its own + endedFlow := &endedFlowConn{} + initiatorConn := xgress_common.NewXgressConn(endedFlow, false, xgress_common.ConnTypeTunnel) + terminatorConn := &blockingConn{closeNotify: make(chan struct{})} + + initiator := xgress.NewXgress(circuitId, "test", initAddr, initiatorConn, xgress.Initiator, options, nil) + terminator := xgress.NewXgress(circuitId, "test", termAddr, terminatorConn, xgress.Terminator, options, nil) + + initiator.SetDataPlaneAdapter(adapter) + terminator.SetDataPlaneAdapter(adapter) + adapter.RegisterXgress(initiator) + adapter.RegisterXgress(terminator) + adapter.ConnectCircuit(circuitId, initAddr, termAddr) + adapter.ConnectCircuit(circuitId, termAddr, initAddr) + + initiatorClosed := make(chan struct{}) + initiator.AddCloseHandler(xgress.CloseHandlerF(func(*xgress.Xgress) { + close(initiatorClosed) + })) + + initiator.Start() + terminator.Start() + + select { + case <-initiatorClosed: + case <-time.After(30 * time.Second): + req.Fail("initiating xgress was never closed after its connection ended") + } + + req.True(initiator.IsClosed(), "initiating xgress should report closed") + req.Equal(int32(1), endedFlow.closeCount.Load(), + "closing the xgress should release the connection underneath it, exactly once") +} + +// blockingConn is a peer that never produces or accepts anything, standing in for a hosted +// application that has no reason to speak first. +type blockingConn struct { + closeNotify chan struct{} + closed bool +} + +func (self *blockingConn) LogContext() string { return "blocking" } + +func (self *blockingConn) ReadPayload() ([]byte, map[uint8][]byte, error) { + <-self.closeNotify + return nil, nil, io.EOF +} + +func (self *blockingConn) WritePayload(b []byte, _ map[uint8][]byte) (int, error) { + return len(b), nil +} + +func (self *blockingConn) Close() error { + if !self.closed { + self.closed = true + close(self.closeNotify) + } + return nil +} + +func (self *blockingConn) HandleControlMsg(xgress.ControlType, channel.Headers, xgress.ControlReceiver) error { + return nil +} + +// endedFlowConn models a datagram flow whose association has expired. Reads report end of input, +// and the deadline setters succeed the way udp_vconn's no-op implementations do, so the liveness +// probe in ReadPayload cannot tell that the flow is gone. +type endedFlowConn struct { + net.Conn + closeCount atomic.Int32 +} + +func (self *endedFlowConn) Read([]byte) (int, error) { return 0, io.EOF } +func (self *endedFlowConn) Write(b []byte) (int, error) { return len(b), nil } +func (self *endedFlowConn) Close() error { self.closeCount.Add(1); return nil } +func (self *endedFlowConn) LocalAddr() net.Addr { return nil } +func (self *endedFlowConn) RemoteAddr() net.Addr { return nil } +func (self *endedFlowConn) SetDeadline(time.Time) error { return nil } +func (self *endedFlowConn) SetReadDeadline(time.Time) error { return nil } +func (self *endedFlowConn) SetWriteDeadline(time.Time) error { return nil } From 9d053bfdb670844100d97352b26afedc86eb097f Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 13:21:24 -0400 Subject: [PATCH 45/73] Update deps and changelog - bumps external dependencies to the latest within their current major versions, leaving the openziti dependencies pinned per the LTS dependency policy - picks up bbolt v1.5.0, which drops a linear scan over all open read transactions from inside bbolt's global transaction mutex, lifting a ceiling on controller read throughput under high concurrency - notes the bbolt improvement in the 2.0.3 release notes - moves the #4139 l2 config types entry from 2.0.2 to 2.0.3, where it actually ships - adds the missing 2.0.3 entries for #4126 and #4063 --- CHANGELOG.md | 10 +- go.mod | 103 ++++++++++----------- go.sum | 236 +++++++++++++++++++++--------------------------- zititest/go.mod | 101 ++++++++++----------- zititest/go.sum | 233 +++++++++++++++++++++-------------------------- 5 files changed, 312 insertions(+), 371 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d203cc2b2..3e0ac2038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## What's New * Bug fixes +* Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear + scan over all open read transactions that ran while holding bbolt's single global transaction + mutex. Every controller read transaction takes that mutex twice, on open and on close, so the scan + cost grew with read concurrency and could put a controller serving a high rate of service-list and + policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks + fully busy while little work completes, and timeouts unexplained by the actual workload. ## Contributors @@ -24,6 +30,9 @@ Thanks to the community members who contributed to this release. * [Issue #4161](https://github.com/openziti/ziti/issues/4161) - [Backport-2.0] Leaderless controller strands terminator operations during cluster membership changes * [Issue #4146](https://github.com/openziti/ziti/issues/4146) - [Backport-2.0] Upgraded controller rejects legacy clients' existing sessions and gives no recovery signal for invalid service tokens * [Issue #4142](https://github.com/openziti/ziti/issues/4142) - [Backport-2.0] Service-policy enforcer deletes valid legacy sessions; type= queries use numeric id against the string-mapped symbol + * [Issue #4139](https://github.com/openziti/ziti/issues/4139) - [Backport-2.0] Add l2 service configuration types + * [Issue #4126](https://github.com/openziti/ziti/issues/4126) - [Backport-2.0] Legacy create-session signs service JWT with a mismatched session id after dedup + * [Issue #4063](https://github.com/openziti/ziti/issues/4063) - External JWT enrollment fails when a configured role attributes claims selector is absent from the JWT * [Issue #4052](https://github.com/openziti/ziti/issues/4052) - [Backport-2.0] The `ziti` CLI now refreshes an expired access token using the cached refresh token # Release 2.0.2 @@ -55,7 +64,6 @@ GitHub Security Advisories for full details, impact, and affected versions. * [Issue #4136](https://github.com/openziti/ziti/issues/4136) - [Backport-2.0] ziti tunnel ignores --dnsSvcIpRange * [Issue #4149](https://github.com/openziti/ziti/issues/4149) - [Backport-2.0] Upgrading a running 1.x controller/router to 2.x fails to create the service user * [Issue #4108](https://github.com/openziti/ziti/issues/4108) - Fix controller panic / potential data corruption by copying terminator peer data, instance secret, and eventual event data out of bolt-managed memory - * [Issue #4139](https://github.com/openziti/ziti/issues/4139) - [Backport-2.0] Add l2 service configuration types # Release 2.0.1 diff --git a/go.mod b/go.mod index 41e9df7b5..719d8a5d4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ replace github.com/michaelquigley/pfxlog => github.com/michaelquigley/pfxlog v0. require ( github.com/AppsFlyer/go-sundheit v0.6.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 github.com/Jeffail/gabs v1.4.0 github.com/Jeffail/gabs/v2 v2.7.0 @@ -24,18 +24,18 @@ require ( github.com/ef-ds/deque v1.0.4 github.com/fatih/color v1.19.0 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa - github.com/gaissmai/extnetip v1.3.1 + github.com/gaissmai/extnetip v1.3.2 github.com/go-acme/lego/v4 v4.35.2 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-openapi/errors v0.22.8 - github.com/go-openapi/jsonpointer v0.24.0 - github.com/go-openapi/loads v0.24.0 - github.com/go-openapi/runtime v0.32.4 - github.com/go-openapi/spec v0.22.6 - github.com/go-openapi/strfmt v0.26.4 - github.com/go-openapi/swag v0.27.0 - github.com/go-openapi/swag/jsonutils v0.27.0 - github.com/go-openapi/validate v0.26.0 + github.com/go-openapi/jsonpointer v1.0.0 + github.com/go-openapi/loads v0.25.1 + github.com/go-openapi/runtime v0.33.0 + github.com/go-openapi/spec v0.22.9 + github.com/go-openapi/strfmt v0.27.0 + github.com/go-openapi/swag v0.29.0 + github.com/go-openapi/swag/jsonutils v0.29.0 + github.com/go-openapi/validate v0.26.3 github.com/go-resty/resty/v2 v2.17.2 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -50,7 +50,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 - github.com/jedib0t/go-pretty/v6 v6.8.1 + github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/jellydator/ttlcache/v3 v3.4.1 github.com/jessevdk/go-flags v1.6.1 github.com/jinzhu/copier v0.4.0 @@ -59,7 +59,7 @@ require ( github.com/lucsky/cuid v1.2.1 github.com/mdlayher/netlink v1.11.2 github.com/michaelquigley/pfxlog v1.0.0 - github.com/miekg/dns v1.1.72 + github.com/miekg/dns v1.1.73 github.com/mitchellh/mapstructure v1.5.0 github.com/natefinch/lumberjack v2.0.0+incompatible github.com/openziti/agent v1.0.33 @@ -78,31 +78,31 @@ require ( github.com/openziti/xweb/v3 v3.0.4 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 - github.com/rabbitmq/amqp091-go v1.12.0 + github.com/rabbitmq/amqp091-go v1.14.0 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rodaine/table v1.3.1 github.com/russross/blackfriday v1.6.0 github.com/shirou/gopsutil/v3 v3.24.5 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 github.com/xeipuuv/gojsonschema v1.2.0 - github.com/zitadel/oidc/v3 v3.47.5 + github.com/zitadel/oidc/v3 v3.49.2 go.etcd.io/bbolt v1.5.0 go.uber.org/atomic v1.11.0 go4.org v0.0.0-20260112195520-a5071408f32f - golang.org/x/crypto v0.53.0 - golang.org/x/net v0.56.0 + golang.org/x/crypto v0.55.0 + golang.org/x/net v0.58.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.21.0 - golang.org/x/sys v0.46.0 - golang.org/x/term v0.44.0 - golang.org/x/text v0.38.0 - google.golang.org/protobuf v1.36.11 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/term v0.45.0 + golang.org/x/text v0.41.0 + google.golang.org/protobuf v1.36.12 gopkg.in/AlecAivazis/survey.v1 v1.8.8 gopkg.in/resty.v1 v1.12.0 gopkg.in/yaml.v2 v2.4.0 @@ -112,11 +112,11 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect - github.com/Azure/go-amqp v1.6.0 // indirect + github.com/Azure/go-amqp v1.7.0 // indirect github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect - github.com/antchfx/xpath v1.3.6 // indirect + github.com/antchfx/xpath v1.3.8 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/boltdb/bolt v1.3.1 // indirect @@ -125,33 +125,32 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creack/pty v1.1.11 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/go-chi/chi/v5 v5.2.5 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-chi/chi/v5 v5.3.1 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.2 // indirect - github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/analysis v0.26.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect - github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.27.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/loading v0.27.0 // indirect - github.com/go-openapi/swag/mangling v0.27.0 // indirect - github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.0 // indirect - github.com/go-openapi/swag/typeutils v0.27.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.0 // indirect + github.com/go-openapi/swag/cmdutils v0.29.0 // indirect + github.com/go-openapi/swag/conv v0.29.0 // indirect + github.com/go-openapi/swag/fileutils v0.29.0 // indirect + github.com/go-openapi/swag/loading v0.29.0 // indirect + github.com/go-openapi/swag/mangling v0.29.0 // indirect + github.com/go-openapi/swag/netutils v0.29.0 // indirect + github.com/go-openapi/swag/pools v0.29.0 // indirect + github.com/go-openapi/swag/stringutils v0.29.0 // indirect + github.com/go-openapi/swag/typeutils v0.29.0 // indirect + github.com/go-openapi/swag/yamlutils v0.29.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-metrics v0.6.1 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -159,25 +158,24 @@ require ( github.com/kr/pty v1.1.8 // indirect github.com/kyokomi/emoji/v2 v2.2.13 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect github.com/mattn/go-tty v0.0.8 // indirect - github.com/mdlayher/socket v0.6.0 // indirect + github.com/mdlayher/socket v0.6.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/openziti/go-term-markdown v1.0.1 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -194,17 +192,14 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zitadel/logging v0.7.0 // indirect github.com/zitadel/schema v1.3.2 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/tools v0.46.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect nhooyr.io/websocket v1.8.17 // indirect ) diff --git a/go.sum b/go.sum index 2ab3db781..236a73efc 100644 --- a/go.sum +++ b/go.sum @@ -41,16 +41,16 @@ github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliG github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk= github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 h1:kE5kpeiSqu4jcCQ/sWuyggMXJ/pT6oQ99+8hwPmyeJ0= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0/go.mod h1:IAN3Z0DMtehoxoQQnfqg1891z1P7GNoDryKtFcAyMBI= -github.com/Azure/go-amqp v1.6.0 h1:pMnBstxSd2JnvTopR/L9MUdQi4e5Mp9FscP4kZ0rZ8M= -github.com/Azure/go-amqp v1.6.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-amqp v1.7.0 h1:9VlH/LEWr386XWWJRNON0eslFqSClYBXP4HewvIqkDQ= +github.com/Azure/go-amqp v1.7.0/go.mod h1:pCJaHsvRlmmFUpxyQbh2qPkUFqYJeRBTqJSHKJadvPg= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -74,13 +74,13 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antchfx/jsonquery v1.3.7 h1:LUoue12xcCj6Q41kYUSAS0UJ+9s3XyxbP5uh7x8aMsw= github.com/antchfx/jsonquery v1.3.7/go.mod h1:oGh95SRUXZfnma1B7Q0p1rhgDeSgghub4W+JwnUYv2o= -github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/xpath v1.3.8 h1:RQlkLaJDKk1Ew1H6CUPUTKM+IQxm+6HTyOgcrfqOU9c= +github.com/antchfx/xpath v1.3.8/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -138,8 +138,6 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 h1:fctNkSsavbXpt8geFWZb8n+noCqS8MrOXRJ/YfdZ2dQ= @@ -164,8 +162,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -176,13 +174,13 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gaissmai/extnetip v1.3.1 h1:e8xa3hJG/+6NevjZXVkC60WBLypNjH/ugEy4kaPoKAU= -github.com/gaissmai/extnetip v1.3.1/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= +github.com/gaissmai/extnetip v1.3.2 h1:lNjdWx0pT6hXSfp4lc6jG1WPne0/PS2T8kRPYw26NsM= +github.com/gaissmai/extnetip v1.3.2/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU= github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -190,68 +188,66 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= -github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= +github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= -github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= -github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= -github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= -github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= -github.com/go-openapi/runtime v0.32.4 h1:8ElGj/3goG0itt0nBPP6Cm57ehcYyuHoI3O20nxgvkw= -github.com/go-openapi/runtime v0.32.4/go.mod h1:Bz6keOZw1NX4T6f+m42OoT1MBPDt6Re13dbccHyGH/4= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/loads v0.25.1 h1:toKQdIDLxlqfKLLGUUmUsiTd5/X0Chzvde9EGYQP/Ac= +github.com/go-openapi/loads v0.25.1/go.mod h1:33Hen4tsKXHL45TyYojvfD5fZUFN4O1y4r/XhsRW2zc= +github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= +github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= -github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= -github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= -github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= -github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= -github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= -github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= -github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= -github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= -github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= -github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= -github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= -github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= -github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= -github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= -github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= -github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= +github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/swag v0.29.0 h1:vJ8m3Xv7L4VfGgr8nK77CHTQRg2nyFPXySpV0w3xPB8= +github.com/go-openapi/swag v0.29.0/go.mod h1:8FrS8OFgntDRBzpHD7SyqDJTmTDPZo8Kvv0OuQW+Mr4= +github.com/go-openapi/swag/cmdutils v0.29.0 h1:AKt8Q7ZfR2NmnSJD7B1WcnTOfuXHBvLPJc3W65SCM3Y= +github.com/go-openapi/swag/cmdutils v0.29.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.29.0 h1:4+1TogWpOIzMPzVKrvx1BfqBYlApB7D7DW3EAWpwmp4= +github.com/go-openapi/swag/conv v0.29.0/go.mod h1:ch1l7V87F6zQXuLs5s0RFvrro6aFvrVcfVXn2PTZnu8= +github.com/go-openapi/swag/fileutils v0.29.0 h1:meobnn3MsAkF6XmJn6qw3hPeMAOhwf7XD5BOKeFzqWU= +github.com/go-openapi/swag/fileutils v0.29.0/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= +github.com/go-openapi/swag/jsonutils v0.29.0 h1:Xgnf9g32ycQjQUnDxkhqLraH2FhitcSE3w7ayQB3TgA= +github.com/go-openapi/swag/jsonutils v0.29.0/go.mod h1:5WYmjf6hJcBve+ArzBaUsYy4M1GXsgjIQTmwJKfZHrA= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0 h1:bpSF6LFkJJVtaRtJCzbZADVPVHQYKPwPdKthOQA2/5o= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= +github.com/go-openapi/swag/loading v0.29.0 h1:r1lg2DQbT1VgBwgiPYXBM059RNswFI6r36CC0QCcRGw= +github.com/go-openapi/swag/loading v0.29.0/go.mod h1:l/Z4MNbom0jSqzvWJqK2VUUWEceBknGEuVbLHLq4KN0= +github.com/go-openapi/swag/mangling v0.29.0 h1:RVKyucZ2rvA/M/sqxuNZGW8Mf0+1qypVX8n2GwV11lM= +github.com/go-openapi/swag/mangling v0.29.0/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= +github.com/go-openapi/swag/netutils v0.29.0 h1:2Y9tiqzdzRf++Tf5SGVK4Rk1iop2mo1+W3fNdcflMCo= +github.com/go-openapi/swag/netutils v0.29.0/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno= +github.com/go-openapi/swag/pools v0.29.0 h1:uMQcoJeHJ8fWkdfEXJZMMpqk6hpfW8qTL5Q/IoRFFII= +github.com/go-openapi/swag/pools v0.29.0/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= +github.com/go-openapi/swag/stringutils v0.29.0 h1:/IEOuZ7PGJi6lqgH83dVt7/A9eHsDGEH1459lm+gpEo= +github.com/go-openapi/swag/stringutils v0.29.0/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= +github.com/go-openapi/swag/typeutils v0.29.0 h1:HrWCYZeXVVNDo/7QQPRaYk33XeIDxksbxpalID3bWR8= +github.com/go-openapi/swag/typeutils v0.29.0/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.29.0 h1:JOKKuhMnBx4HYTM+kPEYw8S5YKKU9PnC4Mwb+c69BBA= +github.com/go-openapi/swag/yamlutils v0.29.0/go.mod h1:/+FVozjFWZzku6mRz5U/Qmq5Yk8PLFxBLLWA/jHaxYE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= +github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= +github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= +github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -358,8 +354,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= -github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-metrics v0.6.1 h1:V7j9cgHTXl4OebnX3y0l6ZSoO5dTp0VI0K8Y7JfRsS4= +github.com/hashicorp/go-metrics v0.6.1/go.mod h1:XOozbQKeJz12GG8cCcQb/E5vNVeTdJt0aHbM+uHnL1M= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -400,8 +396,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= @@ -412,10 +408,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -423,7 +417,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/judedaryl/go-arrayutils v0.0.1 h1:89rWXRVp1c1gcE1UEWvFuohVMeYwfA0y4TMZtE8dS58= github.com/judedaryl/go-arrayutils v0.0.1/go.mod h1:vqtnlEkOBpDGHS3U3kQtMJZGTOC+SBFAQYj2KcxLf1A= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -431,7 +424,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -461,8 +453,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -470,29 +462,29 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/michaelquigley/pfxlog v0.6.10 h1:IbC/H3MmSDcPlQHF1UZPQU13Dkrs0+ycWRyQd2ihnjw= github.com/michaelquigley/pfxlog v0.6.10/go.mod h1:gEiNTfKEX6cJHSwRpOuqBpc8oYrlhMiDK/xMk/gV7D0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -516,15 +508,14 @@ github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= @@ -574,8 +565,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -592,31 +583,23 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI= -github.com/rabbitmq/amqp091-go v1.12.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/amqp091-go v1.14.0 h1:RSaT7aOKt/OrkVUyswPDW29lnRz9psuGmfZFBmLqLek= +github.com/rabbitmq/amqp091-go v1.14.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -646,10 +629,9 @@ github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsB github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -691,8 +673,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -724,10 +706,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= -github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= -github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4= -github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY= +github.com/zitadel/oidc/v3 v3.49.2 h1:yKvB2Hx6rVWp0vOTk1pEyXAPxJusgIHNmr20AxOohmQ= +github.com/zitadel/oidc/v3 v3.49.2/go.mod h1:HwoguOGo0eem0RK5Gb+P6Q4aQLVinJ9LhomlVEA57ck= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= @@ -761,8 +741,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -776,8 +757,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -788,8 +769,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -816,8 +797,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -862,8 +841,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -890,8 +869,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -918,7 +897,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -932,8 +910,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -945,7 +921,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -953,7 +928,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -961,13 +935,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -977,8 +951,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1037,8 +1011,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1145,8 +1117,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/zititest/go.mod b/zititest/go.mod index dbd8456a0..5797fc0c1 100644 --- a/zititest/go.mod +++ b/zititest/go.mod @@ -13,7 +13,7 @@ replace github.com/michaelquigley/pfxlog => github.com/michaelquigley/pfxlog v0. require ( github.com/Jeffail/gabs v1.4.0 github.com/Jeffail/gabs/v2 v2.7.0 - github.com/go-openapi/runtime v0.32.4 + github.com/go-openapi/runtime v0.33.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/michaelquigley/pfxlog v1.0.0 @@ -30,22 +30,22 @@ require ( github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.10.1 github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 go.etcd.io/bbolt v1.5.0 - golang.org/x/net v0.56.0 - google.golang.org/protobuf v1.36.11 + golang.org/x/net v0.58.0 + google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/AppsFlyer/go-sundheit v0.6.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 // indirect - github.com/Azure/go-amqp v1.6.0 // indirect + github.com/Azure/go-amqp v1.7.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect @@ -89,7 +89,6 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 // indirect github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect @@ -98,37 +97,37 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.19.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect - github.com/gaissmai/extnetip v1.3.1 // indirect + github.com/gaissmai/extnetip v1.3.2 // indirect github.com/go-acme/lego/v4 v4.35.2 // indirect - github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-chi/chi/v5 v5.3.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.2 // indirect + github.com/go-openapi/analysis v0.26.0 // indirect github.com/go-openapi/errors v0.22.8 // indirect - github.com/go-openapi/jsonpointer v0.24.0 // indirect - github.com/go-openapi/jsonreference v0.21.6 // indirect - github.com/go-openapi/loads v0.24.0 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/loads v0.25.1 // indirect github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect - github.com/go-openapi/spec v0.22.6 // indirect - github.com/go-openapi/strfmt v0.26.4 // indirect - github.com/go-openapi/swag v0.27.0 // indirect - github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.27.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/jsonutils v0.27.0 // indirect - github.com/go-openapi/swag/loading v0.27.0 // indirect - github.com/go-openapi/swag/mangling v0.27.0 // indirect - github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.0 // indirect - github.com/go-openapi/swag/typeutils v0.27.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.0 // indirect - github.com/go-openapi/validate v0.26.0 // indirect + github.com/go-openapi/spec v0.22.9 // indirect + github.com/go-openapi/strfmt v0.27.0 // indirect + github.com/go-openapi/swag v0.29.0 // indirect + github.com/go-openapi/swag/cmdutils v0.29.0 // indirect + github.com/go-openapi/swag/conv v0.29.0 // indirect + github.com/go-openapi/swag/fileutils v0.29.0 // indirect + github.com/go-openapi/swag/jsonutils v0.29.0 // indirect + github.com/go-openapi/swag/loading v0.29.0 // indirect + github.com/go-openapi/swag/mangling v0.29.0 // indirect + github.com/go-openapi/swag/netutils v0.29.0 // indirect + github.com/go-openapi/swag/pools v0.29.0 // indirect + github.com/go-openapi/swag/stringutils v0.29.0 // indirect + github.com/go-openapi/swag/typeutils v0.29.0 // indirect + github.com/go-openapi/swag/yamlutils v0.29.0 // indirect + github.com/go-openapi/validate v0.26.3 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect @@ -139,7 +138,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-metrics v0.6.1 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -149,7 +148,7 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect - github.com/jedib0t/go-pretty/v6 v6.8.1 // indirect + github.com/jedib0t/go-pretty/v6 v6.8.3 // indirect github.com/jellydator/ttlcache/v3 v3.4.1 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jinzhu/copier v0.4.0 // indirect @@ -161,16 +160,16 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucsky/cuid v1.2.1 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-runewidth v0.0.28 // indirect github.com/mattn/go-tty v0.0.8 // indirect github.com/mdlayher/netlink v1.11.2 // indirect - github.com/mdlayher/socket v0.6.0 // indirect + github.com/mdlayher/socket v0.6.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/michaelquigley/figlet v0.1.0 // indirect - github.com/miekg/dns v1.1.72 // indirect + github.com/miekg/dns v1.1.73 // indirect github.com/miekg/pkcs11 v1.1.2 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -182,7 +181,7 @@ require ( github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/oliveagle/jsonpath v0.1.4 // indirect github.com/openziti/cobra-to-md v1.0.1 // indirect github.com/openziti/go-term-markdown v1.0.1 // indirect @@ -192,15 +191,14 @@ require ( github.com/openziti/x509-claims v1.0.3 // indirect github.com/openziti/xweb/v3 v3.0.4 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pkg/sftp v1.13.10 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/rabbitmq/amqp091-go v1.12.0 // indirect + github.com/rabbitmq/amqp091-go v1.14.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rodaine/table v1.3.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -226,8 +224,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zitadel/logging v0.7.0 // indirect - github.com/zitadel/oidc/v3 v3.47.5 // indirect + github.com/zitadel/oidc/v3 v3.49.2 // indirect github.com/zitadel/schema v1.3.2 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -235,17 +232,15 @@ require ( go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.46.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect gopkg.in/AlecAivazis/survey.v1 v1.8.8 // indirect gopkg.in/resty.v1 v1.12.0 // indirect nhooyr.io/websocket v1.8.17 // indirect diff --git a/zititest/go.sum b/zititest/go.sum index ad47cd8ea..7f6d6281d 100644 --- a/zititest/go.sum +++ b/zititest/go.sum @@ -41,16 +41,16 @@ github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliG github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk= github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 h1:kE5kpeiSqu4jcCQ/sWuyggMXJ/pT6oQ99+8hwPmyeJ0= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0/go.mod h1:IAN3Z0DMtehoxoQQnfqg1891z1P7GNoDryKtFcAyMBI= -github.com/Azure/go-amqp v1.6.0 h1:pMnBstxSd2JnvTopR/L9MUdQi4e5Mp9FscP4kZ0rZ8M= -github.com/Azure/go-amqp v1.6.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-amqp v1.7.0 h1:9VlH/LEWr386XWWJRNON0eslFqSClYBXP4HewvIqkDQ= +github.com/Azure/go-amqp v1.7.0/go.mod h1:pCJaHsvRlmmFUpxyQbh2qPkUFqYJeRBTqJSHKJadvPg= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -75,7 +75,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -187,8 +186,6 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= github.com/dineshappavoo/basex v0.0.0-20170425072625-481a6f6dc663 h1:fctNkSsavbXpt8geFWZb8n+noCqS8MrOXRJ/YfdZ2dQ= @@ -215,8 +212,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -227,13 +224,13 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gaissmai/extnetip v1.3.1 h1:e8xa3hJG/+6NevjZXVkC60WBLypNjH/ugEy4kaPoKAU= -github.com/gaissmai/extnetip v1.3.1/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= +github.com/gaissmai/extnetip v1.3.2 h1:lNjdWx0pT6hXSfp4lc6jG1WPne0/PS2T8kRPYw26NsM= +github.com/gaissmai/extnetip v1.3.2/go.mod h1:PepswGWH0GJ4XSdiY7bY2EaEPpCQs7OaYcySeW525fg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU= github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -241,68 +238,66 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= -github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= +github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= -github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= -github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= -github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= -github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= -github.com/go-openapi/runtime v0.32.4 h1:8ElGj/3goG0itt0nBPP6Cm57ehcYyuHoI3O20nxgvkw= -github.com/go-openapi/runtime v0.32.4/go.mod h1:Bz6keOZw1NX4T6f+m42OoT1MBPDt6Re13dbccHyGH/4= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/loads v0.25.1 h1:toKQdIDLxlqfKLLGUUmUsiTd5/X0Chzvde9EGYQP/Ac= +github.com/go-openapi/loads v0.25.1/go.mod h1:33Hen4tsKXHL45TyYojvfD5fZUFN4O1y4r/XhsRW2zc= +github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= +github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= -github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= -github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= -github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= -github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= -github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= -github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= -github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= -github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= -github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= -github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= -github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= -github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= -github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= -github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= -github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= -github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= +github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= +github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= +github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc= +github.com/go-openapi/swag v0.29.0 h1:vJ8m3Xv7L4VfGgr8nK77CHTQRg2nyFPXySpV0w3xPB8= +github.com/go-openapi/swag v0.29.0/go.mod h1:8FrS8OFgntDRBzpHD7SyqDJTmTDPZo8Kvv0OuQW+Mr4= +github.com/go-openapi/swag/cmdutils v0.29.0 h1:AKt8Q7ZfR2NmnSJD7B1WcnTOfuXHBvLPJc3W65SCM3Y= +github.com/go-openapi/swag/cmdutils v0.29.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.29.0 h1:4+1TogWpOIzMPzVKrvx1BfqBYlApB7D7DW3EAWpwmp4= +github.com/go-openapi/swag/conv v0.29.0/go.mod h1:ch1l7V87F6zQXuLs5s0RFvrro6aFvrVcfVXn2PTZnu8= +github.com/go-openapi/swag/fileutils v0.29.0 h1:meobnn3MsAkF6XmJn6qw3hPeMAOhwf7XD5BOKeFzqWU= +github.com/go-openapi/swag/fileutils v0.29.0/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI= +github.com/go-openapi/swag/jsonutils v0.29.0 h1:Xgnf9g32ycQjQUnDxkhqLraH2FhitcSE3w7ayQB3TgA= +github.com/go-openapi/swag/jsonutils v0.29.0/go.mod h1:5WYmjf6hJcBve+ArzBaUsYy4M1GXsgjIQTmwJKfZHrA= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0 h1:bpSF6LFkJJVtaRtJCzbZADVPVHQYKPwPdKthOQA2/5o= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.0/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg= +github.com/go-openapi/swag/loading v0.29.0 h1:r1lg2DQbT1VgBwgiPYXBM059RNswFI6r36CC0QCcRGw= +github.com/go-openapi/swag/loading v0.29.0/go.mod h1:l/Z4MNbom0jSqzvWJqK2VUUWEceBknGEuVbLHLq4KN0= +github.com/go-openapi/swag/mangling v0.29.0 h1:RVKyucZ2rvA/M/sqxuNZGW8Mf0+1qypVX8n2GwV11lM= +github.com/go-openapi/swag/mangling v0.29.0/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s= +github.com/go-openapi/swag/netutils v0.29.0 h1:2Y9tiqzdzRf++Tf5SGVK4Rk1iop2mo1+W3fNdcflMCo= +github.com/go-openapi/swag/netutils v0.29.0/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno= +github.com/go-openapi/swag/pools v0.29.0 h1:uMQcoJeHJ8fWkdfEXJZMMpqk6hpfW8qTL5Q/IoRFFII= +github.com/go-openapi/swag/pools v0.29.0/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc= +github.com/go-openapi/swag/stringutils v0.29.0 h1:/IEOuZ7PGJi6lqgH83dVt7/A9eHsDGEH1459lm+gpEo= +github.com/go-openapi/swag/stringutils v0.29.0/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc= +github.com/go-openapi/swag/typeutils v0.29.0 h1:HrWCYZeXVVNDo/7QQPRaYk33XeIDxksbxpalID3bWR8= +github.com/go-openapi/swag/typeutils v0.29.0/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.29.0 h1:JOKKuhMnBx4HYTM+kPEYw8S5YKKU9PnC4Mwb+c69BBA= +github.com/go-openapi/swag/yamlutils v0.29.0/go.mod h1:/+FVozjFWZzku6mRz5U/Qmq5Yk8PLFxBLLWA/jHaxYE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= +github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= +github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= +github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -404,8 +399,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= -github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-metrics v0.6.1 h1:V7j9cgHTXl4OebnX3y0l6ZSoO5dTp0VI0K8Y7JfRsS4= +github.com/hashicorp/go-metrics v0.6.1/go.mod h1:XOozbQKeJz12GG8cCcQb/E5vNVeTdJt0aHbM+uHnL1M= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -452,8 +447,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= -github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= @@ -464,10 +459,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -476,7 +469,6 @@ github.com/judedaryl/go-arrayutils v0.0.1 h1:89rWXRVp1c1gcE1UEWvFuohVMeYwfA0y4TM github.com/judedaryl/go-arrayutils v0.0.1/go.mod h1:vqtnlEkOBpDGHS3U3kQtMJZGTOC+SBFAQYj2KcxLf1A= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -484,7 +476,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -517,8 +508,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -526,23 +517,23 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= +github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -551,8 +542,8 @@ github.com/michaelquigley/figlet v0.1.0/go.mod h1:o01j/eykadr5SKE51qSYuvQ1n9Aftz github.com/michaelquigley/pfxlog v0.6.10 h1:IbC/H3MmSDcPlQHF1UZPQU13Dkrs0+ycWRyQd2ihnjw= github.com/michaelquigley/pfxlog v0.6.10/go.mod h1:gEiNTfKEX6cJHSwRpOuqBpc8oYrlhMiDK/xMk/gV7D0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -582,7 +573,6 @@ github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce h1:TqjP/BTDrwN7zP9xyXVuLsMBXYMt6LLYi55PlrIcq8U= @@ -593,8 +583,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oliveagle/jsonpath v0.1.4 h1:Sr/ffH5YSyQKjSNfvDFkQqAqh3kn/QxF/7j2jjpfOAI= github.com/oliveagle/jsonpath v0.1.4/go.mod h1:diWEHhuLqib29heQcHYHyaLcxFC3KpKa/5ihkZBs1Z8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -648,8 +638,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= @@ -668,31 +658,23 @@ github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1Hbe github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI= -github.com/rabbitmq/amqp091-go v1.12.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/amqp091-go v1.14.0 h1:RSaT7aOKt/OrkVUyswPDW29lnRz9psuGmfZFBmLqLek= +github.com/rabbitmq/amqp091-go v1.14.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -724,10 +706,9 @@ github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsB github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -765,8 +746,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -800,10 +781,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= -github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= -github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4= -github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY= +github.com/zitadel/oidc/v3 v3.49.2 h1:yKvB2Hx6rVWp0vOTk1pEyXAPxJusgIHNmr20AxOohmQ= +github.com/zitadel/oidc/v3 v3.49.2/go.mod h1:HwoguOGo0eem0RK5Gb+P6Q4aQLVinJ9LhomlVEA57ck= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= @@ -837,8 +816,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -852,8 +832,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -864,8 +844,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -892,8 +872,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -938,8 +916,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -966,8 +944,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -994,7 +972,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1008,8 +985,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1021,7 +996,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1029,7 +1003,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1038,13 +1011,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1054,8 +1027,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1114,8 +1087,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1222,8 +1193,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From 4030840859d75e639331604b6896ca22b3c7a781 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 16:55:01 -0400 Subject: [PATCH 46/73] Move the sdk info wait onto ManagementHelperClient - makes the sdk/env info poll a RequireIdentitySdkInfoUpdated method on ManagementHelperClient, alongside the client's other helpers, rather than a package-level function in the test file - drops the context and helper parameters, since the helper already carries the test context it needs to assert through --- tests/api_client_management.go | 15 +++++++++++++++ tests/auth_oidc_test.go | 19 ++----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/api_client_management.go b/tests/api_client_management.go index a7e151bc7..0ee473d8e 100644 --- a/tests/api_client_management.go +++ b/tests/api_client_management.go @@ -121,6 +121,21 @@ func (helper *ManagementHelperClient) GetIdentity(identityId string) (*rest_mode return resp.Payload.Data, nil } +// RequireIdentitySdkInfoUpdated polls until the identity reports appId as its SDK app id, returning the +// identity as read and failing the test if it does not appear within 10 seconds. The controller applies +// sdk/env info updates on a background queue, so they are not guaranteed to be visible by the time +// authentication returns. +func (helper *ManagementHelperClient) RequireIdentitySdkInfoUpdated(identityId string, appId string) *rest_model.IdentityDetail { + var identityDetail *rest_model.IdentityDetail + helper.testCtx.Req.Eventually(func() bool { + var err error + identityDetail, err = helper.GetIdentity(identityId) + return err == nil && identityDetail.SdkInfo != nil && identityDetail.SdkInfo.AppID == appId + }, 10*time.Second, 50*time.Millisecond, "identity %s sdk info was not updated", identityId) + + return identityDetail +} + func (helper *ManagementHelperClient) CreateEnrollmentOtt(identityId *string, expiresAt *time.Time) (*rest_model.CreateLocation, error) { var expAt *strfmt.DateTime diff --git a/tests/auth_oidc_test.go b/tests/auth_oidc_test.go index db539e299..e87ba4ca4 100644 --- a/tests/auth_oidc_test.go +++ b/tests/auth_oidc_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/golang-jwt/jwt/v5" @@ -202,7 +201,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - identityDetail := requireIdentitySdkInfoUpdated(ctx, managementHelper, accessClaims.Subject, payload.SdkInfo.AppID) + identityDetail := managementHelper.RequireIdentitySdkInfoUpdated(accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) @@ -317,7 +316,7 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { t.Run("has the correct sdk and env info", func(t *testing.T) { ctx.testContextChanged(t) - identityDetail := requireIdentitySdkInfoUpdated(ctx, managementHelper, accessClaims.Subject, payload.SdkInfo.AppID) + identityDetail := managementHelper.RequireIdentitySdkInfoUpdated(accessClaims.Subject, payload.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppID, identityDetail.SdkInfo.AppID) ctx.Req.Equal(payload.SdkInfo.AppVersion, identityDetail.SdkInfo.AppVersion) @@ -694,17 +693,3 @@ func Test_Authenticate_OIDC_Auth(t *testing.T) { }) } - -// requireIdentitySdkInfoUpdated polls the management API until the identity reports the given SDK app id and -// returns the identity as read, failing the test if it doesn't appear within 10 seconds. The controller may apply -// sdk/env info updates on a background queue, so they are not guaranteed to be visible when authentication returns. -func requireIdentitySdkInfoUpdated(ctx *TestContext, managementHelper *ManagementHelperClient, identityId string, appId string) *rest_model.IdentityDetail { - var identityDetail *rest_model.IdentityDetail - ctx.Req.Eventually(func() bool { - var err error - identityDetail, err = managementHelper.GetIdentity(identityId) - return err == nil && identityDetail.SdkInfo != nil && identityDetail.SdkInfo.AppID == appId - }, 10*time.Second, 50*time.Millisecond, "identity %s sdk info was not updated", identityId) - - return identityDetail -} From 703f74ae28ed5390c36b243a1661c46cc6cb5e4e Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Thu, 23 Jul 2026 10:22:23 -0400 Subject: [PATCH 47/73] backport fixes GHSA-q8g9-jc4c-jp6q to v2.0 limit pre-auth request body buffering - caps buffered HTTP request bodies at 1 MiB across the client, management, fabric management, and OIDC web APIs - rejects oversized bodies with 413 REQUEST_ENTITY_TOO_LARGE before authentication - surfaces request body read errors instead of ignoring them - returns after writing the request context error response in the client API handler - adds integration coverage for oversized bodies with and without Content-Length --- controller/api/body_limit.go | 23 +++++ controller/apierror/helpers.go | 10 +++ controller/apierror/messages.go | 4 + controller/env/appenv.go | 23 ++++- controller/env/context.go | 5 ++ controller/oidc_auth/provider.go | 11 +++ controller/oidc_auth/render.go | 7 ++ controller/webapis/client-api.go | 4 +- controller/webapis/fabric-management-api.go | 4 +- controller/webapis/management-api.go | 3 +- tests/request_body_limit_test.go | 98 +++++++++++++++++++++ 11 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 controller/api/body_limit.go create mode 100644 tests/request_body_limit_test.go diff --git a/controller/api/body_limit.go b/controller/api/body_limit.go new file mode 100644 index 000000000..acbb2fc67 --- /dev/null +++ b/controller/api/body_limit.go @@ -0,0 +1,23 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package api + +// MaxRequestBodySize is the maximum number of HTTP request body bytes the controller's +// web APIs accept per request. Request bodies are buffered into memory before +// authentication, so this cap bounds the memory an unauthenticated client can consume. +// Requests with larger bodies are rejected with HTTP 413 Request Entity Too Large. +const MaxRequestBodySize = 1024 * 1024 diff --git a/controller/apierror/helpers.go b/controller/apierror/helpers.go index d1e25747f..784683a82 100644 --- a/controller/apierror/helpers.go +++ b/controller/apierror/helpers.go @@ -49,6 +49,16 @@ func NewCouldNotReadBody(err error) *errorz.ApiError { } } +// NewRequestEntityTooLarge returns the 413 ApiError raised when a request body exceeds the +// maximum size the web APIs accept. +func NewRequestEntityTooLarge() *errorz.ApiError { + return &errorz.ApiError{ + AppCode: RequestEntityTooLargeCode, + Message: RequestEntityTooLargeMessage, + Status: RequestEntityTooLargeStatus, + } +} + func NewInvalidAuth() *errorz.ApiError { return &errorz.ApiError{ AppCode: InvalidAuthCode, diff --git a/controller/apierror/messages.go b/controller/apierror/messages.go index 143592547..ec9be1eb5 100644 --- a/controller/apierror/messages.go +++ b/controller/apierror/messages.go @@ -31,6 +31,10 @@ const ( CouldNotReadBodyMessage string = "The body of the request could not be read" CouldNotReadBodyStatus int = http.StatusInternalServerError + RequestEntityTooLargeCode string = "REQUEST_ENTITY_TOO_LARGE" + RequestEntityTooLargeMessage string = "The request body exceeds the maximum accepted size" + RequestEntityTooLargeStatus int = http.StatusRequestEntityTooLarge + InvalidUuidCode string = "INVALID_UUID" InvalidUuidMessage string = "The supplied UUID is invalid" InvalidUuidStatus int = http.StatusBadRequest diff --git a/controller/env/appenv.go b/controller/env/appenv.go index 80a68b12f..2b158a025 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -50,13 +50,13 @@ import ( "github.com/openziti/identity" "github.com/openziti/metrics" "github.com/openziti/sdk-golang/ziti" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/cert" "github.com/openziti/ziti/v2/common/eid" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/openziti/ziti/v2/controller/api" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/config" "github.com/openziti/ziti/v2/controller/db" @@ -64,6 +64,7 @@ import ( "github.com/openziti/ziti/v2/controller/events" "github.com/openziti/ziti/v2/controller/jwtsigner" "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/network" "github.com/openziti/ziti/v2/controller/permissions" @@ -881,11 +882,27 @@ func (ae *AppEnv) GetControllerPublicKey(kid string) crypto.PublicKey { return signers[kid] } -// CreateRequestContext creates a new request context for handling HTTP requests. +// CreateRequestContext creates a new request context for handling HTTP requests. The request body +// is buffered into memory before any authentication check, so bodies larger than +// api.MaxRequestBodySize are rejected with a 413 ApiError instead of being buffered. func (ae *AppEnv) CreateRequestContext(rw http.ResponseWriter, r *http.Request) (*response.RequestContext, error) { rid := eid.New() - body, _ := io.ReadAll(r.Body) + if r.ContentLength > api.MaxRequestBodySize { + return nil, apierror.NewRequestEntityTooLarge() + } + + r.Body = http.MaxBytesReader(rw, r.Body, api.MaxRequestBodySize) + body, err := io.ReadAll(r.Body) + + if err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + return nil, apierror.NewRequestEntityTooLarge() + } + return nil, apierror.NewCouldNotReadBody(err) + } + r.Body = io.NopCloser(bytes.NewReader(body)) securityTokenCtx, err := common.NewSecurityTokenCtx(r, ae.TokenIssuerCache) diff --git a/controller/env/context.go b/controller/env/context.go index 6ee8ec041..a0b6157d4 100644 --- a/controller/env/context.go +++ b/controller/env/context.go @@ -6,12 +6,17 @@ import ( "net/http" "github.com/openziti/ziti/v2/common/eid" + "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/response" ) +// NewRequestContext creates a bare request context for responses rendered outside the normal +// request pipeline. The body read is capped at api.MaxRequestBodySize; a body that exceeds the +// cap is truncated rather than rejected, since this context only renders error responses. func NewRequestContext(rw http.ResponseWriter, r *http.Request) *response.RequestContext { rid := eid.New() + r.Body = http.MaxBytesReader(rw, r.Body, api.MaxRequestBodySize) body, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewReader(body)) diff --git a/controller/oidc_auth/provider.go b/controller/oidc_auth/provider.go index 8494df99c..3f4c53634 100644 --- a/controller/oidc_auth/provider.go +++ b/controller/oidc_auth/provider.go @@ -24,6 +24,8 @@ import ( "github.com/gorilla/mux" "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/controller/api" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/model" "github.com/pkg/errors" @@ -99,6 +101,15 @@ func NewNativeOnlyOP(ctx context.Context, env model.Env, config Config) (http.Ha } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // OIDC endpoints buffer request bodies before authentication, so cap what an + // unauthenticated client can make the controller hold in memory + if r.ContentLength > api.MaxRequestBodySize { + renderJsonApiError(w, apierror.NewRequestEntityTooLarge()) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, api.MaxRequestBodySize) + for iss, handler := range handlers { if err := iss.ValidFor(r.Host); err == nil { handler.ServeHTTP(w, r) diff --git a/controller/oidc_auth/render.go b/controller/oidc_auth/render.go index ea0e707f1..c1a2a0444 100644 --- a/controller/oidc_auth/render.go +++ b/controller/oidc_auth/render.go @@ -24,6 +24,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/edge-api/rest_model" "github.com/openziti/foundation/v2/errorz" + "github.com/openziti/ziti/v2/controller/apierror" ) // render will attempt to send a responses on the provided http.ResponseWriter. All error output will be directed to the @@ -93,7 +94,13 @@ func renderJsonApiError(w http.ResponseWriter, err *errorz.ApiError) { func errorToRestApiError(err error) (*rest_model.APIError, int) { var typedErr *errorz.ApiError + var maxBytesErr *http.MaxBytesError switch { + case errors.As(err, &maxBytesErr): + return &rest_model.APIError{ + Code: apierror.RequestEntityTooLargeCode, + Message: apierror.RequestEntityTooLargeMessage, + }, http.StatusRequestEntityTooLarge case errors.As(err, &typedErr): restErr := &rest_model.APIError{ Code: typedErr.AppCode, diff --git a/controller/webapis/client-api.go b/controller/webapis/client-api.go index 9e8c1501a..0087de72e 100644 --- a/controller/webapis/client-api.go +++ b/controller/webapis/client-api.go @@ -26,7 +26,6 @@ import ( "github.com/openziti/edge-api/rest_client_api_client" "github.com/openziti/edge-api/rest_client_api_server" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/foundation/v2/errorz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/apierror" @@ -195,7 +194,8 @@ func (clientApi ClientApiHandler) newHandler(ae *env.AppEnv) http.Handler { rc, err := ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) + return } api.AddRequestContextToHttpContext(r, rc) diff --git a/controller/webapis/fabric-management-api.go b/controller/webapis/fabric-management-api.go index 2040aabb2..9310bd05a 100644 --- a/controller/webapis/fabric-management-api.go +++ b/controller/webapis/fabric-management-api.go @@ -187,7 +187,7 @@ func (self *FabricManagementApiHandler) WrapHttpHandler(handler http.Handler) ht rc, err := self.ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } @@ -208,7 +208,7 @@ func (self *FabricManagementApiHandler) WrapWsHandler(handler http.Handler) http rc, err := self.ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } diff --git a/controller/webapis/management-api.go b/controller/webapis/management-api.go index 6f550c1aa..96772c3c8 100644 --- a/controller/webapis/management-api.go +++ b/controller/webapis/management-api.go @@ -24,7 +24,6 @@ import ( "github.com/openziti/edge-api/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/foundation/v2/errorz" "github.com/openziti/xweb/v3" "github.com/openziti/ziti/v2/controller/api" "github.com/openziti/ziti/v2/controller/apierror" @@ -138,7 +137,7 @@ func (managementApi ManagementApiHandler) newHandler(ae *env.AppEnv) http.Handle rc, err := ae.CreateRequestContext(rw, r) if err != nil { - env.WriteHttpApiError(rw, errorz.NewUnhandled(err)) + env.WriteHttpError(rw, err) return } diff --git a/tests/request_body_limit_test.go b/tests/request_body_limit_test.go new file mode 100644 index 000000000..e65e3a4e9 --- /dev/null +++ b/tests/request_body_limit_test.go @@ -0,0 +1,98 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "bytes" + "net/http" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/openziti/ziti/v2/controller/api" +) + +// Test_RequestBodyLimit locks in the pre-auth request body cap. The controller web APIs +// buffer each request body into memory before any authentication check, so bodies larger +// than api.MaxRequestBodySize must be rejected with 413 instead of being buffered. +func Test_RequestBodyLimit(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + oversized := bytes.Repeat([]byte("a"), api.MaxRequestBodySize+1) + + t.Run("oversized body to the unauthenticated client API enroll endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousClientApiRequest().SetBody(oversized).Post("/enroll") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("oversized body to the unauthenticated management API authenticate endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousManagementApiRequest().SetBody(oversized).Post("/authenticate?method=password") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("oversized chunked body without a Content-Length header returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + // a reader body makes resty send chunked transfer encoding, so the server cannot + // reject on the Content-Length header and must stop reading at the cap instead + resp, err := ctx.newAnonymousClientApiRequest().SetBody(bytes.NewReader(oversized)).Post("/enroll") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized chunked body must be rejected once the cap is hit, got body: %s", resp.String()) + }) + + t.Run("oversized body to the unauthenticated OIDC login endpoint returns 413", func(t *testing.T) { + ctx.testContextChanged(t) + + client := resty.NewWithClient(ctx.NewHttpClient(ctx.NewTransport())) + + resp, err := client.R(). + SetHeader("content-type", "application/json"). + SetBody(oversized). + Post("https://" + ctx.ApiHost + "/oidc/login/username") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusRequestEntityTooLarge, resp.StatusCode(), + "an oversized body must be rejected before it is buffered, got body: %s", resp.String()) + }) + + t.Run("normal-size body still reaches the API handlers", func(t *testing.T) { + ctx.testContextChanged(t) + + resp, err := ctx.newAnonymousManagementApiRequest(). + SetBody(`{"username":"bogus","password":"bogus"}`). + Post("/authenticate?method=password") + + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusUnauthorized, resp.StatusCode(), + "a small body must pass the cap and reach normal authentication handling") + }) +} From 1889123bbc6924a2d3a518223efd33789a7e2d62 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 17:46:02 -0400 Subject: [PATCH 48/73] Add consolidated 2.0.3 security advisory notes to the changelog --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0ac2038..b99ea3b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes * Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear scan over all open read transactions that ran while holding bbolt's single global transaction @@ -10,6 +11,44 @@ policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks fully busy while little work completes, and timeouts unexplained by the actual workload. +## Security Advisories + +This release addresses eight security advisories. See the linked GitHub Security Advisories for full +details, impact, and affected versions. + +* [GHSA-q8g9-jc4c-jp6q](https://github.com/openziti/ziti/security/advisories/GHSA-q8g9-jc4c-jp6q) (CVE pending) (High) - The controller buffered the entire body of every + inbound request before any authentication check and with no size cap, so an unauthenticated client could + exhaust controller memory, and crash it, by sending parallel large-body requests to endpoints such as + enrollment. +* [GHSA-j952-6x8x-jmj6](https://github.com/openziti/ziti/security/advisories/GHSA-j952-6x8x-jmj6) (CVE pending) (High) - The unauthenticated legacy enrollment path buffered + the request body a second time, allocating twice the memory per request and roughly halving the bandwidth + needed to drive the controller out of memory. Amplifies GHSA-q8g9-jc4c-jp6q. +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via CreateCircuitV3, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. +* [GHSA-4h58-w989-xgg4](https://github.com/openziti/ziti/security/advisories/GHSA-4h58-w989-xgg4) (CVE pending) (Medium) - The token-based enrollment endpoint skipped + audience and issuer validation when the request carried a `ziti-token-issuer-id` header, so an attacker + holding any unexpired JWT signed by a configured external JWT signer, even one minted for a different + audience, could enroll a new identity onto the network. +* [GHSA-6v5r-p2wr-q492](https://github.com/openziti/ziti/security/advisories/GHSA-6v5r-p2wr-q492) (CVE pending) (Medium) - The current-api-session certificates endpoint + performed an unscoped list, so any authenticated user could read the API session certificates (subject + DNs, fingerprints, and full PEM chains) of all identities, not just their own. +* [GHSA-whjr-3j94-gw3c](https://github.com/openziti/ziti/security/advisories/GHSA-whjr-3j94-gw3c) (CVE pending) (Medium) - A JWKS endpoint URL configured on an external JWT + signer was fetched server-side with no timeout, private-range blocking, or allowlist, letting a caller with + external-jwt-signer management access make the controller issue requests to arbitrary internal URLs, + including cloud metadata endpoints (SSRF). +* [GHSA-354c-gpg9-j988](https://github.com/openziti/ziti/security/advisories/GHSA-354c-gpg9-j988) (CVE pending) (Low) - With promptOnWake or promptOnUnlock enabled on an MFA + posture check, the edge router dereferenced a nil wake/unlock timestamp while locally evaluating an + authorized client's dial or bind, panicking and crashing the router (data-plane denial of service). + ## Contributors Thanks to the community members who contributed to this release. From 0eb555f6c794e56cfe19ebc6957ba6630a78bfac Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 10 Aug 2026 15:46:13 -0400 Subject: [PATCH 49/73] Verify link peer identity against the leaf certificate only - fingerprints only the leaf certificate whose key the TLS handshake proved possession of when verifying a dialing router on an incoming link, instead of the whole presented certificate chain - prevents an enrolled router from being admitted on a link under another router's identity by presenting that router's certificate as filler in its chain - adds a unit test covering leaf-only fingerprinting, including the case where a filler certificate must not contribute a fingerprint --- router/handler_link/bind.go | 24 ++++++++++-- router/handler_link/bind_test.go | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 router/handler_link/bind_test.go diff --git a/router/handler_link/bind.go b/router/handler_link/bind.go index 73da7e456..214fbbda5 100644 --- a/router/handler_link/bind.go +++ b/router/handler_link/bind.go @@ -1,6 +1,7 @@ package handler_link import ( + "crypto/x509" "time" "github.com/michaelquigley/pfxlog" @@ -110,14 +111,18 @@ func (self *bindHandler) BindChannel(binding channel.Binding) error { } func (self *bindHandler) verifyRouter(l xlink.Xlink, ch channel.Channel) error { - var fingerprints []string - for _, cert := range ch.Certificates() { - fingerprints = append(fingerprints, nfpem.FingerprintFromCertificate(cert)) + // Fingerprint only the leaf certificate whose key the TLS handshake proved possession of + // (certs[0]). Fingerprinting the rest of the presented chain would let a peer present a victim + // router's certificate as filler and be admitted under the victim's id, since the controller + // accepts the link if any presented fingerprint matches the claimed router's enrolled one. + fingerprint, err := leafFingerprint(ch.Certificates()) + if err != nil { + return errors.Wrapf(err, "unable to verify router for link %v", l.Id()) } verifyLink := &ctrl_pb.VerifyRouter{ RouterId: l.DestinationId(), - Fingerprints: fingerprints, + Fingerprints: []string{fingerprint}, } ctrlCh := self.ctrl.AnyChannel() @@ -145,6 +150,17 @@ func (self *bindHandler) verifyRouter(l xlink.Xlink, ch channel.Channel) error { return errors.Errorf("unable to verify link [%v]", result.Message) } +// leafFingerprint returns the fingerprint of the leaf certificate whose key the TLS handshake proved +// possession of (certs[0]). Only the leaf is fingerprinted: fingerprinting the rest of the presented +// chain would let a peer present a victim router's certificate as filler and be admitted under the +// victim's id. +func leafFingerprint(certs []*x509.Certificate) (string, error) { + if len(certs) == 0 { + return "", errors.New("no certificates presented") + } + return nfpem.FingerprintFromCertificate(certs[0]), nil +} + type heartbeatCallback struct { latencyMetric metrics.Histogram queueTimeMetric metrics.Histogram diff --git a/router/handler_link/bind_test.go b/router/handler_link/bind_test.go new file mode 100644 index 000000000..af9aca718 --- /dev/null +++ b/router/handler_link/bind_test.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package handler_link + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" + + nfpem "github.com/openziti/foundation/v2/pem" + "github.com/stretchr/testify/require" +) + +// Test_leafFingerprint covers the fingerprint used to verify an incoming link's router: it must come +// only from the leaf certificate the handshake proved (certs[0]). The central case is that a victim +// router's certificate presented as filler elsewhere in the chain must not contribute a fingerprint, +// so it cannot be matched against the victim's enrolled fingerprint to impersonate the victim. +func Test_leafFingerprint(t *testing.T) { + cert := func(cn string) *x509.Certificate { + return &x509.Certificate{Subject: pkix.Name{CommonName: cn}, Raw: []byte("der-" + cn)} + } + + t.Run("returns the leaf fingerprint", func(t *testing.T) { + req := require.New(t) + leaf := cert("router-A") + + fingerprint, err := leafFingerprint([]*x509.Certificate{leaf}) + req.NoError(err) + req.Equal(nfpem.FingerprintFromCertificate(leaf), fingerprint) + }) + + t.Run("ignores filler certs in the rest of the chain", func(t *testing.T) { + req := require.New(t) + leaf := cert("router-A") + filler := cert("router-D") + + fingerprint, err := leafFingerprint([]*x509.Certificate{leaf, filler}) + req.NoError(err) + req.Equal(nfpem.FingerprintFromCertificate(leaf), fingerprint) + req.NotEqual(nfpem.FingerprintFromCertificate(filler), fingerprint, + "a victim cert presented as filler must not contribute a fingerprint") + }) + + t.Run("rejects when no certificates are presented", func(t *testing.T) { + req := require.New(t) + _, err := leafFingerprint(nil) + req.Error(err) + }) +} From c10cb8657a5ca4b18576d883736613a2c444fd62 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Fri, 24 Jul 2026 12:29:34 -0400 Subject: [PATCH 50/73] backport fixes GHSA-j952-6x8x-jmj6 to v2.0 reuse buffered request body in legacy enrollment - reuses the body already buffered by CreateRequestContext instead of reading the request body a second time in legacyGenericEnrollPemHandler - removes the duplicate io.ReadAll allocation for legacy enrollment content types (text/plain, application/pkcs7, application/x-pem-file) --- controller/internal/routes/enroll_router.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/controller/internal/routes/enroll_router.go b/controller/internal/routes/enroll_router.go index d52904924..acd95855b 100644 --- a/controller/internal/routes/enroll_router.go +++ b/controller/internal/routes/enroll_router.go @@ -20,8 +20,6 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" - "fmt" - "io" "net/http" "strings" @@ -280,15 +278,11 @@ func (ro *EnrollRouter) legacyGenericEnrollPemHandler(ae *env.AppEnv, rc *respon return } - body, err := io.ReadAll(rc.Request.Body) - - if err != nil { - rc.RespondWithError(fmt.Errorf("could not read body: %w", err)) - return - } - + // The request body has already been read and buffered into rc.Body by + // CreateRequestContext; reuse that buffer rather than reading rc.Request.Body + // again, which would allocate a second full copy of a pre-auth request body. enrollContext.Data = &model.EnrollmentData{ - ClientCsrPem: body, + ClientCsrPem: rc.Body, } ro.processEnrollContext(ae, rc, enrollContext) From d6ccaff24a410f43eb475fbff00fb20257bd87cc Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Fri, 24 Jul 2026 16:36:22 -0400 Subject: [PATCH 51/73] adds regression test for api session certificate scoping - asserts the current api session certificate list excludes another identity's certificates - asserts scoping is per api session, so a second api session of the same identity is excluded - adds CreateCurrentApiSessionCertificate and ListCurrentApiSessionCertificates client helpers --- tests/api_client_client.go | 52 ++++++++++ tests/api_session_certificates_test.go | 126 +++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/tests/api_client_client.go b/tests/api_client_client.go index b37097534..c837a837b 100644 --- a/tests/api_client_client.go +++ b/tests/api_client_client.go @@ -420,6 +420,58 @@ func (helper *ClientHelperClient) GetCurrentApiSessionDetail() (*rest_model.Curr return resp.Payload.Data, nil } +// CreateCurrentApiSessionCertificate generates a new key and CSR, then submits it to +// create an API session certificate for the currently authenticated API Session. +func (helper *ClientHelperClient) CreateCurrentApiSessionCertificate() (*rest_model.CurrentAPISessionCertificateCreateResponse, error) { + request, err := certtools.NewCertRequest(map[string]string{ + "C": "US", "O": "NetFoundry-API-Test", "CN": uuid.NewString(), + }, nil) + + if err != nil { + return nil, fmt.Errorf("could not create base CSR values: %w", err) + } + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("could not generate private key: %w", err) + } + + csr, err := x509.CreateCertificateRequest(rand.Reader, request, privateKey) + if err != nil { + return nil, fmt.Errorf("could not create CSR: %w", err) + } + + csrPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})) + + params := &clientCurrentApiSession.CreateCurrentAPISessionCertificateParams{ + SessionCertificate: &rest_model.CurrentAPISessionCertificateCreate{ + Csr: &csrPem, + }, + } + + resp, err := helper.API.CurrentAPISession.CreateCurrentAPISessionCertificate(params, nil) + + if err != nil { + return nil, fmt.Errorf("could not create current api session certificate: %w", rest_util.WrapErr(err)) + } + + return resp.Payload.Data, nil +} + +// ListCurrentApiSessionCertificates returns the API session certificates visible to the +// currently authenticated API Session. +func (helper *ClientHelperClient) ListCurrentApiSessionCertificates() (rest_model.CurrentAPISessionCertificateList, error) { + params := &clientCurrentApiSession.ListCurrentAPISessionCertificatesParams{} + + resp, err := helper.API.CurrentAPISession.ListCurrentAPISessionCertificates(params, nil) + + if err != nil { + return nil, fmt.Errorf("could not list current api session certificates: %w", rest_util.WrapErr(err)) + } + + return resp.Payload.Data, nil +} + func (helper *ClientHelperClient) GetTotpMfa() (*rest_model.DetailMfa, error) { params := &clientCurrentIdentity.DetailMfaParams{} diff --git a/tests/api_session_certificates_test.go b/tests/api_session_certificates_test.go index 6d462d7bd..f31a09225 100644 --- a/tests/api_session_certificates_test.go +++ b/tests/api_session_certificates_test.go @@ -306,6 +306,132 @@ func Test_Api_Session_Certs(t *testing.T) { }) } +// Test_Api_Session_Certs_Scoped_To_Api_Session locks in that listing the current API Session's +// certificates only ever returns certificates belonging to the requesting API Session. The list +// was previously unscoped, so any authenticated identity could read every other identity's API +// session certificates. +func Test_Api_Session_Certs_Scoped_To_Api_Session(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + adminManagementClient := ctx.NewEdgeManagementApi(nil) + adminCreds := edge_apis.NewUpdbCredentials(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password) + + adminApiSession, err := adminManagementClient.Authenticate(adminCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(adminApiSession) + + // the admin's own api session certificate is a cert that must never show up in another + // api session's list + adminClient := ctx.NewEdgeClientApi(nil) + _, err = adminClient.Authenticate(adminCreds, nil) + ctx.Req.NoError(err) + + adminCert, err := adminClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(adminCert.ID) + + t.Run("another identity's api session certificates are not listed", func(t *testing.T) { + ctx.testContextChanged(t) + + _, firstCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstCreds) + + _, secondCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondCreds) + + firstClient := ctx.NewEdgeClientApi(nil) + firstApiSession, err := firstClient.Authenticate(firstCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstApiSession) + + secondClient := ctx.NewEdgeClientApi(nil) + secondApiSession, err := secondClient.Authenticate(secondCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondApiSession) + + firstCert, err := firstClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(firstCert.ID) + + secondCert, err := secondClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(secondCert.ID) + + firstList, err := firstClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + firstIds := apiSessionCertificateIds(firstList) + ctx.Req.Contains(firstIds, firstCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(firstIds, secondCert.ID, "expected another identity's api session certificate to not be listed") + ctx.Req.NotContains(firstIds, adminCert.ID, "expected the admin's api session certificate to not be listed") + + secondList, err := secondClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + secondIds := apiSessionCertificateIds(secondList) + ctx.Req.Contains(secondIds, secondCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(secondIds, firstCert.ID, "expected another identity's api session certificate to not be listed") + ctx.Req.NotContains(secondIds, adminCert.ID, "expected the admin's api session certificate to not be listed") + }) + + t.Run("a second api session for the same identity does not list the first api session's certificates", func(t *testing.T) { + ctx.testContextChanged(t) + + _, idCreds, err := adminManagementClient.CreateAndEnrollOttIdentity(false) + ctx.Req.NoError(err) + ctx.Req.NotNil(idCreds) + + firstClient := ctx.NewEdgeClientApi(nil) + firstApiSession, err := firstClient.Authenticate(idCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(firstApiSession) + + secondClient := ctx.NewEdgeClientApi(nil) + secondApiSession, err := secondClient.Authenticate(idCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(secondApiSession) + + firstCert, err := firstClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(firstCert.ID) + + secondCert, err := secondClient.CreateCurrentApiSessionCertificate() + ctx.Req.NoError(err) + ctx.Req.NotEmpty(secondCert.ID) + + ctx.Req.NotEqual(firstCert.ID, secondCert.ID) + + firstList, err := firstClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + // scoping is per api session, not per identity, so the same identity's other + // api session certificate must not appear + firstIds := apiSessionCertificateIds(firstList) + ctx.Req.Contains(firstIds, firstCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(firstIds, secondCert.ID, "expected the same identity's other api session certificate to not be listed") + + secondList, err := secondClient.ListCurrentApiSessionCertificates() + ctx.Req.NoError(err) + + secondIds := apiSessionCertificateIds(secondList) + ctx.Req.Contains(secondIds, secondCert.ID, "expected the api session's own certificate to be listed") + ctx.Req.NotContains(secondIds, firstCert.ID, "expected the same identity's other api session certificate to not be listed") + }) +} + +// apiSessionCertificateIds returns the ids of the given API session certificates. +func apiSessionCertificateIds(certs rest_model.CurrentAPISessionCertificateList) []string { + var ids []string + for _, cert := range certs { + ids = append(ids, *cert.ID) + } + return ids +} + func generateCsr() ([]byte, crypto.PrivateKey, error) { p384 := elliptic.P384() pfxlog.Logger().Infof("generating %s EC key", p384.Params().Name) From 92116f7b177b9fa4cfc5a38324190251a4eaaf29 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 12 Aug 2026 15:41:04 -0400 Subject: [PATCH 52/73] Note the router link identity advisory in the 2.0.3 release notes --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0ac2038..f6a836331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes * Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear scan over all open read transactions that ran while holding bbolt's single global transaction @@ -18,6 +19,17 @@ Thanks to the community members who contributed to this release. [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a production workload. +## Security Advisories + +This release addresses a router-to-router link identity-spoofing vulnerability. See the linked GitHub +Security Advisory for full details, impact, and affected versions. + +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. + ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) From 1d3569a281f45c18566abb45f712a797a213faf0 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 17:46:03 -0400 Subject: [PATCH 53/73] Add consolidated 2.0.3 security advisory notes to the changelog --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0ac2038..b99ea3b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes * Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear scan over all open read transactions that ran while holding bbolt's single global transaction @@ -10,6 +11,44 @@ policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks fully busy while little work completes, and timeouts unexplained by the actual workload. +## Security Advisories + +This release addresses eight security advisories. See the linked GitHub Security Advisories for full +details, impact, and affected versions. + +* [GHSA-q8g9-jc4c-jp6q](https://github.com/openziti/ziti/security/advisories/GHSA-q8g9-jc4c-jp6q) (CVE pending) (High) - The controller buffered the entire body of every + inbound request before any authentication check and with no size cap, so an unauthenticated client could + exhaust controller memory, and crash it, by sending parallel large-body requests to endpoints such as + enrollment. +* [GHSA-j952-6x8x-jmj6](https://github.com/openziti/ziti/security/advisories/GHSA-j952-6x8x-jmj6) (CVE pending) (High) - The unauthenticated legacy enrollment path buffered + the request body a second time, allocating twice the memory per request and roughly halving the bandwidth + needed to drive the controller out of memory. Amplifies GHSA-q8g9-jc4c-jp6q. +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via CreateCircuitV3, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. +* [GHSA-4h58-w989-xgg4](https://github.com/openziti/ziti/security/advisories/GHSA-4h58-w989-xgg4) (CVE pending) (Medium) - The token-based enrollment endpoint skipped + audience and issuer validation when the request carried a `ziti-token-issuer-id` header, so an attacker + holding any unexpired JWT signed by a configured external JWT signer, even one minted for a different + audience, could enroll a new identity onto the network. +* [GHSA-6v5r-p2wr-q492](https://github.com/openziti/ziti/security/advisories/GHSA-6v5r-p2wr-q492) (CVE pending) (Medium) - The current-api-session certificates endpoint + performed an unscoped list, so any authenticated user could read the API session certificates (subject + DNs, fingerprints, and full PEM chains) of all identities, not just their own. +* [GHSA-whjr-3j94-gw3c](https://github.com/openziti/ziti/security/advisories/GHSA-whjr-3j94-gw3c) (CVE pending) (Medium) - A JWKS endpoint URL configured on an external JWT + signer was fetched server-side with no timeout, private-range blocking, or allowlist, letting a caller with + external-jwt-signer management access make the controller issue requests to arbitrary internal URLs, + including cloud metadata endpoints (SSRF). +* [GHSA-354c-gpg9-j988](https://github.com/openziti/ziti/security/advisories/GHSA-354c-gpg9-j988) (CVE pending) (Low) - With promptOnWake or promptOnUnlock enabled on an MFA + posture check, the edge router dereferenced a nil wake/unlock timestamp while locally evaluating an + authorized client's dial or bind, panicking and crashing the router (data-plane denial of service). + ## Contributors Thanks to the community members who contributed to this release. From 75c6c74b4d195cc513d28844765345806bf1b0c1 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 17:46:03 -0400 Subject: [PATCH 54/73] Add consolidated 2.0.3 security advisory notes to the changelog --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0ac2038..b99ea3b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## What's New +* Security fixes (see Security Advisories below) * Bug fixes * Controller read throughput under load: this release picks up bbolt v1.5.0, which removes a linear scan over all open read transactions that ran while holding bbolt's single global transaction @@ -10,6 +11,44 @@ policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks fully busy while little work completes, and timeouts unexplained by the actual workload. +## Security Advisories + +This release addresses eight security advisories. See the linked GitHub Security Advisories for full +details, impact, and affected versions. + +* [GHSA-q8g9-jc4c-jp6q](https://github.com/openziti/ziti/security/advisories/GHSA-q8g9-jc4c-jp6q) (CVE pending) (High) - The controller buffered the entire body of every + inbound request before any authentication check and with no size cap, so an unauthenticated client could + exhaust controller memory, and crash it, by sending parallel large-body requests to endpoints such as + enrollment. +* [GHSA-j952-6x8x-jmj6](https://github.com/openziti/ziti/security/advisories/GHSA-j952-6x8x-jmj6) (CVE pending) (High) - The unauthenticated legacy enrollment path buffered + the request body a second time, allocating twice the memory per request and roughly halving the bandwidth + needed to drive the controller out of memory. Amplifies GHSA-q8g9-jc4c-jp6q. +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via CreateCircuitV3, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. +* [GHSA-4h58-w989-xgg4](https://github.com/openziti/ziti/security/advisories/GHSA-4h58-w989-xgg4) (CVE pending) (Medium) - The token-based enrollment endpoint skipped + audience and issuer validation when the request carried a `ziti-token-issuer-id` header, so an attacker + holding any unexpired JWT signed by a configured external JWT signer, even one minted for a different + audience, could enroll a new identity onto the network. +* [GHSA-6v5r-p2wr-q492](https://github.com/openziti/ziti/security/advisories/GHSA-6v5r-p2wr-q492) (CVE pending) (Medium) - The current-api-session certificates endpoint + performed an unscoped list, so any authenticated user could read the API session certificates (subject + DNs, fingerprints, and full PEM chains) of all identities, not just their own. +* [GHSA-whjr-3j94-gw3c](https://github.com/openziti/ziti/security/advisories/GHSA-whjr-3j94-gw3c) (CVE pending) (Medium) - A JWKS endpoint URL configured on an external JWT + signer was fetched server-side with no timeout, private-range blocking, or allowlist, letting a caller with + external-jwt-signer management access make the controller issue requests to arbitrary internal URLs, + including cloud metadata endpoints (SSRF). +* [GHSA-354c-gpg9-j988](https://github.com/openziti/ziti/security/advisories/GHSA-354c-gpg9-j988) (CVE pending) (Low) - With promptOnWake or promptOnUnlock enabled on an MFA + posture check, the edge router dereferenced a nil wake/unlock timestamp while locally evaluating an + authorized client's dial or bind, panicking and crashing the router (data-plane denial of service). + ## Contributors Thanks to the community members who contributed to this release. From 82037ba2605f4c4a238ccc28e594d00f486f3136 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 17:46:04 -0400 Subject: [PATCH 55/73] Add consolidated 2.0.3 security advisory notes to the changelog --- CHANGELOG.md | 49 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a836331..b99ea3b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,44 @@ policy queries into a lock convoy: many goroutines waiting on one mutex, a machine that looks fully busy while little work completes, and timeouts unexplained by the actual workload. +## Security Advisories + +This release addresses eight security advisories. See the linked GitHub Security Advisories for full +details, impact, and affected versions. + +* [GHSA-q8g9-jc4c-jp6q](https://github.com/openziti/ziti/security/advisories/GHSA-q8g9-jc4c-jp6q) (CVE pending) (High) - The controller buffered the entire body of every + inbound request before any authentication check and with no size cap, so an unauthenticated client could + exhaust controller memory, and crash it, by sending parallel large-body requests to endpoints such as + enrollment. +* [GHSA-j952-6x8x-jmj6](https://github.com/openziti/ziti/security/advisories/GHSA-j952-6x8x-jmj6) (CVE pending) (High) - The unauthenticated legacy enrollment path buffered + the request body a second time, allocating twice the memory per request and roughly halving the bandwidth + needed to drive the controller out of memory. Amplifies GHSA-q8g9-jc4c-jp6q. +* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a + router verified the dialing router's identity against the whole presented certificate chain instead of the + leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could + present another router's certificate as filler and be admitted on a link under that router's identity, + letting it intercept, inject, drop, or strand the circuits routed over that link. +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via CreateCircuitV3, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. +* [GHSA-4h58-w989-xgg4](https://github.com/openziti/ziti/security/advisories/GHSA-4h58-w989-xgg4) (CVE pending) (Medium) - The token-based enrollment endpoint skipped + audience and issuer validation when the request carried a `ziti-token-issuer-id` header, so an attacker + holding any unexpired JWT signed by a configured external JWT signer, even one minted for a different + audience, could enroll a new identity onto the network. +* [GHSA-6v5r-p2wr-q492](https://github.com/openziti/ziti/security/advisories/GHSA-6v5r-p2wr-q492) (CVE pending) (Medium) - The current-api-session certificates endpoint + performed an unscoped list, so any authenticated user could read the API session certificates (subject + DNs, fingerprints, and full PEM chains) of all identities, not just their own. +* [GHSA-whjr-3j94-gw3c](https://github.com/openziti/ziti/security/advisories/GHSA-whjr-3j94-gw3c) (CVE pending) (Medium) - A JWKS endpoint URL configured on an external JWT + signer was fetched server-side with no timeout, private-range blocking, or allowlist, letting a caller with + external-jwt-signer management access make the controller issue requests to arbitrary internal URLs, + including cloud metadata endpoints (SSRF). +* [GHSA-354c-gpg9-j988](https://github.com/openziti/ziti/security/advisories/GHSA-354c-gpg9-j988) (CVE pending) (Low) - With promptOnWake or promptOnUnlock enabled on an MFA + posture check, the edge router dereferenced a nil wake/unlock timestamp while locally evaluating an + authorized client's dial or bind, panicking and crashing the router (data-plane denial of service). + ## Contributors Thanks to the community members who contributed to this release. @@ -19,17 +57,6 @@ Thanks to the community members who contributed to this release. [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a production workload. -## Security Advisories - -This release addresses a router-to-router link identity-spoofing vulnerability. See the linked GitHub -Security Advisory for full details, impact, and affected versions. - -* [GHSA-hhm9-wf63-g7qj](https://github.com/openziti/ziti/security/advisories/GHSA-hhm9-wf63-g7qj) (CVE pending) (Medium) - When accepting an incoming router-to-router link, a - router verified the dialing router's identity against the whole presented certificate chain instead of the - leaf certificate whose key the TLS handshake proved. An attacker holding enrolled router credentials could - present another router's certificate as filler and be admitted on a link under that router's identity, - letting it intercept, inject, drop, or strand the circuits routed over that link. - ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) From 5964561abbddfd7b7c6169bc9caf0ef85be177bf Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Tue, 7 Jul 2026 21:46:56 -0400 Subject: [PATCH 56/73] backport fixes GHSA-4h58-w989-xgg4 to v2.0 enforce issuer and audience in ext-jwt token enrollment - enforces issuer and audience claims inside TokenIssuerExtJwt.VerifyToken, which previously verified only the token signature and resolved the signing key by kid - closes the ziti-token-issuer-id header enrollment path accepting a validly-signed token minted for a different audience or issuer that shares signing keys - mirrors the runtime ext-jwt authentication and by-inspection enrollment paths so all paths validate claims consistently - adds a controller/model unit test covering foreign-audience, foreign-issuer, and missing-audience rejection --- controller/model/token_provider_cache.go | 41 +++++++ controller/model/token_provider_cache_test.go | 106 ++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/controller/model/token_provider_cache.go b/controller/model/token_provider_cache.go index c59c33a0e..1719558d4 100644 --- a/controller/model/token_provider_cache.go +++ b/controller/model/token_provider_cache.go @@ -619,6 +619,10 @@ func (r *TokenIssuerExtJwt) VerifyToken(token string) *common.TokenVerificationR return result } + if result.Error = r.verifyIssuerAndAudience(claims); result.Error != nil { + return result + } + result.IdClaimValue, result.Error = resolveStringClaimSelector(claims, r.IdentityIdClaimsSelector()) if result.Error != nil { @@ -628,6 +632,43 @@ func (r *TokenIssuerExtJwt) VerifyToken(token string) *common.TokenVerificationR return result } +// verifyIssuerAndAudience confirms the token's issuer and audience claims match this signer's +// configured values. Signature verification alone resolves the signing key by kid, which is not +// sufficient: a signer that shares keys across audiences (for example a common JWKS) would +// otherwise accept a validly-signed token minted for a different audience or issuer. Callers must +// invoke this only after the signature has been verified. +func (r *TokenIssuerExtJwt) verifyIssuerAndAudience(claims jwt.MapClaims) error { + issuer, err := claims.GetIssuer() + + if err != nil { + return fmt.Errorf("could not retrieve issuer claim from token: %w", err) + } + + if issuer == "" { + return errors.New("token claims did not contain an issuer") + } + + if issuer != r.ExpectedIssuer() { + return fmt.Errorf("token issuer [%s] does not match expected issuer [%s]", issuer, r.ExpectedIssuer()) + } + + audiences, err := claims.GetAudience() + + if err != nil { + return fmt.Errorf("could not retrieve audience claim from token: %w", err) + } + + if len(audiences) == 0 { + return errors.New("token claims did not contain an audience") + } + + if !stringz.Contains(audiences, r.ExpectedAudience()) { + return fmt.Errorf("token audience %v does not match expected audience [%s]", audiences, r.ExpectedAudience()) + } + + return nil +} + // resolveStringSliceClaimProperty extracts a string or string array from JWT claims using a JSON pointer. // Returns a string slice even if the claim is a single string value. An unset selector, a pointer that does // not resolve against the claims, and a null claim all resolve to no values without error. A claim that is diff --git a/controller/model/token_provider_cache_test.go b/controller/model/token_provider_cache_test.go index 73e39d3d3..7f63f02df 100644 --- a/controller/model/token_provider_cache_test.go +++ b/controller/model/token_provider_cache_test.go @@ -1,12 +1,118 @@ package model import ( + "crypto/x509" "testing" + "time" "github.com/golang-jwt/jwt/v5" + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/controller/db" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/stretchr/testify/require" ) +// Test_TokenIssuerExtJwt_VerifyToken_EnforcesIssuerAndAudience verifies that a validly-signed +// token minted for a different issuer or audience is rejected. The by-inspection enrollment path +// already enforces this; VerifyToken (used by the by-issuer-id enrollment path) must not be weaker. +func Test_TokenIssuerExtJwt_VerifyToken_EnforcesIssuerAndAudience(t *testing.T) { + req := require.New(t) + + testRootCa := newRootCa() + leafKeyPair := testRootCa.NewLeafWithAKID() + + jwksEndpoint := "https://example.com/.well-known/jwks" + + jwksResolver, err := newTestJwksResolver() + req.NoError(err) + + leafKey, err := newKey(leafKeyPair.cert, []*x509.Certificate{leafKeyPair.cert, testRootCa.cert}) + req.NoError(err) + + jwksResolver.AddKey(leafKey, leafKeyPair.key) + + expectedIssuer := "https://idp.example.com" + expectedAudience := "ziti-controller" + + signerRec := &TokenIssuerExtJwt{ + kidToPubKey: map[string]common.IssuerPublicKey{}, + externalJwtSigner: &db.ExternalJwtSigner{ + BaseExtEntity: boltz.BaseExtEntity{ + Id: "fake-id", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + Name: "test-signer", + JwksEndpoint: &jwksEndpoint, + Issuer: &expectedIssuer, + Audience: &expectedAudience, + Enabled: true, + }, + jwksResolver: jwksResolver, + } + + req.NoError(signerRec.Resolve(false)) + + sign := func(claims jwt.MapClaims) string { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = leafKey.KeyId + signed, err := token.SignedString(leafKeyPair.key) + req.NoError(err) + return signed + } + + t.Run("accepts a token with matching issuer and audience", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "aud": expectedAudience, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.Truef(result.IsValid(), "token with matching issuer/audience must be accepted: %v", result.Error) + }) + + t.Run("rejects a validly-signed token with a foreign audience", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "aud": "some-other-audience", + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token minted for a foreign audience must be rejected") + }) + + t.Run("rejects a validly-signed token with a foreign issuer", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": "https://attacker.example.com", + "aud": expectedAudience, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token minted by a foreign issuer must be rejected") + }) + + t.Run("rejects a token missing the audience claim", func(t *testing.T) { + req := require.New(t) + signed := sign(jwt.MapClaims{ + "iss": expectedIssuer, + "sub": "user-123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + result := signerRec.VerifyToken(signed) + req.False(result.IsValid(), "token without an audience claim must be rejected") + }) +} + func Test_resolveStringSliceClaimProperty(t *testing.T) { t.Run("returns empty when the selector is unset", func(t *testing.T) { req := require.New(t) From b79d6aa324ea97455caa5d6fb815d8f86db554f3 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Sat, 25 Jul 2026 12:03:39 -0400 Subject: [PATCH 57/73] backport fixes GHSA-whjr-3j94-gw3c to v2.0 constrain external jwt signer JWKS fetching - adds HardenedJwksResolver, a jwks.Resolver that fetches an external jwt signer's jwksEndpoint with an http/https-only scheme check, a total timeout and a redirect cap - adds JwksFetchPolicy, gating a fetch on both the URL hostname and the address being connected to, applied to the first request and to every redirect hop - hostname gate: deniedHostnames blocks, allowedHostnames is exclusive when set; entries are an exact hostname or a '*.suffix' wildcard that matches subdomains at any depth but never the suffix itself, normalized to lower case punycode without a trailing dot - address gate: built-in blocked (metadata, link-local, link-local multicast, unspecified), then deniedIPs, then allowedIPs, then blockPrivateAddresses, first-match-wins with deny over allow - keeps the gates independent, so neither can authorize what the other refuses; the address check runs in the dialer against the resolved address, so a hostname that resolves to a blocked address is refused - adds the [edge.externalJwtSigners.jwksFetch] config section with compatible defaults: empty hostname lists, blockPrivateAddresses false, timeout 5s, maxRedirects 5 - takes IP lists as a flat address or a CIDR block, hostname lists as names only, and rejects an entry belonging to the other list at startup - rejects a jwksEndpoint the policy refuses when an external jwt signer is created or updated, and logs an existing signer whose endpoint the configuration now refuses when the token issuer cache loads - documents both gates, their deny-wins precedence, the accepted entry forms and the wildcard matching rules in etc/ctrl.with.edge.yml and CHANGELOG.md under Release 2.0.2 --- controller/config/config_edge.go | 336 +++++++ controller/config/config_edge_test.go | 255 ++++++ .../model/external_jwt_signer_manager.go | 37 +- controller/model/jwks_resolver.go | 392 ++++++++ controller/model/jwks_resolver_test.go | 848 ++++++++++++++++++ controller/model/token_provider_cache.go | 24 +- etc/ctrl.with.edge.yml | 135 +++ .../external_jwt_signer_jwks_endpoint_test.go | 88 ++ 8 files changed, 2112 insertions(+), 3 deletions(-) create mode 100644 controller/model/jwks_resolver.go create mode 100644 controller/model/jwks_resolver_test.go create mode 100644 tests/external_jwt_signer_jwks_endpoint_test.go diff --git a/controller/config/config_edge.go b/controller/config/config_edge.go index 3eae0cf13..cf9b3afd7 100644 --- a/controller/config/config_edge.go +++ b/controller/config/config_edge.go @@ -37,6 +37,7 @@ import ( "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/controller/command" "github.com/pkg/errors" + "golang.org/x/net/idna" ) const ( @@ -72,6 +73,17 @@ const ( DefaultIdentityOnlineStatusUnknownTimeout = 5 * time.Minute DefaultIdentityOnlineStatusSource = IdentityStatusSourceHybrid + + // DefaultJwksFetchBlockPrivateAddresses leaves private and loopback addresses reachable by + // default so that deployments using an internal IdP keep working. Metadata and link-local + // addresses are blocked regardless of this setting. + DefaultJwksFetchBlockPrivateAddresses = false + + // DefaultJwksFetchTimeout bounds the total time spent fetching a JWKS endpoint. + DefaultJwksFetchTimeout = 5 * time.Second + + // DefaultJwksFetchMaxRedirects bounds how many redirects a JWKS fetch will follow. + DefaultJwksFetchMaxRedirects = 5 ) type Enrollment struct { @@ -136,6 +148,67 @@ func (o *Oidc) MaxTokenDuration() time.Duration { return common.MaxTokenDuration(o.RefreshTokenDuration, o.AccessTokenDuration, o.IdTokenDuration) } +// ExternalJwtSigners holds settings that govern how the controller interacts with +// external JWT signers. +type ExternalJwtSigners struct { + JwksFetch JwksFetch +} + +// JwksFetch controls the server-side fetch of an external JWT signer's jwksEndpoint. +// The endpoint URL is supplied by an operator, so the fetch is constrained to keep it +// from being pointed at addresses the controller can reach but a caller should not. +// +// A hop is fetched only if it passes two independent gates. Neither gate can authorize what +// the other refuses, and both are applied to the initial request and to every redirect. +// +// The host gate is applied to the URL's hostname: +// +// 1. DeniedHostnames - blocked +// 2. AllowedHostnames, when non-empty and the host does not match - blocked +// 3. otherwise - passes +// +// The address gate is applied to the resolved address being connected to, first-match-wins, +// deny before allow: +// +// 1. built-in blocked addresses (cloud metadata, link-local, link-local multicast, +// unspecified) - always blocked, AllowedIPs cannot override +// 2. DeniedIPs - blocked, AllowedIPs cannot override +// 3. AllowedIPs - allowed; a carve-out of tier 4 only +// 4. BlockPrivateAddresses and the address is private or loopback - blocked +// 5. everything else - allowed +type JwksFetch struct { + // BlockPrivateAddresses blocks private and loopback addresses (address gate tier 4). + // Defaults to false so deployments with an internal IdP keep working; tier 1 applies + // regardless. + BlockPrivateAddresses bool + + // DeniedIPs are CIDRs that are always blocked (address gate tier 2), above + // AllowedIPs. + DeniedIPs []*net.IPNet + + // AllowedIPs are CIDRs that carve an exception out of BlockPrivateAddresses + // (address gate tier 3). They do not override tier 1 or DeniedIPs. + AllowedIPs []*net.IPNet + + // DeniedHostnames are normalized hostname patterns that are blocked (hostname gate tier 1). Host + // matching only ever narrows what may be fetched: it cannot authorize an address the + // address gate blocks, and a caller can still reach the same target under another name, + // so the address gate remains the boundary. + DeniedHostnames []string + + // AllowedHostnames are normalized hostname patterns that, when non-empty, are the only hosts that + // may be fetched (hostname gate tier 2). Entries are an exact hostname (idp.example.com) or a + // wildcard suffix (*.example.com), which matches any subdomain but not the suffix itself. + AllowedHostnames []string + + // Timeout bounds the total time spent on a single JWKS fetch, including redirects. + Timeout time.Duration + + // MaxRedirects bounds how many redirects a JWKS fetch will follow. Every hop is + // address-checked. Zero disables redirects. + MaxRedirects int +} + type EdgeConfig struct { Enabled bool Api Api @@ -149,6 +222,7 @@ type EdgeConfig struct { caCerts []*x509.Certificate caCertPool *x509.CertPool DisablePostureChecks bool + ExternalJwtSigners ExternalJwtSigners } type HttpTimeouts struct { @@ -182,10 +256,24 @@ type IdentityStatusConfig struct { UnknownTimeout time.Duration } +// DefaultJwksFetch returns the default JWKS fetch settings. The defaults are deliberately +// compatible with existing deployments: only the non-disableable built-in blocked addresses +// are refused. +func DefaultJwksFetch() JwksFetch { + return JwksFetch{ + BlockPrivateAddresses: DefaultJwksFetchBlockPrivateAddresses, + Timeout: DefaultJwksFetchTimeout, + MaxRedirects: DefaultJwksFetchMaxRedirects, + } +} + func NewEdgeConfig() *EdgeConfig { return &EdgeConfig{ Enabled: false, caPems: bytes.NewBuffer(nil), + ExternalJwtSigners: ExternalJwtSigners{ + JwksFetch: DefaultJwksFetch(), + }, } } @@ -703,6 +791,250 @@ func (c *EdgeConfig) loadIdentityStatusConfig(cfgmap map[interface{}]interface{} return nil } +// loadExternalJwtSignersSection loads [edge.externalJwtSigners]. Every value is optional; +// absent values keep the defaults from DefaultJwksFetch. +func (c *EdgeConfig) loadExternalJwtSignersSection(edgeConfigMap map[any]any) error { + c.ExternalJwtSigners.JwksFetch = DefaultJwksFetch() + + value, found := edgeConfigMap["externalJwtSigners"] + + if !found || value == nil { + return nil + } + + extJwtSignersMap, ok := value.(map[any]any) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners], must be a map", value) + } + + value, found = extJwtSignersMap["jwksFetch"] + + if !found || value == nil { + return nil + } + + jwksFetchMap, ok := value.(map[any]any) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch], must be a map", value) + } + + jwksFetch := &c.ExternalJwtSigners.JwksFetch + + if val, found := jwksFetchMap["blockPrivateAddresses"]; found && val != nil { + switch typedVal := val.(type) { + case bool: + jwksFetch.BlockPrivateAddresses = typedVal + case string: + boolVal, err := strconv.ParseBool(typedVal) + if err != nil { + return errors.Errorf("invalid value %q for [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses], must be a boolean", typedVal) + } + jwksFetch.BlockPrivateAddresses = boolVal + default: + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses], must be a boolean", val) + } + } + + if val, found := jwksFetchMap["deniedIPs"]; found && val != nil { + addresses, err := parseCidrList(val, "edge.externalJwtSigners.jwksFetch.deniedIPs") + if err != nil { + return err + } + jwksFetch.DeniedIPs = addresses + } + + if val, found := jwksFetchMap["allowedIPs"]; found && val != nil { + addresses, err := parseCidrList(val, "edge.externalJwtSigners.jwksFetch.allowedIPs") + if err != nil { + return err + } + jwksFetch.AllowedIPs = addresses + } + + if val, found := jwksFetchMap["deniedHostnames"]; found && val != nil { + hosts, err := parseHostnameList(val, "edge.externalJwtSigners.jwksFetch.deniedHostnames") + if err != nil { + return err + } + jwksFetch.DeniedHostnames = hosts + } + + if val, found := jwksFetchMap["allowedHostnames"]; found && val != nil { + hosts, err := parseHostnameList(val, "edge.externalJwtSigners.jwksFetch.allowedHostnames") + if err != nil { + return err + } + jwksFetch.AllowedHostnames = hosts + } + + if val, found := jwksFetchMap["timeout"]; found && val != nil { + strVal, ok := val.(string) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.timeout], must be a string duration", val) + } + + durationVal, err := time.ParseDuration(strVal) + + if err != nil { + return errors.Errorf("error parsing [edge.externalJwtSigners.jwksFetch.timeout], invalid duration string %s, cannot parse as duration (e.g. 5s): %v", strVal, err) + } + + if durationVal <= 0 { + return errors.Errorf("invalid value %s for [edge.externalJwtSigners.jwksFetch.timeout], must be greater than zero", strVal) + } + + jwksFetch.Timeout = durationVal + } + + if val, found := jwksFetchMap["maxRedirects"]; found && val != nil { + intVal, ok := val.(int) + + if !ok { + return errors.Errorf("invalid type %T for [edge.externalJwtSigners.jwksFetch.maxRedirects], must be an integer", val) + } + + if intVal < 0 { + return errors.Errorf("invalid value %v for [edge.externalJwtSigners.jwksFetch.maxRedirects], must not be negative", intVal) + } + + jwksFetch.MaxRedirects = intVal + } + + return nil +} + +// parseCidrList parses a list of CIDRs, accepting a bare IP address as a single address CIDR +// (/32 for IPv4, /128 for IPv6). Hostnames are rejected: the address check happens at dial +// time against the resolved IP, so a hostname entry could never be matched reliably. +func parseCidrList(value any, field string) ([]*net.IPNet, error) { + values, ok := value.([]any) + + if !ok { + return nil, errors.Errorf("invalid type %T for [%s], must be a list of CIDRs", value, field) + } + + var result []*net.IPNet + + for _, entry := range values { + strVal, ok := entry.(string) + + if !ok { + return nil, errors.Errorf("invalid type %T for an entry in [%s], must be a string CIDR", entry, field) + } + + ipNet, err := parseCidrOrIp(strings.TrimSpace(strVal)) + + if err != nil { + return nil, errors.Errorf("invalid value %q in [%s]: %v", strVal, field, err) + } + + result = append(result, ipNet) + } + + return result, nil +} + +// parseHostnameList parses a list of hostname patterns, returning them normalized for comparison +// against a URL's hostname. +func parseHostnameList(value any, field string) ([]string, error) { + values, ok := value.([]any) + + if !ok { + return nil, errors.Errorf("invalid type %T for [%s], must be a list of hostnames", value, field) + } + + var result []string + + for _, entry := range values { + strVal, ok := entry.(string) + + if !ok { + return nil, errors.Errorf("invalid type %T for an entry in [%s], must be a string hostname", entry, field) + } + + pattern, err := parseHostnamePattern(strings.TrimSpace(strVal)) + + if err != nil { + return nil, errors.Errorf("invalid value %q in [%s]: %v", strVal, field, err) + } + + result = append(result, pattern) + } + + return result, nil +} + +// parseHostnamePattern validates a host entry and returns it normalized. An entry is either an +// exact host (idp.example.com) or a wildcard suffix (*.example.com), which matches any +// subdomain of that suffix but not the suffix itself. IP addresses are rejected: matching an +// address by name comparison would not be an address check. +func parseHostnamePattern(value string) (string, error) { + if value == "" { + return "", errors.New("must not be empty") + } + + host := strings.TrimPrefix(value, "*.") + isWildcard := host != value + + if net.ParseIP(strings.Trim(host, "[]")) != nil { + return "", errors.New("must be a hostname, use deniedIPs or allowedIPs for IP addresses") + } + + if host == "" { + return "", errors.New("must include a hostname after the leading \"*.\"") + } + + if strings.ContainsAny(host, "*/:@ \t") { + return "", errors.New("must be a bare host name, without a scheme, port or path, and a wildcard is only supported as a leading \"*.\"") + } + + normalized := NormalizeHostname(host) + + if normalized == "" { + return "", errors.New("must be a valid host name") + } + + if isWildcard { + return "*." + normalized, nil + } + + return normalized, nil +} + +// NormalizeHostname returns a host in the form used for comparison: lower-cased, without a +// trailing dot, and converted to punycode when it contains non-ASCII labels. Config entries +// and request hosts are both normalized this way so that they compare consistently. +func NormalizeHostname(host string) string { + host = strings.TrimSuffix(strings.TrimSpace(host), ".") + + if ascii, err := idna.Lookup.ToASCII(host); err == nil { + host = ascii + } + + return strings.ToLower(host) +} + +// parseCidrOrIp parses a CIDR or a bare IP address into a *net.IPNet. A bare IP address +// becomes a single address CIDR. +func parseCidrOrIp(value string) (*net.IPNet, error) { + if _, ipNet, err := net.ParseCIDR(value); err == nil { + return ipNet, nil + } + + if ip := net.ParseIP(value); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + return &net.IPNet{IP: ip4, Mask: net.CIDRMask(32, 32)}, nil + } + + return &net.IPNet{IP: ip.To16(), Mask: net.CIDRMask(128, 128)}, nil + } + + return nil, errors.New("must be a CIDR (e.g. 10.0.0.0/8) or an IP address, hostnames are not supported") +} + func LoadEdgeConfigFromMap(configMap map[interface{}]interface{}) (*EdgeConfig, error) { edgeConfig := NewEdgeConfig() @@ -748,6 +1080,10 @@ func LoadEdgeConfigFromMap(configMap map[interface{}]interface{}) (*EdgeConfig, return nil, err } + if err = edgeConfig.loadExternalJwtSignersSection(edgeConfigMap); err != nil { + return nil, err + } + if v, ok := edgeConfigMap["disablePostureChecks"]; ok { if boolVal, ok := v.(bool); ok { edgeConfig.DisablePostureChecks = boolVal diff --git a/controller/config/config_edge_test.go b/controller/config/config_edge_test.go index b52fcd7af..9972c6085 100644 --- a/controller/config/config_edge_test.go +++ b/controller/config/config_edge_test.go @@ -362,6 +362,261 @@ func Test_CalculateCaPems(t *testing.T) { } +func Test_loadExternalJwtSignersSection(t *testing.T) { + t.Run("an absent section yields defaults", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{})) + + req.False(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + req.Empty(c.ExternalJwtSigners.JwksFetch.DeniedIPs) + req.Empty(c.ExternalJwtSigners.JwksFetch.AllowedIPs) + req.Empty(c.ExternalJwtSigners.JwksFetch.DeniedHostnames) + req.Empty(c.ExternalJwtSigners.JwksFetch.AllowedHostnames) + req.Equal(DefaultJwksFetchTimeout, c.ExternalJwtSigners.JwksFetch.Timeout) + req.Equal(DefaultJwksFetchMaxRedirects, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("an absent jwksFetch sub-section yields defaults", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{}, + })) + + req.False(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + req.Equal(DefaultJwksFetchTimeout, c.ExternalJwtSigners.JwksFetch.Timeout) + req.Equal(DefaultJwksFetchMaxRedirects, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("a fully specified section is parsed", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "blockPrivateAddresses": true, + "deniedIPs": []any{"10.0.0.0/8", "203.0.113.5"}, + "allowedIPs": []any{"192.168.10.0/24", "fd00:1234::1"}, + "timeout": "12s", + "maxRedirects": 2, + }, + }, + })) + + jwksFetch := c.ExternalJwtSigners.JwksFetch + + req.True(jwksFetch.BlockPrivateAddresses) + req.Equal(12*time.Second, jwksFetch.Timeout) + req.Equal(2, jwksFetch.MaxRedirects) + + req.Len(jwksFetch.DeniedIPs, 2) + req.Equal("10.0.0.0/8", jwksFetch.DeniedIPs[0].String()) + req.Equal("203.0.113.5/32", jwksFetch.DeniedIPs[1].String(), "a bare IPv4 address should be treated as a /32") + + req.Len(jwksFetch.AllowedIPs, 2) + req.Equal("192.168.10.0/24", jwksFetch.AllowedIPs[0].String()) + req.Equal("fd00:1234::1/128", jwksFetch.AllowedIPs[1].String(), "a bare IPv6 address should be treated as a /128") + }) + + t.Run("host lists are parsed and normalized", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedHostnames": []any{"Blocked.Example.COM", "internal.example.com."}, + "allowedHostnames": []any{"idp.example.com", "*.idp.example.org"}, + }, + }, + })) + + jwksFetch := c.ExternalJwtSigners.JwksFetch + + req.Equal([]string{"blocked.example.com", "internal.example.com"}, jwksFetch.DeniedHostnames, + "host entries should be lower-cased with any trailing dot removed") + req.Equal([]string{"idp.example.com", "*.idp.example.org"}, jwksFetch.AllowedHostnames) + }) + + t.Run("a host list entry that is an IP address is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedHostnames": []any{"10.0.0.5"}, + }, + }, + }) + + req.Error(err, "an IP address in a host list would be a name comparison, not an address check") + req.Contains(err.Error(), "deniedIPs") + }) + + t.Run("a host list entry with a scheme, port or path is an error", func(t *testing.T) { + entries := []string{"https://idp.example.com", "idp.example.com:443", "idp.example.com/jwks", "idp.*.example.com", "*.", ""} + + for _, entry := range entries { + t.Run(entry, func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "allowedHostnames": []any{entry}, + }, + }, + }) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + } + }) + + t.Run("blockPrivateAddresses accepts a string boolean", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "blockPrivateAddresses": "true", + }, + }, + })) + + req.True(c.ExternalJwtSigners.JwksFetch.BlockPrivateAddresses) + }) + + t.Run("maxRedirects of zero is allowed and disables redirects", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + req.NoError(c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "maxRedirects": 0, + }, + }, + })) + + req.Equal(0, c.ExternalJwtSigners.JwksFetch.MaxRedirects) + }) + + t.Run("an invalid CIDR is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedIPs": []any{"not-an-address"}, + }, + }, + }) + + req.Error(err) + req.Contains(err.Error(), "deniedIPs") + }) + + t.Run("a hostname entry is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "allowedIPs": []any{"idp.example.com"}, + }, + }, + }) + + req.Error(err, "hostnames are not valid entries, they cannot be enforced at dial time") + req.Contains(err.Error(), "allowedIPs") + }) + + t.Run("a non-list address value is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "deniedIPs": "10.0.0.0/8", + }, + }, + }) + + req.Error(err) + }) + + t.Run("an invalid duration is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "timeout": "not-a-duration", + }, + }, + }) + + req.Error(err) + }) + + t.Run("a non-positive timeout is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "timeout": "0s", + }, + }, + }) + + req.Error(err, "an unbounded fetch is exactly what this section exists to prevent") + }) + + t.Run("a negative maxRedirects is an error", func(t *testing.T) { + req := require.New(t) + + c := NewEdgeConfig() + + err := c.loadExternalJwtSignersSection(map[any]any{ + "externalJwtSigners": map[any]any{ + "jwksFetch": map[any]any{ + "maxRedirects": -1, + }, + }, + }) + + req.Error(err) + }) +} + func newSelfSignedCert(commonName string, isCas bool) (*x509.Certificate, crypto.PrivateKey) { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { diff --git a/controller/model/external_jwt_signer_manager.go b/controller/model/external_jwt_signer_manager.go index 386cdc5e5..ccc5bbef6 100644 --- a/controller/model/external_jwt_signer_manager.go +++ b/controller/model/external_jwt_signer_manager.go @@ -17,14 +17,18 @@ package model import ( - "github.com/openziti/ziti/v2/controller/storage/ast" - "github.com/openziti/ziti/v2/controller/storage/boltz" + "strings" + + "github.com/openziti/foundation/v2/errorz" "github.com/openziti/ziti/v2/common/pb/edge_cmd_pb" + "github.com/openziti/ziti/v2/controller/apierror" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/ast" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/pkg/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -33,6 +37,7 @@ import ( func NewExternalJwtSignerManager(env Env) *ExternalJwtSignerManager { manager := &ExternalJwtSignerManager{ baseEntityManager: newBaseEntityManager[*ExternalJwtSigner, *db.ExternalJwtSigner](env, env.GetStores().ExternalJwtSigner), + jwksFetchPolicy: NewJwksFetchPolicy(JwksFetchConfig(env)), } manager.impl = manager @@ -43,6 +48,10 @@ func NewExternalJwtSignerManager(env Env) *ExternalJwtSignerManager { type ExternalJwtSignerManager struct { baseEntityManager[*ExternalJwtSigner, *db.ExternalJwtSigner] + + // jwksFetchPolicy is used to reject a jwksEndpoint at create/update time that could never + // be fetched. The same policy governs the fetch itself. + jwksFetchPolicy *JwksFetchPolicy } func (self *ExternalJwtSignerManager) NewModelEntity() *ExternalJwtSigner { @@ -50,6 +59,10 @@ func (self *ExternalJwtSignerManager) NewModelEntity() *ExternalJwtSigner { } func (self *ExternalJwtSignerManager) Create(entity *ExternalJwtSigner, ctx *change.Context) error { + if err := self.validateJwksEndpoint(entity); err != nil { + return err + } + return DispatchCreate[*ExternalJwtSigner](self, entity, ctx) } @@ -59,9 +72,29 @@ func (self *ExternalJwtSignerManager) ApplyCreate(cmd *command.CreateEntityComma } func (self *ExternalJwtSignerManager) Update(entity *ExternalJwtSigner, checker fields.UpdatedFields, ctx *change.Context) error { + if err := self.validateJwksEndpoint(entity); err != nil { + return err + } + return DispatchUpdate[*ExternalJwtSigner](self, entity, checker, ctx) } +// validateJwksEndpoint rejects a jwksEndpoint that the configured jwks fetch policy could +// never fetch, so the operator finds out on create/update rather than on a failed fetch. An +// endpoint that passes here can still be refused at fetch time, which is where the +// authoritative check lives. +func (self *ExternalJwtSignerManager) validateJwksEndpoint(entity *ExternalJwtSigner) error { + if entity.JwksEndpoint == nil || strings.TrimSpace(*entity.JwksEndpoint) == "" { + return nil + } + + if err := self.jwksFetchPolicy.ValidateEndpoint(*entity.JwksEndpoint); err != nil { + return apierror.NewBadRequestFieldError(*errorz.NewFieldError(err.Error(), db.FieldExternalJwtSignerJwksEndpoint, *entity.JwksEndpoint)) + } + + return nil +} + func (self *ExternalJwtSignerManager) ApplyUpdate(cmd *command.UpdateEntityCommand[*ExternalJwtSigner], ctx boltz.MutateContext) error { return self.updateEntity(cmd.Entity, cmd.UpdatedFields, ctx) } diff --git a/controller/model/jwks_resolver.go b/controller/model/jwks_resolver.go new file mode 100644 index 000000000..048b90210 --- /dev/null +++ b/controller/model/jwks_resolver.go @@ -0,0 +1,392 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "syscall" + + "github.com/openziti/jwks" + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/db" +) + +// builtInBlockedCidrs are always refused for a JWKS fetch and cannot be re-enabled by +// configuration. They are the addresses that turn the controller's network position into a +// credential oracle, plus the addresses that have no meaning as an IdP endpoint. +var builtInBlockedCidrs = []string{ + "169.254.0.0/16", // IPv4 link-local, includes cloud instance metadata (169.254.169.254) and ECS task metadata (169.254.170.2) + "fe80::/10", // IPv6 link-local + "fd00:ec2::254/128", // AWS instance metadata over IPv6, inside unique-local space + "224.0.0.0/24", // IPv4 link-local multicast + "ff02::/16", // IPv6 link-local multicast + "0.0.0.0/32", // IPv4 unspecified + "::/128", // IPv6 unspecified +} + +// JwksFetchConfig returns the [edge.externalJwtSigners.jwksFetch] settings from the +// environment, falling back to the defaults when no edge configuration is present. +func JwksFetchConfig(env Env) config.JwksFetch { + if cfg := env.GetConfig(); cfg != nil && cfg.Edge != nil { + return cfg.Edge.ExternalJwtSigners.JwksFetch + } + + return config.DefaultJwksFetch() +} + +// JwksFetchPolicy decides whether the controller may fetch a given JWKS endpoint. The +// endpoint URL is operator-supplied, so without this the controller's network position would +// be reachable by whoever can write an external JWT signer. +// +// A hop is fetched only if it passes both gates, and both gates are applied to the initial +// request and to every redirect. Neither gate can authorize what the other refuses: host +// matching only ever narrows what may be fetched. +// +// CheckHostname is the hostname gate, applied to the URL's hostname: +// +// 1. deniedHostnames - blocked +// 2. allowedHostnames - when non-empty and the host does not match, blocked +// 3. anything else - passes +// +// CheckIP is the address gate, applied to the address being connected to, first-match-wins, +// deny before allow: +// +// 1. builtInBlockedCidrs - blocked, not overridable +// 2. deniedIPs - blocked, not overridable by allowedIPs +// 3. allowedIPs - allowed, a carve-out of tier 4 only +// 4. blockPrivateAddresses and the address is private or loopback - blocked +// 5. anything else - allowed +type JwksFetchPolicy struct { + blockPrivateAddresses bool + builtInBlocked []*net.IPNet + deniedIPs []*net.IPNet + allowedIPs []*net.IPNet + deniedHostnames []string + allowedHostnames []string +} + +// NewJwksFetchPolicy returns a JwksFetchPolicy built from the [edge.externalJwtSigners.jwksFetch] +// configuration section. +func NewJwksFetchPolicy(cfg config.JwksFetch) *JwksFetchPolicy { + result := &JwksFetchPolicy{ + blockPrivateAddresses: cfg.BlockPrivateAddresses, + deniedIPs: cfg.DeniedIPs, + allowedIPs: cfg.AllowedIPs, + deniedHostnames: cfg.DeniedHostnames, + allowedHostnames: cfg.AllowedHostnames, + } + + for _, cidr := range builtInBlockedCidrs { + _, ipNet, err := net.ParseCIDR(cidr) + + if err != nil { + // builtInBlockedCidrs is a compile-time constant list, a parse failure is a bug + panic(fmt.Errorf("invalid built-in blocked CIDR %s: %w", cidr, err)) + } + + result.builtInBlocked = append(result.builtInBlocked, ipNet) + } + + return result +} + +// CheckHostname returns nil if the controller may fetch from the given URL hostname, and an +// error otherwise. It is applied to the initial endpoint and to every redirect hop. +// +// Hostname matching narrows only. A hostname that passes here is still subject to CheckIP, and +// a caller may be able to reach the same target under a different name, so this is a filter on +// top of the address gate rather than a boundary of its own. +func (self *JwksFetchPolicy) CheckHostname(hostname string) error { + normalized := config.NormalizeHostname(hostname) + + if normalized == "" { + return fmt.Errorf("jwks endpoint url must include a hostname") + } + + if matchesHostname(self.deniedHostnames, normalized) { + return fmt.Errorf("jwks endpoint hostname %s is not permitted, it matches [edge.externalJwtSigners.jwksFetch.deniedHostnames]", normalized) + } + + if len(self.allowedHostnames) > 0 && !matchesHostname(self.allowedHostnames, normalized) { + return fmt.Errorf("jwks endpoint hostname %s is not permitted, [edge.externalJwtSigners.jwksFetch.allowedHostnames] is set and does not include it", normalized) + } + + return nil +} + +// matchesHostname reports whether a normalized hostname matches any of the given normalized +// patterns. A pattern is either an exact hostname, or a "*.suffix" wildcard that matches any +// subdomain of suffix at any depth but never suffix itself: "*.sub.host.com" matches +// "idp.sub.host.com" and "a.b.sub.host.com", but not "sub.host.com". +func matchesHostname(patterns []string, hostname string) bool { + for _, pattern := range patterns { + if suffix, isWildcard := strings.CutPrefix(pattern, "*."); isWildcard { + if strings.HasSuffix(hostname, "."+suffix) { + return true + } + + continue + } + + if hostname == pattern { + return true + } + } + + return false +} + +// CheckIP returns nil if the controller may connect to the given address for a JWKS fetch, +// and an error describing which tier refused it otherwise. An address that cannot be +// classified is refused. +func (self *JwksFetchPolicy) CheckIP(ip net.IP) error { + if ip == nil { + return fmt.Errorf("jwks endpoint address could not be parsed") + } + + // classify IPv4 and IPv4-mapped IPv6 addresses identically + if ip4 := ip.To4(); ip4 != nil { + ip = ip4 + } + + if containsIP(self.builtInBlocked, ip) { + return fmt.Errorf("jwks endpoint address %s is not permitted, it is a metadata, link-local or unspecified address", ip) + } + + if containsIP(self.deniedIPs, ip) { + return fmt.Errorf("jwks endpoint address %s is not permitted, it matches [edge.externalJwtSigners.jwksFetch.deniedIPs]", ip) + } + + if containsIP(self.allowedIPs, ip) { + return nil + } + + if self.blockPrivateAddresses && (ip.IsPrivate() || ip.IsLoopback()) { + return fmt.Errorf("jwks endpoint address %s is not permitted, private and loopback addresses are blocked by [edge.externalJwtSigners.jwksFetch.blockPrivateAddresses]", ip) + } + + return nil +} + +// ValidateEndpoint checks an operator-supplied jwksEndpoint URL at create/update time so an +// obviously unusable endpoint is reported immediately instead of failing later during a +// fetch. It rejects a scheme other than http/https, a missing host, and a host that is a +// literal blocked IP address. +// +// This is advisory: no name resolution happens here, so a hostname that resolves to a blocked +// address passes this check and is refused by the dialer at fetch time. The dial-time check +// remains the authoritative one. +func (self *JwksFetchPolicy) ValidateEndpoint(endpoint string) error { + target, err := url.Parse(strings.TrimSpace(endpoint)) + + if err != nil { + return fmt.Errorf("could not parse jwks endpoint url: %w", err) + } + + if err = validateJwksEndpointScheme(target); err != nil { + return err + } + + host := target.Hostname() + + if err = self.CheckHostname(host); err != nil { + return err + } + + if ip := net.ParseIP(host); ip != nil { + return self.CheckIP(ip) + } + + return nil +} + +// checkJwksEndpointAllowed returns an error when a signer's jwksEndpoint is one the given +// policy refuses. A signer with no jwksEndpoint, or a blank one, is not reported. +func checkJwksEndpointAllowed(policy *JwksFetchPolicy, signer *db.ExternalJwtSigner) error { + if signer.JwksEndpoint == nil || strings.TrimSpace(*signer.JwksEndpoint) == "" { + return nil + } + + return policy.ValidateEndpoint(*signer.JwksEndpoint) +} + +// CheckDialAddress applies CheckIP to a host:port address as it is about to be dialed. The +// address has already been resolved at that point, which is what makes the check +// rebinding-safe: the address that is connected to is the address that is checked. +func (self *JwksFetchPolicy) CheckDialAddress(address string) error { + host, _, err := net.SplitHostPort(address) + + if err != nil { + return fmt.Errorf("could not parse jwks endpoint dial address %s: %w", address, err) + } + + return self.CheckIP(net.ParseIP(host)) +} + +// containsIP reports whether any of the given networks contains the given address. +func containsIP(ipNets []*net.IPNet, ip net.IP) bool { + for _, ipNet := range ipNets { + if ipNet.Contains(ip) { + return true + } + } + + return false +} + +var _ jwks.Resolver = (*HardenedJwksResolver)(nil) + +// HardenedJwksResolver fetches JWKS responses over HTTP(S) with the constraints an +// operator-supplied URL requires: a bounded total time, a bounded number of redirects, an +// http/https-only scheme, and a JwksFetchPolicy check of every address that is connected to. +// +// The address check is installed as the dialer's control function, so it is the single choke +// point for the first request and for every redirect hop: it runs after DNS resolution and +// before the connection is made, which is what a check of the URL's hostname cannot do. +type HardenedJwksResolver struct { + client *http.Client + policy *JwksFetchPolicy +} + +// NewHardenedJwksResolver returns a HardenedJwksResolver configured from the +// [edge.externalJwtSigners.jwksFetch] configuration section. +func NewHardenedJwksResolver(cfg config.JwksFetch) *HardenedJwksResolver { + if cfg.Timeout <= 0 { + cfg.Timeout = config.DefaultJwksFetchTimeout + } + + if cfg.MaxRedirects < 0 { + cfg.MaxRedirects = config.DefaultJwksFetchMaxRedirects + } + + policy := NewJwksFetchPolicy(cfg) + + dialer := &net.Dialer{ + Timeout: cfg.Timeout, + Control: func(_ string, address string, _ syscall.RawConn) error { + return policy.CheckDialAddress(address) + }, + } + + transport := &http.Transport{ + DialContext: dialer.DialContext, + TLSHandshakeTimeout: cfg.Timeout, + // JWKS fetches are infrequent, so take a fresh, policy-checked connection every time + // rather than reusing one + DisableKeepAlives: true, + } + + maxRedirects := cfg.MaxRedirects + + client := &http.Client{ + Timeout: cfg.Timeout, + Transport: transport, + CheckRedirect: func(request *http.Request, via []*http.Request) error { + if len(via) > maxRedirects { + return fmt.Errorf("jwks endpoint exceeded the maximum of %d redirect(s)", maxRedirects) + } + + if err := validateJwksEndpointScheme(request.URL); err != nil { + return err + } + + // every hop is checked on its own, an allowed first hop does not carry over + return policy.CheckHostname(request.URL.Hostname()) + }, + } + + return &HardenedJwksResolver{ + client: client, + policy: policy, + } +} + +// Get implements jwks.Resolver. It returns the parsed JWKS response and the raw response body. +func (self *HardenedJwksResolver) Get(endpoint string) (*jwks.Response, []byte, error) { + target, err := url.Parse(strings.TrimSpace(endpoint)) + + if err != nil { + return nil, nil, fmt.Errorf("could not parse jwks endpoint url: %w", err) + } + + if err = validateJwksEndpointScheme(target); err != nil { + return nil, nil, err + } + + if err = self.policy.CheckHostname(target.Hostname()); err != nil { + return nil, nil, err + } + + request, err := http.NewRequest(http.MethodGet, target.String(), nil) + + if err != nil { + return nil, nil, fmt.Errorf("could not create jwks endpoint request: %w", err) + } + + request.Header.Set("accept", "application/json") + + response, err := self.client.Do(request) + + if err != nil { + return nil, nil, err + } + + defer func() { + _ = response.Body.Close() + }() + + if response.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("could not fetch JWKS, status code was not 200 OK, got %d", response.StatusCode) + } + + contentType := strings.ToLower(strings.TrimSpace(strings.Split(response.Header.Get("content-type"), ";")[0])) + + if contentType != "application/json" && contentType != "application/jwk-set+json" && contentType != "application/jwk+json" { + return nil, nil, fmt.Errorf("invalid content type %s, expected application/json", contentType) + } + + body, err := io.ReadAll(response.Body) + + if err != nil { + return nil, nil, fmt.Errorf("could not read jwks response: %w", err) + } + + jwksResponse := &jwks.Response{} + + if err = json.Unmarshal(body, jwksResponse); err != nil { + return nil, nil, fmt.Errorf("could not parse jwks response: %w", err) + } + + return jwksResponse, body, nil +} + +// validateJwksEndpointScheme allows only http and https. Anything else either cannot be +// fetched or would let the URL scheme choose the transport. +func validateJwksEndpointScheme(target *url.URL) error { + switch strings.ToLower(target.Scheme) { + case "http", "https": + return nil + } + + return fmt.Errorf("invalid jwks endpoint scheme %s, only http and https are supported", target.Scheme) +} diff --git a/controller/model/jwks_resolver_test.go b/controller/model/jwks_resolver_test.go new file mode 100644 index 000000000..57c73565b --- /dev/null +++ b/controller/model/jwks_resolver_test.go @@ -0,0 +1,848 @@ +package model + +import ( + "net" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/db" + "github.com/stretchr/testify/require" +) + +func Test_JwksFetchPolicy_CheckIP(t *testing.T) { + t.Run("with default settings", func(t *testing.T) { + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + t.Run("built-in blocked addresses are blocked", func(t *testing.T) { + blocked := []string{ + "169.254.169.254", // cloud instance metadata + "169.254.170.2", // ECS task metadata + "fd00:ec2::254", // AWS IMDS over IPv6 + "169.254.10.10", // link-local + "fe80::1", // link-local + "224.0.0.1", // link-local multicast + "ff02::1", // link-local multicast + "0.0.0.0", // unspecified + "::", // unspecified + } + + for _, address := range blocked { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + err := policy.CheckIP(net.ParseIP(address)) + + req.Error(err, "%s must be blocked by the built-in tier", address) + }) + } + }) + + t.Run("private and loopback addresses are allowed, the default posture is compatible", func(t *testing.T) { + allowed := []string{"127.0.0.1", "::1", "10.1.2.3", "172.16.0.1", "192.168.1.1", "fc00::1"} + + for _, address := range allowed { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP(address))) + }) + } + }) + + t.Run("a public address is allowed", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP("93.184.216.34"))) + }) + }) + + t.Run("with blockPrivateAddresses enabled", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("private and loopback addresses are blocked", func(t *testing.T) { + blocked := []string{"127.0.0.1", "::1", "10.1.2.3", "172.16.0.1", "192.168.1.1", "fc00::1"} + + for _, address := range blocked { + t.Run(address, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckIP(net.ParseIP(address))) + }) + } + }) + + t.Run("a public address is still allowed", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckIP(net.ParseIP("93.184.216.34"))) + }) + }) + + t.Run("deniedIPs blocks an otherwise allowed public address", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "203.0.113.0/24") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("203.0.113.5"))) + req.NoError(policy.CheckIP(net.ParseIP("203.0.114.5"))) + }) + + t.Run("allowedIPs carves an exception out of blockPrivateAddresses", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckIP(net.ParseIP("10.1.2.3")), "the carve-out address must be reachable") + req.Error(policy.CheckIP(net.ParseIP("10.2.2.3")), "a private address outside the carve-out must stay blocked") + }) + + t.Run("allowedIPs cannot override the built-in blocked tier", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedIPs = mustCidrs(t, "169.254.0.0/16", "fd00:ec2::/64") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("169.254.169.254")), "the metadata service must never be reachable") + req.Error(policy.CheckIP(net.ParseIP("fd00:ec2::254")), "the IPv6 metadata service must never be reachable") + }) + + t.Run("allowedIPs cannot override deniedIPs", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "10.0.0.0/8") + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.2.3/32") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("10.1.2.3")), "deny wins over allow") + }) + + t.Run("an IPv4-mapped IPv6 address is classified as its IPv4 address", func(t *testing.T) { + t.Run("a mapped metadata address is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:169.254.169.254"))) + }) + + t.Run("a mapped private address is blocked when blockPrivateAddresses is set", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:10.1.2.3"))) + }) + + t.Run("a mapped address matches an IPv4 allowedIPs carve-out", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + jwksFetch.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckIP(net.ParseIP("::ffff:10.1.2.3"))) + }) + + t.Run("a mapped address matches an IPv4 deniedIPs entry", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedIPs = mustCidrs(t, "203.0.113.0/24") + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckIP(net.ParseIP("::ffff:203.0.113.5"))) + }) + }) + + t.Run("an unparsable address is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckIP(nil), "an address that could not be parsed must never be treated as allowed") + }) +} + +func Test_JwksFetchPolicy_CheckHostname(t *testing.T) { + t.Run("with no host lists configured every host passes", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.NoError(policy.CheckHostname("10.0.0.5")) + }) + + t.Run("an empty host is blocked", func(t *testing.T) { + req := require.New(t) + + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + req.Error(policy.CheckHostname("")) + }) + + t.Run("deniedHostnames blocks a matching host", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"blocked.example.com", "*.internal.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("an exact match is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("blocked.example.com")) + }) + + t.Run("a wildcard suffix match is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("idp.internal.example.com")) + req.Error(policy.CheckHostname("deep.idp.internal.example.com")) + }) + + t.Run("the wildcard suffix itself is not matched", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("internal.example.com"), "*.internal.example.com covers subdomains only") + }) + + t.Run("a wildcard matches whole labels only", func(t *testing.T) { + req := require.New(t) + + // xinternal.example.com ends with internal.example.com as a string, but the + // wildcard replaces a whole label, so it is a different host + req.NoError(policy.CheckHostname("xinternal.example.com")) + req.NoError(policy.CheckHostname("notinternal.example.com")) + }) + + t.Run("an unlisted host passes", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("idp.example.com")) + }) + + t.Run("matching is case insensitive and ignores a trailing dot", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("BLOCKED.example.com")) + req.Error(policy.CheckHostname("blocked.example.com.")) + }) + }) + + t.Run("allowedHostnames is exclusive when set", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com", "*.idp.example.org"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a listed host passes", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.NoError(policy.CheckHostname("eu.idp.example.org")) + }) + + t.Run("an unlisted host is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("evil.example.com")) + }) + + t.Run("a literal IP host is blocked", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.CheckHostname("93.184.216.34"), "an allowedHostnames list can only be satisfied by a name") + }) + }) + + t.Run("deniedHostnames wins over allowedHostnames", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"old.idp.example.com"} + jwksFetch.AllowedHostnames = []string{"*.idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + req.Error(policy.CheckHostname("old.idp.example.com")) + req.NoError(policy.CheckHostname("new.idp.example.com")) + }) + + t.Run("an allowed host does not authorize a blocked address", func(t *testing.T) { + req := require.New(t) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + req.NoError(policy.CheckHostname("idp.example.com")) + req.Error(policy.CheckIP(net.ParseIP("169.254.169.254")), "the hostname gate must never widen the address gate") + }) +} + +func Test_JwksFetchPolicy_ValidateEndpoint(t *testing.T) { + t.Run("with default settings", func(t *testing.T) { + policy := NewJwksFetchPolicy(config.DefaultJwksFetch()) + + t.Run("an http or https endpoint is accepted", func(t *testing.T) { + endpoints := []string{ + "https://idp.example.com/.well-known/jwks.json", + "http://idp.example.com/.well-known/jwks.json", + "HTTPS://idp.example.com/jwks", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("a non-http scheme is rejected", func(t *testing.T) { + endpoints := []string{"file:///etc/passwd", "ftp://idp.example.com/jwks", "idp.example.com/jwks", ""} + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("an endpoint without a host is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("http:///jwks")) + }) + + t.Run("a literal metadata address is rejected", func(t *testing.T) { + endpoints := []string{ + "http://169.254.169.254/latest/meta-data/", + "http://169.254.170.2/v2/credentials", + "http://[fd00:ec2::254]/latest/meta-data/", + "http://169.254.169.254:8080/jwks", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint(endpoint)) + }) + } + }) + + t.Run("a literal private address is accepted, the default posture allows it", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint("https://10.1.2.3/jwks")) + }) + }) + + t.Run("with blockPrivateAddresses enabled", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a literal private address is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://10.1.2.3/jwks")) + }) + + t.Run("a hostname is accepted, the dial-time check remains authoritative", func(t *testing.T) { + req := require.New(t) + + // no DNS resolution happens at create/update time, so a hostname that resolves to + // a blocked address is caught by the dialer instead + req.NoError(policy.ValidateEndpoint("https://localhost/jwks")) + }) + + t.Run("a literal private address in allowedIPs is accepted", func(t *testing.T) { + req := require.New(t) + + carveOut := config.DefaultJwksFetch() + carveOut.BlockPrivateAddresses = true + carveOut.AllowedIPs = mustCidrs(t, "10.1.0.0/16") + + req.NoError(NewJwksFetchPolicy(carveOut).ValidateEndpoint("https://10.1.2.3/jwks")) + }) + }) + + t.Run("with host lists configured", func(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"old.idp.example.com"} + jwksFetch.AllowedHostnames = []string{"idp.example.com", "*.idp.example.org"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("an allowed host is accepted", func(t *testing.T) { + req := require.New(t) + + req.NoError(policy.ValidateEndpoint("https://idp.example.com/.well-known/jwks.json")) + req.NoError(policy.ValidateEndpoint("https://eu.idp.example.org/jwks")) + }) + + t.Run("a host outside allowedHostnames is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://other.example.com/jwks")) + }) + + t.Run("a denied host is rejected", func(t *testing.T) { + req := require.New(t) + + req.Error(policy.ValidateEndpoint("https://old.idp.example.com/jwks")) + }) + }) +} + +func Test_checkJwksEndpointAllowed(t *testing.T) { + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + policy := NewJwksFetchPolicy(jwksFetch) + + t.Run("a signer without a jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{})) + }) + + t.Run("a signer with a blank jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + endpoint := " " + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) + + t.Run("a signer with an allowed jwks endpoint is not reported", func(t *testing.T) { + req := require.New(t) + + endpoint := "https://idp.example.com/jwks" + + req.NoError(checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) + + t.Run("a signer whose jwks endpoint the configuration now refuses is reported", func(t *testing.T) { + req := require.New(t) + + // the signer was created before the host list was set, so it is orphaned by the + // current configuration and the operator needs to know at startup + endpoint := "https://old-idp.example.com/jwks" + + err := checkJwksEndpointAllowed(policy, &db.ExternalJwtSigner{JwksEndpoint: &endpoint}) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + + t.Run("a signer whose jwks endpoint is a blocked address is reported", func(t *testing.T) { + req := require.New(t) + + endpoint := "http://169.254.169.254/latest/meta-data/" + + req.Error(checkJwksEndpointAllowed(NewJwksFetchPolicy(config.DefaultJwksFetch()), + &db.ExternalJwtSigner{JwksEndpoint: &endpoint})) + }) +} + +func Test_HardenedJwksResolver_Get(t *testing.T) { + const jwksBody = `{"keys":[{"kty":"RSA","kid":"test-kid","n":"AQAB","e":"AQAB"}]}` + + t.Run("a valid JWKS endpoint resolves", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + response, body, err := resolver.Get(server.URL + "/jwks") + + req.NoError(err) + req.Len(response.Keys, 1) + req.Equal("test-kid", response.Keys[0].KeyId) + req.Equal(jwksBody, string(body)) + }) + + t.Run("a blocked address is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/jwks") + + req.Error(err) + req.Contains(err.Error(), "is not permitted") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a hostname that resolves to a blocked address is refused at dial time", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + // the URL host is a name, so only the resolved address can be checked - this is the + // DNS rebinding case, and it is why enforcement lives in the dialer + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.BlockPrivateAddresses = true + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "is not permitted") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a redirect to a blocked address is refused", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/redirect-to-metadata") + + req.Error(err, "the metadata address must be blocked on a redirect hop, not just on the first hop") + req.Contains(err.Error(), "is not permitted", "the redirect hop must be refused by the address policy") + }) + + t.Run("a host that is not in allowedHostnames is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"idp.example.com"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + req.Zero(server.requestCount(), "the request must be stopped before it reaches the endpoint") + }) + + t.Run("a host in deniedHostnames is refused without contacting the endpoint", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.DeniedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err = resolver.Get(endpoint) + + req.Error(err) + req.Contains(err.Error(), "deniedHostnames") + req.Zero(server.requestCount()) + }) + + t.Run("a host in allowedHostnames is fetched", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + response, _, err := resolver.Get(endpoint) + + req.NoError(err) + req.Len(response.Keys, 1) + }) + + t.Run("a redirect to a host that is not in allowedHostnames is refused", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + endpoint, err := server.urlWithHost("localhost") + req.NoError(err) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.AllowedHostnames = []string{"localhost"} + + resolver := NewHardenedJwksResolver(jwksFetch) + + // the first hop is an allowed host, the redirect target is not - each hop is checked + // on its own + _, _, err = resolver.Get(strings.Replace(endpoint, "/jwks", "/redirect-to-unlisted-host", 1)) + + req.Error(err) + req.Contains(err.Error(), "allowedHostnames") + }) + + t.Run("a redirect within the cap is followed", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + response, _, err := resolver.Get(server.URL + "/redirect-to-jwks") + + req.NoError(err) + req.Len(response.Keys, 1) + }) + + t.Run("exceeding maxRedirects is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.MaxRedirects = 2 + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/redirect-loop") + + req.Error(err) + req.Contains(err.Error(), "redirect") + }) + + t.Run("maxRedirects of zero refuses to follow any redirect", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.MaxRedirects = 0 + + resolver := NewHardenedJwksResolver(jwksFetch) + + _, _, err := resolver.Get(server.URL + "/redirect-to-jwks") + + req.Error(err) + req.Contains(err.Error(), "redirect") + }) + + t.Run("a slow endpoint is cut off at the timeout", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + jwksFetch := config.DefaultJwksFetch() + jwksFetch.Timeout = 100 * time.Millisecond + + resolver := NewHardenedJwksResolver(jwksFetch) + + start := time.Now() + _, _, err := resolver.Get(server.URL + "/hang") + elapsed := time.Since(start) + + req.Error(err) + req.Less(elapsed, 10*time.Second, "the fetch must be bounded by the configured timeout") + }) + + t.Run("a non-http scheme is refused", func(t *testing.T) { + endpoints := []string{ + "file:///etc/passwd", + "ftp://example.com/jwks", + "gopher://example.com:70/jwks", + "/no/scheme/at/all", + } + + for _, endpoint := range endpoints { + t.Run(endpoint, func(t *testing.T) { + req := require.New(t) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(endpoint) + + req.Error(err) + }) + } + }) + + t.Run("an unparsable url is refused", func(t *testing.T) { + req := require.New(t) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get("http://[::1") + + req.Error(err) + }) + + t.Run("a non-200 status is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/not-found") + + req.Error(err) + }) + + t.Run("a non-json content type is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/html") + + req.Error(err) + }) + + t.Run("a body that is not a JWKS response is an error", func(t *testing.T) { + req := require.New(t) + + server := newTestJwksServer(t, jwksBody) + + resolver := NewHardenedJwksResolver(config.DefaultJwksFetch()) + + _, _, err := resolver.Get(server.URL + "/not-json") + + req.Error(err) + }) +} + +// testJwksServer is a local endpoint that serves the routes the resolver tests exercise and +// counts the requests that reach it. +type testJwksServer struct { + *httptest.Server + requests atomic.Int32 +} + +// requestCount returns how many requests reached the server. +func (self *testJwksServer) requestCount() int { + return int(self.requests.Load()) +} + +// urlWithHost returns the server's URL with its host replaced, keeping the port. +func (self *testJwksServer) urlWithHost(host string) (string, error) { + parsed, err := url.Parse(self.URL) + + if err != nil { + return "", err + } + + parsed.Host = net.JoinHostPort(host, parsed.Port()) + + return parsed.String() + "/jwks", nil +} + +// newTestJwksServer starts a JWKS endpoint for the duration of the test. +func newTestJwksServer(t *testing.T, jwksBody string) *testJwksServer { + result := &testJwksServer{} + + // released on test cleanup so the /hang route does not outlive the test + done := make(chan struct{}) + + result.Server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + result.requests.Add(1) + + switch request.URL.Path { + case "/jwks": + writer.Header().Set("content-type", "application/json") + _, _ = writer.Write([]byte(jwksBody)) + case "/redirect-to-jwks": + http.Redirect(writer, request, "/jwks", http.StatusFound) + case "/redirect-loop": + http.Redirect(writer, request, "/redirect-loop", http.StatusFound) + case "/redirect-to-metadata": + http.Redirect(writer, request, "http://169.254.169.254/latest/meta-data/", http.StatusFound) + case "/redirect-to-unlisted-host": + http.Redirect(writer, request, "http://unlisted.example.com/jwks", http.StatusFound) + case "/hang": + <-done + case "/html": + writer.Header().Set("content-type", "text/html") + _, _ = writer.Write([]byte("")) + case "/not-json": + writer.Header().Set("content-type", "application/json") + _, _ = writer.Write([]byte("this is not json")) + default: + writer.WriteHeader(http.StatusNotFound) + } + })) + + t.Cleanup(func() { + close(done) + result.Close() + }) + + return result +} + +// mustCidrs parses CIDRs for test setup, failing the test if any cannot be parsed. +func mustCidrs(t *testing.T, values ...string) []*net.IPNet { + t.Helper() + + var result []*net.IPNet + + for _, value := range values { + _, ipNet, err := net.ParseCIDR(value) + + if err != nil { + t.Fatalf("could not parse test CIDR %s: %v", value, err) + } + + result = append(result, ipNet) + } + + return result +} diff --git a/controller/model/token_provider_cache.go b/controller/model/token_provider_cache.go index c59c33a0e..1dc9f0c54 100644 --- a/controller/model/token_provider_cache.go +++ b/controller/model/token_provider_cache.go @@ -54,6 +54,10 @@ type TokenIssuerCache struct { // due to xweb API address binds controllerIssuers cmap.ConcurrentMap[string, common.TokenIssuer] + // jwksResolver fetches JWKS endpoints for external issuers. It is shared by every + // external issuer so that they all fetch under the same configured constraints. + jwksResolver *HardenedJwksResolver + env Env } @@ -64,6 +68,7 @@ func NewTokenIssuerCache(env Env) *TokenIssuerCache { env: env, externalIssuers: cmap.New[common.TokenIssuer](), controllerIssuers: cmap.New[common.TokenIssuer](), + jwksResolver: NewHardenedJwksResolver(JwksFetchConfig(env)), } env.GetStores().ExternalJwtSigner.AddEntityEventListenerF(result.onExtJwtCreate, boltz.EntityCreatedAsync) @@ -172,7 +177,7 @@ func (a *TokenIssuerCache) onExtJwtCreate(signer *db.ExternalJwtSigner) { signerRec := &TokenIssuerExtJwt{ externalJwtSigner: signer, - jwksResolver: &jwks.HttpResolver{}, + jwksResolver: a.jwksResolver, kidToPubKey: map[string]common.IssuerPublicKey{}, } @@ -219,6 +224,21 @@ func (a *TokenIssuerCache) onExtJwtDelete(signer *db.ExternalJwtSigner) { a.externalIssuers.Remove(*signer.Issuer) } +// reportBlockedJwksEndpoint logs an existing external JWT signer whose jwksEndpoint the current +// [edge.externalJwtSigners.jwksFetch] configuration refuses. A configuration change can orphan a +// signer that was created while its endpoint was still permitted, so this is reported at startup +// rather than only when a fetch is attempted. Endpoints that resolve to a blocked address are not +// visible here, as no name resolution is done; those are reported by the fetch itself. +func (a *TokenIssuerCache) reportBlockedJwksEndpoint(signer *db.ExternalJwtSigner) { + if err := checkJwksEndpointAllowed(a.jwksResolver.policy, signer); err != nil { + pfxlog.Logger().WithFields(map[string]interface{}{ + "id": signer.Id, + "name": signer.Name, + "jwksEndpoint": *signer.JwksEndpoint, + }).WithError(err).Error("external jwt signer jwks endpoint is not permitted by the current jwks fetch configuration, its keys cannot be resolved and authentication with this signer will fail") + } +} + // loadExisting loads all external JWT signers and controllers during initialization. func (a *TokenIssuerCache) loadExisting() { err := a.env.GetDb().View(func(tx *bbolt.Tx) error { @@ -235,6 +255,8 @@ func (a *TokenIssuerCache) loadExisting() { continue } + a.reportBlockedJwksEndpoint(signer) + a.onExtJwtCreate(signer) } diff --git a/etc/ctrl.with.edge.yml b/etc/ctrl.with.edge.yml index 46517e8de..cb0abd0e9 100644 --- a/etc/ctrl.with.edge.yml +++ b/etc/ctrl.with.edge.yml @@ -262,6 +262,141 @@ edge: # database. Only runs on the raft leader. # revocationEnforcerFrequency: 1m + # externalJwtSigners - optional + # Settings that govern how the controller interacts with external JWT signers. + externalJwtSigners: + # jwksFetch - optional + # Controls the controller's fetch of an external JWT signer's jwksEndpoint. That fetch is + # made by the controller, from the controller's own network position, to a URL supplied by + # whoever can write an external JWT signer. These settings constrain where it may go. + # + # A fetch is allowed only if it passes TWO GATES, and both gates are applied to the first + # request AND to every redirect that is followed: + # + # the HOSTNAME gate - checked against the hostname in the URL + # the ADDRESS gate - checked against the resolved address at connection time, so a + # hostname that resolves to a blocked address is still blocked + # + # NEITHER GATE CAN AUTHORIZE WHAT THE OTHER REFUSES. Hostname matching only ever narrows what + # may be fetched: allowedHostnames does not make a blocked address reachable, and + # allowedIPs does not make a blocked hostname reachable. + # + # HOSTNAME gate, first-match-wins. DENY WINS OVER ALLOW: + # + # 1. deniedHostnames - blocked + # 2. allowedHostnames - when set, ONLY these hostnames may be fetched; anything else is + # blocked + # 3. anything else - passes to the address gate + # + # ADDRESS gate, first-match-wins. DENY WINS OVER ALLOW: + # + # 1. built-in blocked addresses - always blocked, allowedIPs CANNOT override: + # cloud instance metadata (169.254.169.254, 169.254.170.2, fd00:ec2::254) + # link-local (169.254.0.0/16, fe80::/10) + # link-local multicast (224.0.0.0/24, ff02::/16) + # unspecified (0.0.0.0, ::) + # 2. deniedIPs - blocked, allowedIPs CANNOT override + # 3. allowedIPs - allowed; this is a carve-out of tier 4 ONLY + # 4. blockPrivateAddresses, and the address is private or loopback - blocked + # 5. anything else - allowed + # + # ENTRY FORMS. The two kinds of list do not accept each other's values. + # + # deniedIPs and allowedIPs take IP addresses only, in either of two forms: + # + # a flat IP address - 192.168.5.5 or fd00:1234::1 + # matches that one address only (the same as /32 or /128) + # a CIDR block - 10.10.0.0/16 or fd00:1234::/64 + # matches every address in the block + # + # Both IPv4 and IPv6 are accepted in either form. A hostname in one of these lists is a + # configuration error and the controller will not start: the address gate runs against the + # address actually being connected to, so there is no hostname there to compare against. + # + # deniedHostnames and allowedHostnames take hostnames only, in either of two forms: + # + # an exact hostname - idp.example.com + # matches that hostname only + # a wildcard suffix - '*.example.com' + # matches any subdomain of example.com, AT ANY DEPTH, but NEVER + # example.com itself + # + # Wildcard matching, spelled out, because the last part surprises people: + # + # '*.sub.host.com' MATCHES idp.sub.host.com + # MATCHES a.b.sub.host.com (any depth below the suffix) + # DOES NOT sub.host.com (the suffix itself is NOT matched) + # DOES NOT other.host.com + # DOES NOT xsub.host.com (a whole label must be replaced) + # + # To cover both a domain and its subdomains, list them both: + # + # - sub.host.com + # - '*.sub.host.com' + # + # The '*' is only supported as the entire leading label, written exactly as '*.'. Anything + # else (idp.*.example.com, *idp.example.com, a bare *) is a configuration error. Quote any + # entry that starts with '*', or YAML will reject it as an alias. Matching ignores case and + # a trailing dot, and a unicode hostname is compared in its punycode form. An IP address in + # one of these lists is a configuration error - use deniedIPs or allowedIPs. + jwksFetch: + # deniedHostnames - optional, default empty + # Tier 1 of the HOSTNAME gate: hostnames that may not be fetched. Takes exact hostnames + # and '*.suffix' wildcards, per ENTRY FORMS above. + # This narrows only, it is not a boundary on its own: the same target can be reached + # under a different name or as a literal IP, which is what the address gate is for. + # deniedHostnames: + # - old-idp.example.com + # - '*.internal.example.com' + + # allowedHostnames - optional, default empty + # Tier 2 of the HOSTNAME gate: when this list is non-empty, these are the ONLY hostnames that + # may be fetched, on the first request and on every redirect hop. Same entry forms as + # deniedHostnames, and deniedHostnames still wins. It cannot widen the address gate: a listed + # hostname whose address is blocked stays blocked. Note that while this list is set, an + # endpoint written as a literal IP address can never satisfy it. + # allowedHostnames: + # - idp.example.com + # - '*.idp.example.org' + + # blockPrivateAddresses - optional, default false + # Tier 4 of the ADDRESS gate: blocks private and loopback addresses + # (127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7). Defaults to + # false so a deployment whose IdP is on an internal address keeps working. Tier 1 is + # blocked either way, so the metadata service is never reachable. For the strictest + # posture set this to true, list any internal IdP address in allowedIPs, and list + # the IdP hostnames in allowedHostnames. + # blockPrivateAddresses: false + + # deniedIPs - optional, default empty + # Tier 2 of the ADDRESS gate: addresses that are always blocked. Takes flat IP addresses + # and CIDR blocks, per ENTRY FORMS above. Nothing in allowedIPs or allowedHostnames can + # re-enable an address listed here, so this is the right place for internal ranges that + # must never be fetched. + # deniedIPs: + # - 10.10.0.0/16 # a CIDR block + # - 192.168.5.5 # a flat IP address + # - fd00:1234::/64 # IPv6 is accepted in either form + + # allowedIPs - optional, default empty + # Tier 3 of the ADDRESS gate: a carve-out of tier 4 ONLY. Same entry forms as deniedIPs. + # Listing an address here allows it despite blockPrivateAddresses; it does NOT override + # tier 1 or deniedIPs, and it does NOT satisfy the hostname gate. Use it to reach a + # specific internal IdP while blockPrivateAddresses is true. + # allowedIPs: + # - 10.20.30.0/24 + # - 10.20.40.7 + + # timeout - optional, default 5s + # The total time allowed for a single JWKS fetch, including any redirects. Must be + # greater than zero. + # timeout: 5s + + # maxRedirects - optional, default 5 + # How many redirects a JWKS fetch will follow. Every hop passes through both gates. Set + # to 0 to refuse to follow redirects at all. + # maxRedirects: 5 + # Set to true to disable posture check functionality disablePostureChecks: false diff --git a/tests/external_jwt_signer_jwks_endpoint_test.go b/tests/external_jwt_signer_jwks_endpoint_test.go new file mode 100644 index 000000000..70f10089b --- /dev/null +++ b/tests/external_jwt_signer_jwks_endpoint_test.go @@ -0,0 +1,88 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "testing" + + "github.com/go-openapi/strfmt" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/foundation/v2/errorz" + "github.com/openziti/ziti/v2/common/eid" +) + +// Test_ExternalJwtSigner_JwksEndpoint covers the create-time check on an external JWT signer's +// jwksEndpoint. The controller fetches that URL itself, so an endpoint that the configured +// fetch policy would refuse is rejected up front rather than failing later during a fetch. +func Test_ExternalJwtSigner_JwksEndpoint(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + + adminManClient := ctx.NewEdgeManagementApi(nil) + adminManApiSession, err := adminManClient.Authenticate(ctx.NewAdminCredentials(), nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(adminManApiSession) + + t.Run("an endpoint pointing at the instance metadata service is rejected", func(t *testing.T) { + ctx.testContextChanged(t) + + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("http://169.254.169.254/latest/meta-data/")) + + ctx.Req.Error(err, "the metadata service must never be fetchable, regardless of configuration") + ctx.Req.ApiErrorWithCode(err, errorz.InvalidFieldCode) + ctx.Req.Nil(detail) + }) + + t.Run("an endpoint with a non-http scheme is rejected", func(t *testing.T) { + ctx.testContextChanged(t) + + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("file:///etc/passwd")) + + ctx.Req.Error(err) + ctx.Req.ApiErrorWithCode(err, errorz.InvalidFieldCode) + ctx.Req.Nil(detail) + }) + + t.Run("an endpoint with a hostname is accepted", func(t *testing.T) { + ctx.testContextChanged(t) + + // no name resolution happens at create time, the dial-time check covers a hostname + // that resolves to a blocked address + detail, err := adminManClient.CreateExtJwtSigner(newJwksSignerCreate("https://idp.example.com/.well-known/jwks.json")) + + ctx.Req.NoError(err) + ctx.Req.NotNil(detail) + }) +} + +// newJwksSignerCreate returns a uniquely named external JWT signer create payload that uses +// the given jwksEndpoint. +func newJwksSignerCreate(jwksEndpoint string) *rest_model.ExternalJWTSignerCreate { + name := eid.New() + endpoint := strfmt.URI(jwksEndpoint) + + return &rest_model.ExternalJWTSignerCreate{ + Name: ToPtr(name), + Enabled: ToPtr(true), + Issuer: ToPtr(name + "-issuer"), + Audience: ToPtr(name + "-audience"), + JwksEndpoint: &endpoint, + } +} From 1ae9b5b1212ed6a0cf3be87ad10ab75ac53a86ac Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Thu, 6 Aug 2026 18:59:56 -0400 Subject: [PATCH 58/73] Validate the API session token when creating circuits via CreateCircuitV3 - requires an API session token on CreateCircuitV3 requests and validates it, covering signature, audience, token type, and revocation by token id, identity, and api session - takes the dialing identity from the validated token claims rather than the router-supplied identity id, and rejects a request whose asserted identity does not match the token subject - adds the api session id to the log context, matching the V1 and V2 paths - adds tests for a missing token, an invalid token, and a token belonging to a different identity than the one asserted - notes the advisory in the 2.0.3 release notes --- CHANGELOG.md | 12 ++ common/ctrl_msg/messages.go | 4 +- .../handler_edge_ctrl/create_circuit_v3.go | 77 ++++++++++-- tests/create_circuit_v3_test.go | 117 ++++++++++++++---- 4 files changed, 173 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99ea3b64..9f22d701f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,18 @@ Thanks to the community members who contributed to this release. [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a production workload. +## Security Advisories + +This release addresses a control-plane authorization vulnerability. See the linked GitHub Security Advisory +for full details, impact, and affected versions. + +* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session + token when creating a circuit via `CreateCircuitV3`, taking the dialing identity from a router-supplied + header instead. An attacker holding enrolled router credentials could create circuits on behalf of any + identity permitted to dial the service through that router, without that identity having authenticated, + yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API + sessions were not caught at circuit creation. + ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) diff --git a/common/ctrl_msg/messages.go b/common/ctrl_msg/messages.go index 7718c9ef2..3872dbd72 100644 --- a/common/ctrl_msg/messages.go +++ b/common/ctrl_msg/messages.go @@ -207,7 +207,9 @@ func DecodeCreateCircuitV2Response(m *channel.Message) (*CreateCircuitV2Response // CreateCircuitV3Request is sent from a router to the controller to create a circuit // without a service session token. The router has already authorized the dial locally // via RDM and provides the identity and service IDs directly, along with a pre-assigned -// circuit ID. +// circuit ID. ApiSessionToken is required and must be the token of the dialing identity: +// the controller validates it and treats its claims, rather than IdentityId, as the +// authoritative identity. type CreateCircuitV3Request struct { IdentityId string ServiceId string diff --git a/controller/handler_edge_ctrl/create_circuit_v3.go b/controller/handler_edge_ctrl/create_circuit_v3.go index dd81e3e4e..4e81e89b3 100644 --- a/controller/handler_edge_ctrl/create_circuit_v3.go +++ b/controller/handler_edge_ctrl/create_circuit_v3.go @@ -23,20 +23,22 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/sdk-golang/ziti/edge" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/ctrl_msg" "github.com/openziti/ziti/v2/common/logcontext" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/openziti/ziti/v2/controller/env" "github.com/openziti/ziti/v2/controller/model" + "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/xt" "github.com/sirupsen/logrus" ) // NewCreateCircuitV3Handler creates a handler for CreateCircuitV3 requests. These requests // come from routers that have already authorized the dial locally via RDM, so no service -// session token is required. Instead, the request carries identity ID, service ID, and -// a pre-assigned circuit ID. +// session token is required. An API session token is still required: it proves the dial is +// being made on behalf of an authenticated identity, and the controller uses its claims, +// not the request's identity ID, as the authoritative dialing identity. The request also +// carries the service ID and a pre-assigned circuit ID. func NewCreateCircuitV3Handler(appEnv *env.AppEnv, ch channel.Channel) channel.TypedReceiveHandler { handler := &createCircuitHandler{ baseRequestHandler: baseRequestHandler{ @@ -80,6 +82,7 @@ func (self *createCircuitHandler) createCircuitV3(ctx *createCircuitV3RequestCon if !ctx.loadRouter() { return } + ctx.validateApiSession() ctx.setupLogContext() ctx.loadServiceByIdForDial() ctx.verifyEdgeRouterAccessForIdentity() @@ -96,7 +99,7 @@ func (self *createCircuitHandler) createCircuitV3(ctx *createCircuitV3RequestCon } log := pfxlog.ContextLogger(self.ch.Label()). - WithField("identityId", ctx.req.IdentityId). + WithField("identityId", ctx.identityId()). WithField("serviceId", ctx.req.ServiceId). WithField("circuitId", circuitInfo.Id) @@ -124,17 +127,69 @@ func (self *createCircuitV3RequestContext) UpdateResponse(m *channel.Message) { } } +// validateApiSession validates the API session token accompanying the request and +// establishes the dialing identity from its claims. Connect-v2 routers authorize the dial +// locally against their data model, but the identity ID in the request is router-supplied, +// so the controller validates the token itself. This both proves the dial is being made on +// behalf of an authenticated identity and re-checks expiration and revocation, which the +// router may not have seen yet. +func (self *createCircuitV3RequestContext) validateApiSession() { + if self.err != nil { + return + } + + log := logrus.WithField("routerId", self.sourceRouter.Id). + WithField("operation", self.handler.Label()). + WithField("requestedIdentityId", self.req.IdentityId). + WithField("serviceId", self.req.ServiceId) + + if self.req.ApiSessionToken == "" { + self.err = InvalidApiSessionError{} + log.Error("no api session token provided in create circuit v3 request") + return + } + + claims, err := self.env.ValidateAccessToken(self.req.ApiSessionToken) + if err != nil { + self.err = InvalidApiSessionError{} + log.WithError(err).Error("invalid api session token in create circuit v3 request") + return + } + + // The token is authoritative for identity. A mismatch means the router asked for a + // circuit on behalf of an identity other than the one that authenticated. + if claims.Subject != self.req.IdentityId { + self.err = InvalidApiSessionError{} + log.WithField("apiSessionIdentityId", claims.Subject). + WithField("apiSessionId", claims.ApiSessionId). + Error("create circuit v3 request identity does not match api session identity") + return + } + + self.accessClaims = claims +} + +// identityId returns the identity the circuit is being created for, taken from the +// validated API session token claims. Only valid once validateApiSession has succeeded. +func (self *createCircuitV3RequestContext) identityId() string { + if self.accessClaims == nil { + return "" + } + return self.accessClaims.Subject +} + func (self *createCircuitV3RequestContext) setupLogContext() { if self.err != nil { return } self.logContext = logcontext.NewContext() - traceSpec := self.handler.getAppEnv().TraceManager.GetIdentityTrace(self.req.IdentityId) + traceSpec := self.handler.getAppEnv().TraceManager.GetIdentityTrace(self.identityId()) if traceSpec != nil && time.Now().Before(traceSpec.Until) { self.logContext.SetChannelsMask(traceSpec.ChannelMask) self.logContext.WithField("traceId", traceSpec.TraceId) } + self.logContext.WithField("apiSessionId", self.accessClaims.ApiSessionId) } func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { @@ -157,11 +212,11 @@ func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { return } - dialable, err := self.handler.getAppEnv().Managers.EdgeService.IsDialableByIdentity(self.req.ServiceId, self.req.IdentityId) + dialable, err := self.handler.getAppEnv().Managers.EdgeService.IsDialableByIdentity(self.req.ServiceId, self.identityId()) if err != nil { self.err = internalError(err) logrus.WithField("serviceId", self.req.ServiceId). - WithField("identityId", self.req.IdentityId). + WithField("identityId", self.identityId()). WithField("operation", self.handler.Label()). WithError(err). Error("unable to verify dial access to service") @@ -171,7 +226,7 @@ func (self *createCircuitV3RequestContext) loadServiceByIdForDial() { if !dialable { self.err = InvalidServiceError{} logrus.WithField("serviceId", self.req.ServiceId). - WithField("identityId", self.req.IdentityId). + WithField("identityId", self.identityId()). WithField("operation", self.handler.Label()). Error("identity does not have dial access to service") } @@ -181,16 +236,16 @@ func (self *createCircuitV3RequestContext) verifyEdgeRouterAccessForIdentity() { if self.err != nil { return } - self.verifyEdgeRouterAccess(self.req.IdentityId, self.service.Id) + self.verifyEdgeRouterAccess(self.identityId(), self.service.Id) } func (self *createCircuitV3RequestContext) newCircuitCreateParms(serviceId string, peerData map[uint32][]byte) model.CreateCircuitParams { return &connectV3CircuitParams{ circuitId: self.req.CircuitId, serviceId: serviceId, - identityId: self.req.IdentityId, + identityId: self.identityId(), sourceRouter: self.sourceRouter, - clientId: &identity.TokenId{Token: self.req.IdentityId, Data: peerData}, + clientId: &identity.TokenId{Token: self.identityId(), Data: peerData}, logCtx: self.logContext, deadline: time.Now().Add(self.handler.getAppEnv().GetHostController().GetNetwork().GetOptions().RouteTimeout), } diff --git a/tests/create_circuit_v3_test.go b/tests/create_circuit_v3_test.go index 0797a176f..b5f18b491 100644 --- a/tests/create_circuit_v3_test.go +++ b/tests/create_circuit_v3_test.go @@ -24,6 +24,8 @@ import ( "time" "github.com/google/uuid" + "github.com/openziti/edge-api/rest_util" + edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/sdk-golang/ziti/edge" "github.com/openziti/ziti/v2/common/ctrl_msg" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" @@ -71,11 +73,33 @@ func Test_CreateCircuitV3(t *testing.T) { terminatorWatcher.waitForTerminators(5 * time.Second) - // Create a dialer identity (has dial access via dialerRole). - dialerIdentity := ctx.AdminManagementSession.requireNewIdentity(false, "dialerRole") + managementHelper := ctx.NewEdgeManagementApi(nil) + adminCreds := edge_apis.NewUpdbCredentials(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password) + adminCreds.CaPool = ctx.ControllerCaPool() + _, err = managementHelper.Authenticate(adminCreds, nil) + ctx.Req.NoError(rest_util.WrapErr(err)) - // Create an identity with no dial access. - noAccessIdentity := ctx.AdminManagementSession.requireNewIdentity(false, "noAccessRole") + clientHelper := ctx.NewEdgeClientApi(nil) + + // The controller takes the dialing identity from the API session token's claims, so + // each identity here needs a real, authenticated token rather than just an ID. + authenticateIdentity := func(roleAttributes ...string) (string, string) { + identityDetail, certCreds, err := managementHelper.CreateAndEnrollOttIdentity(false, roleAttributes...) + ctx.Req.NoError(err) + certCreds.CaPool = ctx.ControllerCaPool() + + accessToken, claims, err := clientHelper.OidcAccessToken(certCreds) + ctx.Req.NoError(err) + ctx.Req.Equal(*identityDetail.ID, claims.Subject) + + return *identityDetail.ID, accessToken + } + + // A dialer identity, with dial access via dialerRole. + dialerIdentityId, dialerToken := authenticateIdentity("dialerRole") + + // An identity with no dial access. + noAccessIdentityId, noAccessToken := authenticateIdentity("noAccessRole") // Get the control channel from the edge router to the controller. ctrlCh := edgeRouter.GetNetworkControllers().AnyCtrlChannel() @@ -105,14 +129,22 @@ func Test_CreateCircuitV3(t *testing.T) { return ctrl_msg.DecodeCreateCircuitV3Response(msg) } + requireErrorCode := func(err error, code uint32) { + ctx.Req.Error(err) + circuitErr, ok := err.(*circuitV3Error) + ctx.Req.True(ok, "expected a controller error response, got %v", err) + ctx.Req.Equal(code, circuitErr.code, "unexpected error code, message was: %v", circuitErr.msg) + } + t.Run("successful circuit creation", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: svc.Id, - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } resp, err := sendV3Request(req) @@ -122,46 +154,80 @@ func Test_CreateCircuitV3(t *testing.T) { ctx.Req.NotEmpty(resp.Address) }) - t.Run("invalid identity", func(t *testing.T) { + t.Run("missing api session token", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: "bogus-identity-id", + IdentityId: dialerIdentityId, ServiceId: svc.Id, CircuitId: uuid.NewString(), PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) + }) + + t.Run("invalid api session token", func(t *testing.T) { + ctx.NextTest(t) + + req := &ctrl_msg.CreateCircuitV3Request{ + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: "not-a-valid-token", + PeerData: map[uint32][]byte{}, + } + + _, err := sendV3Request(req) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) + }) + + // A router must not be able to dial on behalf of an identity other than the one + // whose API session token it presents, even when that identity has dial access. + t.Run("api session token for a different identity", func(t *testing.T) { + ctx.NextTest(t) + + req := &ctrl_msg.CreateCircuitV3Request{ + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: noAccessToken, + PeerData: map[uint32][]byte{}, + } + + _, err := sendV3Request(req) + requireErrorCode(err, edge.ErrorCodeInvalidApiSession) }) t.Run("invalid service", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: "bogus-service-id", - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: "bogus-service-id", + CircuitId: uuid.NewString(), + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidService) }) t.Run("identity without dial access", func(t *testing.T) { ctx.NextTest(t) req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: noAccessIdentity.Id, - ServiceId: svc.Id, - CircuitId: uuid.NewString(), - PeerData: map[uint32][]byte{}, + IdentityId: noAccessIdentityId, + ServiceId: svc.Id, + CircuitId: uuid.NewString(), + ApiSessionToken: noAccessToken, + PeerData: map[uint32][]byte{}, } _, err := sendV3Request(req) - ctx.Req.Error(err) + requireErrorCode(err, edge.ErrorCodeInvalidService) }) t.Run("duplicate circuit ID", func(t *testing.T) { @@ -170,10 +236,11 @@ func Test_CreateCircuitV3(t *testing.T) { circuitId := uuid.NewString() req := &ctrl_msg.CreateCircuitV3Request{ - IdentityId: dialerIdentity.Id, - ServiceId: svc.Id, - CircuitId: circuitId, - PeerData: map[uint32][]byte{}, + IdentityId: dialerIdentityId, + ServiceId: svc.Id, + CircuitId: circuitId, + ApiSessionToken: dialerToken, + PeerData: map[uint32][]byte{}, } resp, err := sendV3Request(req) From 249b02cb1542b1527d0119babe808cbac7a92e71 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 19 Aug 2026 17:46:03 -0400 Subject: [PATCH 59/73] Add consolidated 2.0.3 security advisory notes to the changelog --- CHANGELOG.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f22d701f..b99ea3b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,18 +57,6 @@ Thanks to the community members who contributed to this release. [#4184](https://github.com/openziti/ziti/issues/4184) and validated the fix against a production workload. -## Security Advisories - -This release addresses a control-plane authorization vulnerability. See the linked GitHub Security Advisory -for full details, impact, and affected versions. - -* [GHSA-7868-235p-7497](https://github.com/openziti/ziti/security/advisories/GHSA-7868-235p-7497) (CVE pending) (Medium) - The controller did not validate the API session - token when creating a circuit via `CreateCircuitV3`, taking the dialing identity from a router-supplied - header instead. An attacker holding enrolled router credentials could create circuits on behalf of any - identity permitted to dial the service through that router, without that identity having authenticated, - yielding data-plane access under an impersonated identity. The same gap meant expired and revoked API - sessions were not caught at circuit creation. - ## Component Updates and Bug Fixes * github.com/openziti/ziti/v2: [v2.0.2 -> v2.0.3](https://github.com/openziti/ziti/compare/v2.0.2...v2.0.3) From c39a9b82b5a9cf715aef2b2449cbcc760d6bbbdb Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 22 Jul 2026 09:21:20 -0400 Subject: [PATCH 60/73] backport fixes GHSA-354c-gpg9-j988 to v2.0 router MFA posture nil panic - nil-check state.Woken/state.Unlocked in MfaCheck.Evaluate before dereferencing - align router wake/unlock semantics with the controller's PassedOnWake/PassedOnUnlock: an absent or pre-MFA wake/unlock event passes; only a post-MFA event beyond the grace period fails - recover panics in EvaluatePostureCheck, converting them into a failed check (denied access) instead of an unrecovered goroutine panic crashing the router - add regression tests for router/posture MFA evaluation - adjust mfa_test.go import to the sdk-golang v1 module path used on release-v2.0.x (cherry picked from commit fbca515bc80ac180d7277d62fd964d3278a35061) --- router/posture/errors.go | 21 ++++- router/posture/mfa.go | 8 +- router/posture/mfa_test.go | 158 +++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 router/posture/mfa_test.go diff --git a/router/posture/errors.go b/router/posture/errors.go index 2e3757154..0cf02be07 100644 --- a/router/posture/errors.go +++ b/router/posture/errors.go @@ -2,7 +2,9 @@ package posture import ( "fmt" + "runtime/debug" + "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" "github.com/pkg/errors" ) @@ -66,7 +68,24 @@ func (pae *PolicyAccessErrors) Error() string { return fmt.Sprintf("%d policies failed: %s", len(*pae), subErr) } -func EvaluatePostureCheck(postureCheck *edge_ctrl_pb.DataState_PostureCheck, data *InstanceData) *CheckError { +// EvaluatePostureCheck evaluates a single posture check against the supplied posture +// state. It never panics: an evaluation panic is recovered and reported as a failed +// check, denying access instead of crashing the router. +func EvaluatePostureCheck(postureCheck *edge_ctrl_pb.DataState_PostureCheck, data *InstanceData) (result *CheckError) { + defer func() { + if r := recover(); r != nil { + pfxlog.Logger().WithField("postureCheckId", postureCheck.Id). + WithField("postureCheckName", postureCheck.Name). + Errorf("posture check evaluation panicked, treating as failed: %v\n%s", r, debug.Stack()) + + result = &CheckError{ + Id: postureCheck.Id, + Name: postureCheck.Name, + Cause: fmt.Errorf("posture check evaluation panicked: %v", r), + } + } + }() + check := CtrlCheckToLogic(postureCheck) return check.Evaluate(data) } diff --git a/router/posture/mfa.go b/router/posture/mfa.go index 59a711a14..9948a2b89 100644 --- a/router/posture/mfa.go +++ b/router/posture/mfa.go @@ -50,11 +50,11 @@ func (m *MfaCheck) Evaluate(state *InstanceData) *CheckError { } } - if m.PromptOnWake { + if m.PromptOnWake && state.Woken != nil { wokenAt := state.Woken.Time.AsTime() wokenGraceEndsAt := wokenAt.Add(PromptGracePeriod) - if now.After(wokenGraceEndsAt) { + if wokenAt.After(*state.PassedMfaAt) && now.After(wokenGraceEndsAt) { return &CheckError{ Id: m.Id, Name: m.Name, @@ -63,11 +63,11 @@ func (m *MfaCheck) Evaluate(state *InstanceData) *CheckError { } } - if m.PromptOnUnlock { + if m.PromptOnUnlock && state.Unlocked != nil { unlockedAt := state.Unlocked.Time.AsTime() unlockedGraceEndsAt := unlockedAt.Add(PromptGracePeriod) - if now.After(unlockedGraceEndsAt) { + if unlockedAt.After(*state.PassedMfaAt) && now.After(unlockedGraceEndsAt) { return &CheckError{ Id: m.Id, Name: m.Name, diff --git a/router/posture/mfa_test.go b/router/posture/mfa_test.go new file mode 100644 index 000000000..6f2f56839 --- /dev/null +++ b/router/posture/mfa_test.go @@ -0,0 +1,158 @@ +package posture + +import ( + "testing" + "time" + + "github.com/openziti/sdk-golang/pb/edge_client_pb" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func newMfaCheck(promptOnWake, promptOnUnlock bool) *MfaCheck { + return &MfaCheck{ + DataState_PostureCheck: &edge_ctrl_pb.DataState_PostureCheck{ + Id: "test-mfa-id", + Name: "my-mfa-check", + }, + DataState_PostureCheck_Mfa: &edge_ctrl_pb.DataState_PostureCheck_Mfa{ + TimeoutSeconds: NoTimeout, + PromptOnWake: promptOnWake, + PromptOnUnlock: promptOnUnlock, + }, + } +} + +func newMfaInstanceData(passedMfaAt time.Time) *InstanceData { + return &InstanceData{ + IdentityId: "test-identity", + ApiSessionId: "test-api-session", + PassedMfaAt: &passedMfaAt, + } +} + +// MFA passed, promptOnWake enabled, but no wake event has ever been reported: Woken is nil. +// Must pass without panicking, matching the controller's PassedOnWake semantics. +func TestMfaCheck_PromptOnWake_NilWoken_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when promptOnWake is set and no wake event was reported, got: %v", result) + } +} + +// MFA passed, promptOnUnlock enabled, but no unlock event has ever been reported: Unlocked is nil. +// Must pass without panicking, matching the controller's PassedOnUnlock semantics. +func TestMfaCheck_PromptOnUnlock_NilUnlocked_Passes(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when promptOnUnlock is set and no unlock event was reported, got: %v", result) + } +} + +// Both prompts enabled, neither event reported. Must pass without panicking. +func TestMfaCheck_BothPrompts_NilEvents_Passes(t *testing.T) { + check := newMfaCheck(true, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when no wake/unlock events were reported, got: %v", result) + } +} + +// A wake event that occurred before MFA was last passed is satisfied by that MFA pass. +func TestMfaCheck_PromptOnWake_WokenBeforeMfa_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-time.Hour)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when wake event predates the MFA pass, got: %v", result) + } +} + +// An unlock event that occurred before MFA was last passed is satisfied by that MFA pass. +func TestMfaCheck_PromptOnUnlock_UnlockedBeforeMfa_Passes(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Minute)) + data.Unlocked = &edge_client_pb.PostureResponse_Unlocked{ + Time: timestamppb.New(time.Now().Add(-time.Hour)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass when unlock event predates the MFA pass, got: %v", result) + } +} + +// A wake event after the last MFA pass, still within the grace period, passes. +func TestMfaCheck_PromptOnWake_WokenAfterMfaWithinGrace_Passes(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-time.Minute)), + } + + if result := check.Evaluate(data); result != nil { + t.Fatalf("expected pass for wake event within the grace period, got: %v", result) + } +} + +// A wake event after the last MFA pass, beyond the grace period, fails. +func TestMfaCheck_PromptOnWake_WokenAfterMfaBeyondGrace_Fails(t *testing.T) { + check := newMfaCheck(true, false) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Woken = &edge_client_pb.PostureResponse_Woken{ + Time: timestamppb.New(time.Now().Add(-10 * time.Minute)), + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure for wake event beyond the grace period without MFA resupply, got pass") + } +} + +// An unlock event after the last MFA pass, beyond the grace period, fails. +func TestMfaCheck_PromptOnUnlock_UnlockedAfterMfaBeyondGrace_Fails(t *testing.T) { + check := newMfaCheck(false, true) + data := newMfaInstanceData(time.Now().Add(-time.Hour)) + data.Unlocked = &edge_client_pb.PostureResponse_Unlocked{ + Time: timestamppb.New(time.Now().Add(-10 * time.Minute)), + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure for unlock event beyond the grace period without MFA resupply, got pass") + } +} + +// MFA never passed still fails regardless of prompt flags. +func TestMfaCheck_NeverPassedMfa_Fails(t *testing.T) { + check := newMfaCheck(true, true) + data := &InstanceData{ + IdentityId: "test-identity", + ApiSessionId: "test-api-session", + } + + if result := check.Evaluate(data); result == nil { + t.Fatal("expected failure when MFA has never been passed, got pass") + } +} + +// EvaluatePostureCheck must convert an evaluation panic into a failed check, never +// propagate it to the caller. An unknown subtype produces a nil Checker whose +// Evaluate call panics. +func TestEvaluatePostureCheck_PanicBecomesFailedCheck(t *testing.T) { + postureCheck := &edge_ctrl_pb.DataState_PostureCheck{ + Id: "test-unknown-id", + Name: "my-unknown-check", + } + + result := EvaluatePostureCheck(postureCheck, &InstanceData{}) + + if result == nil { + t.Fatal("expected a non-nil CheckError for an unevaluable posture check, got nil") + } +} From 99ca242ecc99fcda0e0c0e9fa93596a20f80d85f Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Sat, 8 Aug 2026 19:47:17 -0400 Subject: [PATCH 61/73] Serialize router control-channel connect/disconnect. For #4264 The controller decided which of two racing connections for a router was current by comparing router instances, but loaded one per connect by evicting the router cache and reading back through it. Two connects could both evict, and whichever read second was handed the instance the first had just published. A shared instance makes the two connections indistinguishable: the connect path cannot reject the second into an occupied slot, and when either channel dies the disconnect path finds itself current and tears down the registration the other is still using. The surviving channel is never re-bound, so the router stays connected at the transport layer while absent from the model, unable to recover. Connect and disconnect were also unserialized, so a stale or superseded disconnect could interleave with a live connection and take its links with it. - serializes a router's connect and disconnect with a per-router striped lock - keeps at most one connection per router: a connect into an occupied slot is rejected via an error from ConnectRouter, so the bind fails and NewChannel closes it without starting rx or registering it, and the occupant is displaced; the router redials into the freed slot - displaces an occupant by closing its channel and also invoking the teardown directly, since a channel that is already closed never fires its close handler again; without this a dead but still registered connection holds the slot forever and every redial is rejected against a slot nothing can free - refuses a connect whose control channel is already closed rather than registering it, so a connection no disconnect could ever remove is never published - gives every connection its own router instance via RouterManager.NewCtrlChanRouter, read through readUncached so the cache neither supplies nor receives it, which is what makes comparing instances meaningful - moves recording the channel and connect time out of the accept path, so a caller cannot attach the wrong channel or forget to attach one - serializes link publication with that teardown on the same per-router stripe. Validating currency and then publishing without it is a check-then-act: a report can find the connection current and, by the time it reaches the link manager, the teardown has already snapshotted and cleared the router's links, so the link is recreated after everything that would have removed it. It is then absent from the router's own index while still in the link table with a disconnected source, and a reconnect reporting the same iteration can adopt that stale source instead of rebuilding the link - guards the entire DisconnectRouter teardown by connection currency, all or nothing, and clears the connected flag and link index only when the registration was actually given up, with the flag cleared under the same shard lock as the map removal so the two cannot be observed disagreeing; the connected flag decides whether the controller accepts a router's link reports, so clearing it for the wrong connection silences a router that is up and reporting - reduces MarkConnected to publishing the connection; the takeover-close moves into ConnectRouter's reject path - makes the per-router unlock idempotent so callers can defer it as a leak-safety net and still unlock early before closing a channel outside the lock - stops the replaced RouterSender in routerTxMap.Add so a takeover does not leak the old sender's goroutine when the broker's asynchronous RouterDisconnected loses the race to the redial's RouterConnected - discards pending peer state changes for a router whose channel has closed, since sending on one fails immediately and the failed send is retried as soon as the event loop turns, spinning the loop and flooding the log - queues the peer-state send-done event on every path, so a missing channel can no longer leave sendInProgress set and stall that router's updates permanently - resolves a router's version from its connected instance when validating link conn info, since the version arrives in the hello and so is absent from an instance loaded from the database - normalizes both endpoints to the connected instance in shortestPath, which is keyed and compared by pointer and so treated an endpoint held as any other instance of the same router as absent from the graph, reporting a router as unroutable from itself. That worked before only because the connect path published its instance into the router cache, so a cache read and the connected map returned the same object; nothing stated the requirement - configures test logging once per package in TestMain, so a test no longer writes global logger state while a previous test's shutdown logging reads it Original issue: #4196. (cherry picked from commit 647c4daa1eb158b07c4678a09f5f09664a045154) --- common/concurrency/striped_id_locker.go | 66 ++++ common/concurrency/striped_id_locker_test.go | 127 ++++++ common/ctrlchan/channel_test.go | 74 ++++ controller/handler_ctrl/accept.go | 25 +- controller/model/router_manager.go | 102 ++++- controller/model/router_manager_test.go | 83 ++++ controller/model/router_model.go | 3 + controller/network/link_validation_test.go | 99 +++++ controller/network/main_test.go | 35 ++ controller/network/network.go | 131 ++++++- controller/network/network_path.go | 10 + controller/network/route_perf_test.go | 8 - controller/network/router_connect_test.go | 383 +++++++++++++++++++ controller/network/router_messaging.go | 25 +- controller/network/router_messaging_test.go | 58 +++ controller/sync_strats/rtx.go | 12 +- controller/sync_strats/rtx_add_test.go | 53 +++ 17 files changed, 1240 insertions(+), 54 deletions(-) create mode 100644 common/concurrency/striped_id_locker.go create mode 100644 common/concurrency/striped_id_locker_test.go create mode 100644 controller/model/router_manager_test.go create mode 100644 controller/network/link_validation_test.go create mode 100644 controller/network/main_test.go create mode 100644 controller/network/router_connect_test.go create mode 100644 controller/network/router_messaging_test.go create mode 100644 controller/sync_strats/rtx_add_test.go diff --git a/common/concurrency/striped_id_locker.go b/common/concurrency/striped_id_locker.go new file mode 100644 index 000000000..1bcf52e4e --- /dev/null +++ b/common/concurrency/striped_id_locker.go @@ -0,0 +1,66 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package concurrency + +import "sync" + +// StripedIdLocker is a fixed-size set of mutexes selected by hashing a string +// id. Operations on the same id always serialize; operations on different ids +// proceed concurrently unless they happen to hash to the same slot. Ids that +// collide on a slot share a lock (false contention), so size the locker +// comfortably above the expected concurrency to keep collisions rare. +// +// It is intended as a lighter-weight alternative to a single coarse mutex when +// the protected work is keyed by id and most concurrent callers touch +// different ids. +type StripedIdLocker struct { + locks []sync.Mutex +} + +// NewStripedIdLocker returns a StripedIdLocker with the given number of slots. +// slots is clamped to a minimum of 1. +func NewStripedIdLocker(slots int) *StripedIdLocker { + if slots < 1 { + slots = 1 + } + return &StripedIdLocker{locks: make([]sync.Mutex, slots)} +} + +// LockFor locks the slot for id and returns a function that unlocks it. The +// returned function must be called exactly once. Typical use: +// +// defer locker.LockFor(id)() +func (self *StripedIdLocker) LockFor(id string) func() { + m := &self.locks[self.indexFor(id)] + m.Lock() + return m.Unlock +} + +// indexFor maps id to a slot using FNV-1a (inlined to avoid allocating a hasher +// on this hot path). +func (self *StripedIdLocker) indexFor(id string) uint32 { + const ( + offset32 = 2166136261 + prime32 = 16777619 + ) + h := uint32(offset32) + for i := 0; i < len(id); i++ { + h ^= uint32(id[i]) + h *= prime32 + } + return h % uint32(len(self.locks)) +} diff --git a/common/concurrency/striped_id_locker_test.go b/common/concurrency/striped_id_locker_test.go new file mode 100644 index 000000000..4c578b386 --- /dev/null +++ b/common/concurrency/striped_id_locker_test.go @@ -0,0 +1,127 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package concurrency + +import ( + "fmt" + "sync" + "testing" + "time" +) + +// TestStripedIdLocker_SameIdSerializes verifies that two lockers for the same id +// are mutually exclusive: the second LockFor blocks until the first unlocks. +func TestStripedIdLocker_SameIdSerializes(t *testing.T) { + locker := NewStripedIdLocker(16) + + unlock := locker.LockFor("link-1") + + acquired := make(chan struct{}) + go func() { + secondUnlock := locker.LockFor("link-1") + close(acquired) + secondUnlock() + }() + + select { + case <-acquired: + t.Fatal("second LockFor for the same id acquired while the first was still held") + case <-time.After(50 * time.Millisecond): + // expected: still blocked + } + + unlock() + + select { + case <-acquired: + // expected: unblocked once the first lock was released + case <-time.After(time.Second): + t.Fatal("second LockFor did not acquire after the first was released") + } +} + +// TestStripedIdLocker_MutualExclusion runs many goroutines incrementing per-id +// counters guarded only by the striped locker. With correct per-id +// serialization every counter ends at the expected total. Run with -race to +// also catch incorrect sharing. +func TestStripedIdLocker_MutualExclusion(t *testing.T) { + const ( + ids = 50 + goroutines = 32 + incsPerWorker = 200 + ) + + // Use more ids than slots so multiple ids share slots; correctness must not + // depend on a 1:1 id-to-slot mapping. + locker := NewStripedIdLocker(8) + counters := make([]int, ids) + + var wg sync.WaitGroup + for g := 0; g < goroutines; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < incsPerWorker; i++ { + id := fmt.Sprintf("id-%d", i%ids) + unlock := locker.LockFor(id) + counters[i%ids]++ + unlock() + } + }() + } + wg.Wait() + + expected := goroutines * incsPerWorker / ids + for i, c := range counters { + if c != expected { + t.Fatalf("counter[%d] = %d, expected %d", i, c, expected) + } + } +} + +// TestStripedIdLocker_DifferentIdsConcurrent verifies that ids landing on +// different slots do not block each other. +func TestStripedIdLocker_DifferentIdsConcurrent(t *testing.T) { + locker := NewStripedIdLocker(256) + + // Find two ids that map to different slots. + a := "router-a" + var b string + for i := 0; ; i++ { + candidate := fmt.Sprintf("router-%d", i) + if locker.indexFor(candidate) != locker.indexFor(a) { + b = candidate + break + } + } + + unlockA := locker.LockFor(a) + defer unlockA() + + done := make(chan struct{}) + go func() { + locker.LockFor(b)() // should not block on a's slot + close(done) + }() + + select { + case <-done: + // expected + case <-time.After(time.Second): + t.Fatal("LockFor on a different slot blocked while an unrelated slot was held") + } +} diff --git a/common/ctrlchan/channel_test.go b/common/ctrlchan/channel_test.go index 092a9f765..c8f0d116d 100644 --- a/common/ctrlchan/channel_test.go +++ b/common/ctrlchan/channel_test.go @@ -653,3 +653,77 @@ func TestDialCtrlChannel_ReconnectCycle(t *testing.T) { }, 5*time.Second, 10*time.Millisecond, "loop iteration %d, should match channel iteration %d", i, dialChannel.iteration.Load()) } } + +// TestReject_BindErrorNotEstablishedOrRxStarted validates the boundary that router-connect rejection +// relies on: when the ctrl-channel bind handler returns an error (as CtrlAccepter.Bind does when +// network.ConnectRouter rejects a busy slot), channel.NewChannel returns that error, the MultiListener +// closes the underlay without registering the channel, and the receive loop never starts. This +// no-rx / no-register outcome is the contract the fake-CtrlChannel unit tests cannot observe. +func TestReject_BindErrorNotEstablishedOrRxStarted(t *testing.T) { + req := require.New(t) + + listenerAddr := "tcp:127.0.0.1:40010" + id := &identity.TokenId{Token: "test-controller"} + + var bindAttempts atomic.Int32 + var established atomic.Int32 + var rxFired atomic.Bool + + multiListener := channel.NewMultiListener( + func(underlay channel.Underlay, closeCallback func()) (channel.MultiChannel, error) { + bindAttempts.Add(1) + listenerChannel := NewListenerCtrlChannel() + multiConfig := &channel.MultiChannelConfig{ + LogicalName: "ctrl/" + underlay.ConnectionId(), + Options: channel.DefaultOptions(), + UnderlayHandler: listenerChannel, + Underlay: underlay, + BindHandler: channel.BindHandlerF(func(binding channel.Binding) error { + // Would flip if the rx loop ever started for this (rejected) channel. + binding.AddReceiveHandlerF(echoContentType, func(*channel.Message, channel.Channel) { + rxFired.Store(true) + }) + // Reject, as network.ConnectRouter does for a busy slot. + return fmt.Errorf("simulated router connect rejected (busy slot)") + }), + } + + multiCh, err := channel.NewMultiChannel(multiConfig) + if err != nil { + // Rejected: NewMultiChannel closed the underlay and never started rx; do not register. + return nil, err + } + established.Add(1) + return multiCh, nil + }, + func(underlay channel.Underlay) error { + return fmt.Errorf("ungrouped connections not supported") + }, + ) + + bindAddr, err := transport.ParseAddress(listenerAddr) + req.NoError(err) + listenerConfig := channel.ListenerConfig{ConnectOptions: channel.DefaultOptions().ConnectOptions} + listener, err := channel.NewClassicListenerF(id, bindAddr, listenerConfig, multiListener.AcceptUnderlay) + req.NoError(err) + defer func() { _ = listener.Close() }() + + headers := channel.Headers{} + headers.PutStringHeader(channel.TypeHeader, ChannelTypeDefault) + headers.PutBoolHeader(channel.IsGroupedHeader, true) + headers.PutBoolHeader(channel.IsFirstGroupConnection, true) + + dialer := channel.NewClassicDialer(channel.DialerConfig{Identity: id, Endpoint: bindAddr}) + underlay, err := dialer.CreateWithHeaders(5*time.Second, headers) + req.NoError(err) + defer func() { _ = underlay.Close() }() + + // The dial reaches the listener's bind (the hello is acked before the factory runs). + req.Eventually(func() bool { return bindAttempts.Load() >= 1 }, 5*time.Second, 10*time.Millisecond, + "listener bind should have run for the dialed connection") + + // The rejected connection is never established, and its receive loop never starts. Give any spurious + // rx a moment to (not) happen. + req.Never(func() bool { return established.Load() != 0 || rxFired.Load() }, 500*time.Millisecond, 50*time.Millisecond, + "a rejected bind must not establish a channel or start its receive loop") +} diff --git a/controller/handler_ctrl/accept.go b/controller/handler_ctrl/accept.go index 529332fd3..e3b7f7625 100644 --- a/controller/handler_ctrl/accept.go +++ b/controller/handler_ctrl/accept.go @@ -17,8 +17,6 @@ package handler_ctrl import ( - "time" - "github.com/google/uuid" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" @@ -111,16 +109,12 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { ch := binding.GetChannel() log := pfxlog.Logger().WithField("routerId", ch.Id()) - // Use a new copy of the router instance each time we connect. That way we can tell on disconnect - // if we're working with the right connection, in case connects and disconnects happen quickly. - // It also means that the channel and connected time fields don't change and we don't have to protect them - r, err := self.network.GetReloadedRouter(ch.Id()) + // A fresh instance per connection, carrying this channel: that is what lets connect and disconnect tell + // two connections for one router apart, and keeps them from writing over each other's state. + r, err := self.network.NewCtrlChanRouter(ch) if err != nil { return err } - if r == nil { - return errors.Errorf("no router with id [%v] found, closing connection", ch.Id()) - } var ctrlChanListeners map[string][]string @@ -190,8 +184,6 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { return errors.New("channel provided no headers, not accepting router connection as version info not provided") } - r.Control = ch.(channel.MultiChannel).GetUnderlayHandler().(ctrlchan.CtrlChannel) - r.ConnectTime = time.Now() if err := binding.Bind(newBindHandler(self.heartbeatOptions, r, self.network, self.xctrls)); err != nil { return errors.Wrap(err, "error binding router") } @@ -200,9 +192,16 @@ func (self *CtrlAccepter) Bind(binding channel.Binding) error { binding.AddPeekHandler(self.traceHandler) } - log.Info("accepted new router connection") + if err = self.network.ConnectRouter(r); err != nil { + if network.IsConnectRejected(err) { + log.Info("router connect rejected; another connection is already current, router will redial") + } + // Returning the error fails the bind, so NewChannel closes this channel's underlay without + // starting rx or registering it. That preserves the rx-gate for a rejected connection. + return err + } - self.network.ConnectRouter(r) + log.Info("accepted new router connection") return nil } diff --git a/controller/model/router_manager.go b/controller/model/router_manager.go index 9d254eba2..cbcf7d8a5 100644 --- a/controller/model/router_manager.go +++ b/controller/model/router_manager.go @@ -27,6 +27,9 @@ import ( "time" "github.com/openziti/channel/v4/protobufs" + "github.com/openziti/channel/v4" + "github.com/openziti/ziti/v2/common/concurrency" + "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/openziti/ziti/v2/common/inspect" "github.com/openziti/ziti/v2/common/pb/cmd_pb" "github.com/openziti/ziti/v2/common/pb/ctrl_pb" @@ -38,9 +41,9 @@ import ( "google.golang.org/protobuf/proto" "github.com/michaelquigley/pfxlog" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/controller/db" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" cmap "github.com/orcaman/concurrent-map/v2" "github.com/pkg/errors" "go.etcd.io/bbolt" @@ -79,8 +82,9 @@ func NewRouter(id, name, fingerprint string, cost uint16, noTraversal bool) *Rou type RouterManager struct { baseEntityManager[*Router, *db.Router] - cache cmap.ConcurrentMap[string, *Router] - connected cmap.ConcurrentMap[string, *Router] + cache cmap.ConcurrentMap[string, *Router] + connected cmap.ConcurrentMap[string, *Router] + connectLocks *concurrency.StripedIdLocker } func newRouterManager(env Env) *RouterManager { @@ -89,6 +93,7 @@ func newRouterManager(env Env) *RouterManager { baseEntityManager: newBaseEntityManager[*Router, *db.Router](env, routerStore), cache: cmap.New[*Router](), connected: cmap.New[*Router](), + connectLocks: concurrency.NewStripedIdLocker(256), } result.impl = result @@ -115,28 +120,61 @@ func (self *RouterManager) NewModelEntity() *Router { return &Router{} } -func (self *RouterManager) MarkConnected(r *Router) { - if router, _ := self.connected.Get(r.Id); router != nil { - if ch := router.Control; ch != nil { - if err := ch.Close(); err != nil { - pfxlog.Logger().WithError(err).Error("error closing control channel") - } +// LockConnectFor acquires the per-router connect lock for the given router id and returns the unlock +// function. It serializes a router's connect and disconnect processing so they cannot interleave. The +// returned unlock is idempotent, so a caller may both defer it (as a leak-safety net across all exit +// paths) and call it early (e.g. to release before closing a channel outside the lock) without +// double-unlocking. The flag is unshared and only ever touched by the goroutine holding the lock, so it +// needs no synchronization. +func (self *RouterManager) LockConnectFor(id string) func() { + rawUnlock := self.connectLocks.LockFor(id) + unlocked := false + return func() { + if !unlocked { + unlocked = true + rawUnlock() } } +} +// MarkConnected publishes r as the current connection for its router id. Callers must serialize with +// LockConnectFor and must have already ensured the slot is free (ConnectRouter rejects a busy slot rather +// than taking over here), so this only records the connection; it does not close any prior channel. +func (self *RouterManager) MarkConnected(r *Router) { r.Connected.Store(true) self.connected.Set(r.Id, r) } +// MarkDisconnected gives up r's registration, and does nothing at all if r is not the registration holder. +// +// A connection only owns the state on its own instance, so a connection that has already been replaced must +// not clear it. What makes that worth enforcing rather than assuming is where the connected flag is read: +// the handler for a router's link reports drops them when it is false, so clearing it for the wrong +// connection silences a router that is up and reporting, with nothing to correct it. +// +// This is all-or-nothing rather than a fix for instances shared between connections. Pointer identity +// cannot tell two connections apart when they share an instance, so in that case the shared instance is the +// registration holder and its state is cleared here regardless. Only giving each connection its own +// instance prevents that. func (self *RouterManager) MarkDisconnected(r *Router) { - r.Connected.Store(false) - self.connected.RemoveCb(r.Id, func(key string, v *Router, exists bool) bool { + removed := self.connected.RemoveCb(r.Id, func(key string, v *Router, exists bool) bool { if exists && v != r { pfxlog.Logger().WithField("routerId", r.Id).Info("router not current connect, not clearing from connected map") return false } - return exists + if !exists { + return false + } + // Under the shard lock so the flag and the map entry change together: the link report path looks the + // router up and then reads the flag, and would otherwise see a removed entry still marked connected. + r.Connected.Store(false) + return true }) + + if !removed { + return + } + r.routerLinks.Clear() } @@ -196,6 +234,46 @@ func (self *RouterManager) Exists(id string) (bool, error) { return exists, err } +// NewCtrlChanRouter builds the Router instance representing one control-channel connection, with the +// channel already recorded on it. +// +// The instance is deliberately kept out of the router cache. The connect path writes connection-scoped +// state onto it (Control, ConnectTime, VersionInfo, Connected, links), and the controller decides which of +// two racing connections for a router is current by comparing instances. Handing a cached instance to a +// second connection makes the two indistinguishable: neither the connect path's occupied-slot check nor the +// disconnect path's currency check can tell them apart, so a connection that dies takes its replacement's +// registration down with it, and each connection's writes land on the other's state. +// +// Read, by contrast, is cache-backed and returns router entity data. Connection state belongs to +// GetConnected, not to Read. +func (self *RouterManager) NewCtrlChanRouter(ch channel.Channel) (*Router, error) { + r, err := self.readUncached(ch.Id()) + if err != nil { + return nil, err + } + if r == nil { + return nil, fmt.Errorf("no router with id [%v] found", ch.Id()) + } + + multiCh, ok := ch.(channel.MultiChannel) + if !ok { + return nil, fmt.Errorf("control channel for router [%v] is not a multi-channel, got %T", ch.Id(), ch) + } + + ctrlCh, ok := multiCh.GetUnderlayHandler().(ctrlchan.CtrlChannel) + if !ok { + return nil, fmt.Errorf("control channel for router [%v] has unexpected underlay handler type %T", ch.Id(), multiCh.GetUnderlayHandler()) + } + + r.Control = ctrlCh + r.ConnectTime = time.Now() + + return r, nil +} + +// readUncached reads a router straight from the database, bypassing the cache in both directions: it +// neither reads a cached instance nor publishes the one it creates. Callers needing an instance they can +// own get it here. func (self *RouterManager) readUncached(id string) (*Router, error) { entity := &Router{} err := self.GetDb().View(func(tx *bbolt.Tx) error { diff --git a/controller/model/router_manager_test.go b/controller/model/router_manager_test.go new file mode 100644 index 000000000..cb5859408 --- /dev/null +++ b/controller/model/router_manager_test.go @@ -0,0 +1,83 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package model + +import ( + "testing" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" +) + +func newMarkDisconnectedTestManager() *RouterManager { + return &RouterManager{connected: cmap.New[*Router]()} +} + +// TestMarkDisconnected_LeavesStateOfNonHolderAlone: a connection owns only the state on its own instance, so +// one that has already been replaced must not clear it. The connected flag decides whether the controller +// accepts that router's link reports, so clearing it for the wrong connection silences a router that is up +// and reporting with nothing to correct it. +func TestMarkDisconnected_LeavesStateOfNonHolderAlone(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + superseded := NewRouter("r1", "r1", "", 0, false) + current := NewRouter("r1", "r1", "", 0, false) + + superseded.Connected.Store(true) + superseded.routerLinks.Add(&Link{Id: "l1", DstId: "dst"}, "dst") + + mgr.MarkConnected(current) + + mgr.MarkDisconnected(superseded) + + require.Same(t, current, mgr.GetConnected("r1"), "the registration holder must be left in place") + require.True(t, current.Connected.Load(), "the holder's connected flag must not be cleared") + require.True(t, superseded.Connected.Load(), + "a non-holder's state is not this call's to clear") + require.Len(t, superseded.GetLinks(), 1, "a non-holder's links are not this call's to clear") +} + +// TestMarkDisconnected_ClearsStateOfHolder is the ordinary case: the registration holder disconnecting gives +// up the registration and its per-connection state together. +func TestMarkDisconnected_ClearsStateOfHolder(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + current := NewRouter("r1", "r1", "", 0, false) + current.routerLinks.Add(&Link{Id: "l1", DstId: "dst"}, "dst") + mgr.MarkConnected(current) + require.True(t, current.Connected.Load()) + + mgr.MarkDisconnected(current) + + require.Nil(t, mgr.GetConnected("r1"), "the registration must be given up") + require.False(t, current.Connected.Load(), "the holder's connected flag must be cleared") + require.Empty(t, current.GetLinks(), "the holder's links must be cleared") +} + +// TestMarkDisconnected_UnregisteredIsANoop: a connection that never registered, or whose registration is +// already gone, has nothing to give up. +func TestMarkDisconnected_UnregisteredIsANoop(t *testing.T) { + mgr := newMarkDisconnectedTestManager() + + r := NewRouter("r1", "r1", "", 0, false) + r.Connected.Store(true) + + mgr.MarkDisconnected(r) + + require.Nil(t, mgr.GetConnected("r1")) + require.True(t, r.Connected.Load(), "with no registration to give up there is nothing to clear") +} diff --git a/controller/model/router_model.go b/controller/model/router_model.go index 55c586ef7..c88e5acf7 100644 --- a/controller/model/router_model.go +++ b/controller/model/router_model.go @@ -45,6 +45,9 @@ type Router struct { Control ctrlchan.CtrlChannel Connected atomic.Bool ConnectTime time.Time + // VersionInfo is reported in the router's hello and is not persisted, so it is only populated on the + // instance built for a control-channel connection. It is nil on an instance loaded from the database. + // Read it from GetConnected rather than from whatever instance is to hand. VersionInfo *versions.VersionInfo routerLinks RouterLinks Cost uint16 diff --git a/controller/network/link_validation_test.go b/controller/network/link_validation_test.go new file mode 100644 index 000000000..a45770729 --- /dev/null +++ b/controller/network/link_validation_test.go @@ -0,0 +1,99 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "testing" + + "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/inspect" + "github.com/openziti/ziti/v2/common/pb/mgmt_pb" + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// newDbLoadedSrcLink builds a link whose source is a router instance loaded from the database. Such an +// instance carries no version, since a router's version arrives in its hello and is not persisted. +func newDbLoadedSrcLink(t *testing.T, srcId string) *model.Link { + t.Helper() + src := &model.Router{} + src.Id = srcId + require.Nil(t, src.VersionInfo, "a database-loaded router carries no version") + + return &model.Link{Id: "l1", DstId: "dst", Src: src} +} + +// TestCheckLinkConns_UnknownVersionIsSkipped: with no connected instance there is no way to know what the +// router reports, so the comparison is skipped rather than failed, matching how a router too old to report +// conn info is treated. +func TestCheckLinkConns_UnknownVersionIsSkipped(t *testing.T) { + _, network, _ := newConnectTestNetwork(t) + + link := newDbLoadedSrcLink(t, "r1") + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, &inspect.LinkInspectDetail{}, result) + + require.True(t, result.IsValid, "an unknown router version must not make a link invalid") + require.Empty(t, result.Messages) +} + +// TestCheckLinkConns_UsesConnectedVersion: the version lives on the connected instance, so reading it off +// whatever instance the link references reports a link as invalid whenever that instance is one loaded from +// the database rather than the connected one. +func TestCheckLinkConns_UsesConnectedVersion(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + link := newDbLoadedSrcLink(t, "r1") + + connected := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + connected.VersionInfo = &versions.VersionInfo{Version: "v1.0.0"} + network.Router.MarkConnected(connected) + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, &inspect.LinkInspectDetail{}, result) + + require.True(t, result.IsValid, "the connected instance's version must be what decides the comparison") + require.Empty(t, result.Messages) +} + +// TestCheckLinkConns_ComparesWhenVersionKnown: once the version is known and high enough, the conn info +// comparison proceeds, so skipping above does not quietly disable the check. +func TestCheckLinkConns_ComparesWhenVersionKnown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + connected := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + connected.VersionInfo = &versions.VersionInfo{Version: "v1.6.6"} + network.Router.MarkConnected(connected) + + link := &model.Link{Id: "l1", DstId: "dst", Src: connected} + + // The router reports a connection the controller does not know about, which the comparison must catch. + routerLink := &inspect.LinkInspectDetail{ + Connections: []*inspect.LinkConnection{ + {Type: "default", Source: "127.0.0.1:1000", Dest: "127.0.0.1:2000"}, + }, + } + + result := &mgmt_pb.RouterLinkDetail{IsValid: true} + network.checkLinkConns(link, routerLink, result) + + // A conn count mismatch reports a message without marking the link invalid, so the message is what + // shows the comparison ran rather than being skipped. + require.NotEmpty(t, result.Messages, "a known version must let the conn info comparison run") + require.Contains(t, result.Messages[0], "len(ctrlConns)") +} diff --git a/controller/network/main_test.go b/controller/network/main_test.go new file mode 100644 index 000000000..cc300cab5 --- /dev/null +++ b/controller/network/main_test.go @@ -0,0 +1,35 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "os" + "testing" + + "github.com/michaelquigley/pfxlog" + "github.com/sirupsen/logrus" +) + +// TestMain configures logging once for the whole package, keeping the path-finding perf tests quiet. +// GlobalInit writes package-level logger state, so calling it from inside a test races any goroutine a +// previous test left running that logs while shutting down: a TestContext's api-session heartbeat +// collector, for instance, does a final flush after its close notification, and that flush logs. Doing +// this before any test starts leaves no concurrent reader to race with. +func TestMain(m *testing.M) { + pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) + os.Exit(m.Run()) +} diff --git a/controller/network/network.go b/controller/network/network.go index 42969f570..bf00c51f5 100644 --- a/controller/network/network.go +++ b/controller/network/network.go @@ -33,6 +33,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4/protobufs" + "github.com/openziti/channel/v4" "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/foundation/v2/debugz" "github.com/openziti/foundation/v2/goroutines" @@ -265,9 +266,11 @@ func (network *Network) GetConnectedRouter(routerId string) *model.Router { return network.Router.GetConnected(routerId) } -func (network *Network) GetReloadedRouter(routerId string) (*model.Router, error) { - network.Router.RemoveFromCache(routerId) - return network.Router.Read(routerId) +// NewCtrlChanRouter returns the Router instance representing a new control-channel connection, with the +// channel recorded on it. Each connection gets its own instance, which is what lets the connect and +// disconnect paths tell two racing connections for one router apart. +func (network *Network) NewCtrlChanRouter(ch channel.Channel) (*model.Router, error) { + return network.Router.NewCtrlChanRouter(ch) } func (network *Network) GetRouter(routerId string) (*model.Router, error) { @@ -348,7 +351,47 @@ func (network *Network) ConnectedRouter(id string) bool { return network.Router.IsConnected(id) } -func (network *Network) ConnectRouter(r *model.Router) { +var ( + // ErrConnectRejected indicates a router connect was rejected because another connection for the same + // router is already current. It is returned by ConnectRouter and propagated out of the bind handler so + // NewChannel closes the rejected connection's underlay without starting rx or registering it; the + // router then redials. + ErrConnectRejected = errors.New("router connect rejected: another connection is already current") + + // ErrConnectChannelClosed indicates a router connect was refused because its control channel was + // already closed by the time the connect decision was made, so the connection must not be registered. + ErrConnectChannelClosed = errors.New("router connect rejected: control channel already closed") +) + +// IsConnectRejected reports whether err is (or wraps) a connect refusal that the router recovers from by +// redialing, so the accept path can log it at info rather than treating it as a bind failure. +func IsConnectRejected(err error) bool { + return errors.Is(err, ErrConnectRejected) || errors.Is(err, ErrConnectChannelClosed) +} + +// ConnectRouter registers r as the current connection for its router id, serialized per router. If the +// slot is already held by a different connection it rejects this one (returning ErrConnectRejected) and +// displaces the occupant so its teardown runs; the router redials into the freed slot. A connection whose +// channel is already closed is refused outright (ErrConnectChannelClosed) rather than registered. There is +// at most one connection per router in the connected map at a time. +func (network *Network) ConnectRouter(r *model.Router) error { + unlock := network.Router.LockConnectFor(r.Id) + defer unlock() // leak-safety net; idempotent, so the explicit unlocks below are the ones that matter + + if cur := network.Router.GetConnected(r.Id); cur != nil && cur != r { + // Displace the occupant outside the lock: the teardown acquires the stripe itself (we have + // released it), so there is no reentrant self-deadlock. + unlock() + network.displaceConnection(cur) + return ErrConnectRejected + } + + // Its close handler has already run and never fires again, so nothing would remove it from the + // connected map and every redial would bounce off a slot that can never be freed. + if r.Control == nil || r.Control.IsClosed() { + return ErrConnectChannelClosed + } + network.Link.BuildRouterLinks(r) network.Router.MarkConnected(r) @@ -359,7 +402,10 @@ func (network *Network) ConnectRouter(r *model.Router) { go h.RouterConnected(r) } } + unlock() + go network.ValidateTerminators(r) + return nil } func (network *Network) ValidateTerminators(r *model.Router) { @@ -462,9 +508,59 @@ func (n *Network) ValidateRouterErtTerminators(filter string, cb ErtTerminatorVa return int64(len(result.Entities)), evalF, nil } +// isCurrentConnection reports whether r is still the router's current, connected connection, by pointer +// identity against the connected map (mirrors the check in NotifyExistingLink). A stale or superseded +// connection returns false. +func (network *Network) isCurrentConnection(r *model.Router) bool { + return network.Router.GetConnected(r.Id) == r && r.Connected.Load() +} + +// displaceConnection removes cur, the connection occupying its router's connected slot, so that a redial +// can take the slot. Closing the channel is not sufficient on its own: if it is already closed, its close +// handler has already run and will never run again, so nothing would remove cur and every subsequent +// connect would be rejected against a slot that can never be freed. The teardown is therefore also +// invoked directly; it is gated on connection currency, so it is a no-op once the close handler has +// cleared the slot. Must be called with the router's connect stripe released, since the teardown +// acquires it. +func (network *Network) displaceConnection(cur *model.Router) { + if ch := cur.Control; ch != nil && !ch.IsClosed() { + if err := ch.Close(); err != nil { + pfxlog.Logger().WithError(err).WithField("routerId", cur.Id). + Error("error closing superseded control channel while rejecting connect") + } + } + network.DisconnectRouter(cur) +} + func (network *Network) DisconnectRouter(r *model.Router) { - // 1: remove Links for Router - for _, l := range r.GetLinks() { + // Lock-free pre-check: a stale/superseded disconnect (e.g. the old connection after a takeover) has + // nothing to tear down and must not touch the live connection's state; bail without blocking. + if !network.isCurrentConnection(r) { + return + } + + unlock := network.Router.LockConnectFor(r.Id) + defer unlock() + + // Re-check under the stripe: a newer connection may have taken over between the pre-check and the + // lock. The teardown is all-or-nothing and must not run against a superseded connection. + if !network.isCurrentConnection(r) { + return + } + + // Snapshot the router's links before marking it disconnected: MarkDisconnected clears the + // router's link set (routerLinks.Clear()), so a later r.GetLinks() would return nothing and + // the link-removal/reroute cascade below would be skipped entirely. + links := r.GetLinks() + + // Mark the router disconnected before the RerouteLink cascade, so reroute and everything it + // calls (shortestPath, connected-map reads) sees the dying router as gone. Otherwise reroute + // runs while the router still appears connected and can compute a replacement path through the + // very router that is being removed. + network.Router.MarkDisconnected(r) + + // remove Links for Router, rerouting circuits off any that were connected + for _, l := range links { wasConnected := l.CurrentState().Mode == model.Connected if l.Src.Id == r.Id { network.Link.Remove(l) @@ -473,8 +569,6 @@ func (network *Network) DisconnectRouter(r *model.Router) { network.RerouteLink(l) } } - // 2: remove Router - network.Router.MarkDisconnected(r) for _, h := range network.routerPresenceHandlers.Value() { h.RouterDisconnected(r) @@ -488,23 +582,33 @@ func (network *Network) NotifyExistingLink(srcRouter *model.Router, reportedLink WithField("destRouterId", reportedLink.DestRouterId). WithField("iteration", reportedLink.Iteration) + // Publish under the stripe DisconnectRouter holds: checking currency and then publishing without it + // lets a report recreate a link after the teardown has snapshotted and cleared it. Events go out after + // the unlock, since a dispatcher may be slow and this stripe is shared with connect and disconnect. + unlock := network.Router.LockConnectFor(srcRouter.Id) + src := network.Router.GetConnected(srcRouter.Id) if src == nil { + unlock() log.Info("ignoring links message processed after router disconnected") return } if src != srcRouter || !srcRouter.Connected.Load() { + unlock() log.Info("ignoring links message processed from old router connection") return } dst := network.Router.GetConnected(reportedLink.DestRouterId) + link, created := network.Link.RouterReportedLink(reportedLink, src, dst) + + unlock() + if dst == nil { network.NotifyLinkIdEvent(reportedLink.Id, event.LinkFromRouterDisconnectedDest) } - link, created := network.Link.RouterReportedLink(reportedLink, src, dst) if created { network.NotifyLinkEvent(link, event.LinkFromRouterNew) log.Info("router reported link added") @@ -1544,9 +1648,14 @@ func (network *Network) checkLinkConns(ctrlLink *model.Link, routerLink *inspect }) } - // ensure that conn info is being reported + // The version comes from the hello, so only the connected instance has it. Not knowing it means the + // comparison cannot be made, which is not a fault of the link. if srcR := ctrlLink.Src; srcR != nil { - hasMinVersion, err := srcR.VersionInfo.HasMinimumVersion("v1.6.6") + connectedSrc := network.Router.GetConnected(srcR.Id) + if connectedSrc == nil || connectedSrc.VersionInfo == nil { + return + } + hasMinVersion, err := connectedSrc.VersionInfo.HasMinimumVersion("v1.6.6") if err != nil { result.IsValid = false result.Messages = append(result.Messages, err.Error()) diff --git a/controller/network/network_path.go b/controller/network/network_path.go index e7df0ee46..ff88abb12 100644 --- a/controller/network/network_path.go +++ b/controller/network/network_path.go @@ -144,6 +144,16 @@ func (network *Network) shortestPath(srcR *model.Router, dstR *model.Router) ([] return nil, 0, errors.New("not routable (!srcR||!dstR)") } + // The graph below is pointer-keyed, so an endpoint held as a different instance of the same router is + // not a node in it and the search reports the router unroutable from itself. Callers legitimately hold + // other instances: each connection has its own, and the router cache holds a database-loaded one. + if connected := network.Router.GetConnected(srcR.Id); connected != nil { + srcR = connected + } + if connected := network.Router.GetConnected(dstR.Id); connected != nil { + dstR = connected + } + if srcR == dstR { return []*model.Router{srcR}, 0, nil } diff --git a/controller/network/route_perf_test.go b/controller/network/route_perf_test.go index 1b8dfecd4..2deddca44 100644 --- a/controller/network/route_perf_test.go +++ b/controller/network/route_perf_test.go @@ -22,14 +22,9 @@ import ( "testing" "github.com/openziti/ziti/v2/controller/model" - - "github.com/michaelquigley/pfxlog" - "github.com/sirupsen/logrus" ) func TestShortestPathAgainstEstablished(t *testing.T) { - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) - ctx := model.NewTestContext(t) defer ctx.Cleanup() @@ -151,7 +146,6 @@ func TestShortestPathAgainstEstablished(t *testing.T) { func BenchmarkShortestPathPerfWithRouterChanges(b *testing.B) { b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() @@ -242,7 +236,6 @@ type expectedRoute struct { func BenchmarkShortestPathPerf(b *testing.B) { b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() @@ -314,7 +307,6 @@ func BenchmarkShortestPathPerf(b *testing.B) { func BenchmarkMoreRealisticShortestPathPerf(b *testing.B) { //b.StopTimer() - pfxlog.GlobalInit(logrus.WarnLevel, pfxlog.DefaultOptions()) ctx := model.NewTestContext(b) defer ctx.Cleanup() diff --git a/controller/network/router_connect_test.go b/controller/network/router_connect_test.go new file mode 100644 index 000000000..f4560c732 --- /dev/null +++ b/controller/network/router_connect_test.go @@ -0,0 +1,383 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/openziti/channel/v4" + "github.com/openziti/transport/v2" + "github.com/openziti/transport/v2/tcp" + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + "github.com/openziti/ziti/v2/controller/change" + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// fakeCtrlChannel is a minimal ctrlchan.CtrlChannel double for connect/disconnect lifecycle tests. +// It embeds the interface (unimplemented methods panic if called), and implements only the close-related +// methods the reject/kick path exercises. onClose, if set, runs synchronously on the first Close to +// simulate the real channel close handler firing DisconnectRouter. +type fakeCtrlChannel struct { + ctrlchan.CtrlChannel + closed atomic.Bool + onClose func() +} + +func (f *fakeCtrlChannel) Close() error { + if f.closed.CompareAndSwap(false, true) && f.onClose != nil { + f.onClose() + } + return nil +} + +func (f *fakeCtrlChannel) IsClosed() bool { return f.closed.Load() } + +func (f *fakeCtrlChannel) IsConnected() bool { return !f.closed.Load() } + +func newConnectTestNetwork(t *testing.T) (*model.TestContext, *Network, transport.Address) { + ctx := model.NewTestContext(t) + t.Cleanup(ctx.Cleanup) + + config := newTestConfig(ctx) + t.Cleanup(func() { close(config.closeNotify) }) + + network, err := NewNetwork(config, ctx) + require.NoError(t, err) + + addr, err := tcp.AddressParser{}.Parse("tcp:0.0.0.0:0") + require.NoError(t, err) + + return ctx, network, addr +} + +// TestConnectRouter_RejectsAndKicksWhenBusy: a connect into an occupied slot returns ErrConnectRejected, +// kicks the occupant (whose teardown clears the slot), and a subsequent redial then connects cleanly. +func TestConnectRouter_RejectsAndKicksWhenBusy(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + currentCh := &fakeCtrlChannel{} + cur := model.NewRouterForTest("r1", "", addr, currentCh, 0, false) + // Simulate the real close handler: closing the occupant runs its DisconnectRouter. + currentCh.onClose = func() { network.DisconnectRouter(cur) } + + require.NoError(t, network.ConnectRouter(cur)) + require.Equal(t, cur, network.Router.GetConnected("r1")) + + newCh := &fakeCtrlChannel{} + rNew := model.NewRouterForTest("r1", "", addr, newCh, 0, false) + err := network.ConnectRouter(rNew) + require.ErrorIs(t, err, ErrConnectRejected) + require.True(t, IsConnectRejected(err)) + require.True(t, currentCh.IsClosed(), "occupant should have been kicked") + require.Nil(t, network.Router.GetConnected("r1"), "kicked occupant's teardown should clear the slot") + require.False(t, rNew.Connected.Load(), "rejected connect must not be registered") + + // Redial into the now-clear slot succeeds. + rRedial := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rRedial)) + require.Equal(t, rRedial, network.Router.GetConnected("r1")) + require.True(t, rRedial.Connected.Load()) +} + +// TestConnectRouter_SetsUpWhenClear: a connect into an empty slot registers the router. +func TestConnectRouter_SetsUpWhenClear(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + require.Equal(t, r, network.Router.GetConnected("r1")) + require.True(t, r.Connected.Load()) +} + +// TestDisconnectRouter_IgnoresStaleConnection: a disconnect for a superseded connection must not disturb +// the current one (the §9 race guard). +func TestDisconnectRouter_IgnoresStaleConnection(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + rNew := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rNew)) + require.Equal(t, rNew, network.Router.GetConnected("r1")) + + // A stale disconnect for a different (old) instance of the same router id. + rOld := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + network.DisconnectRouter(rOld) + + require.Equal(t, rNew, network.Router.GetConnected("r1"), "stale disconnect must not evict the current connection") + require.True(t, rNew.Connected.Load()) +} + +// TestDisconnectRouter_CurrentTearsDown: a disconnect for the current connection clears it. +func TestDisconnectRouter_CurrentTearsDown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + require.Equal(t, r, network.Router.GetConnected("r1")) + + network.DisconnectRouter(r) + require.Nil(t, network.Router.GetConnected("r1")) + require.False(t, r.Connected.Load()) +} + +// TestReject_DisplacesOccupantThatCannotTearItselfDown is the regression guard against a permanent +// reject loop. A connection whose channel closes without its close handler running can never remove +// itself from the connected map, so a reject that merely closed the channel would leave the slot occupied +// forever and every redial would be rejected against a slot nothing can free. The reject must displace +// the occupant itself, so the next redial finds the slot clear. +func TestReject_DisplacesOccupantThatCannotTearItselfDown(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + // No onClose: the occupant's channel goes closed without any teardown running, modeling both a + // channel already closed before the reject and one whose close handler has already run. + currentCh := &fakeCtrlChannel{} + cur := model.NewRouterForTest("r1", "", addr, currentCh, 0, false) + require.NoError(t, network.ConnectRouter(cur)) + require.Equal(t, cur, network.Router.GetConnected("r1")) + currentCh.closed.Store(true) + + rNew := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.ErrorIs(t, network.ConnectRouter(rNew), ErrConnectRejected) + require.Nil(t, network.Router.GetConnected("r1"), "reject must displace an occupant that cannot tear itself down") + require.False(t, cur.Connected.Load()) + require.False(t, rNew.Connected.Load(), "rejected connect must not be registered") + + // The redial therefore makes progress instead of bouncing off the slot forever. + rRedial := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rRedial)) + require.Equal(t, rRedial, network.Router.GetConnected("r1")) + require.True(t, rRedial.Connected.Load()) +} + +// TestConnectRouter_RefusesAlreadyClosedChannel: a connect whose channel is already closed must be +// refused rather than registered. Registering it would put a connection in the connected map that no +// disconnect can ever remove, since its close handler has already run, wedging the router out of this +// controller permanently. +func TestConnectRouter_RefusesAlreadyClosedChannel(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + deadCh := &fakeCtrlChannel{} + deadCh.closed.Store(true) + r := model.NewRouterForTest("r1", "", addr, deadCh, 0, false) + + err := network.ConnectRouter(r) + require.ErrorIs(t, err, ErrConnectChannelClosed) + require.True(t, IsConnectRejected(err), "an already-closed channel is a refusal the router redials after") + require.Nil(t, network.Router.GetConnected("r1"), "a closed connection must not occupy the slot") + require.False(t, r.Connected.Load()) + + // A subsequent healthy connect is unaffected. + rOk := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(rOk)) + require.Equal(t, rOk, network.Router.GetConnected("r1")) +} + +// TestConnectDisconnectRace exercises concurrent connect/disconnect for one router id under the race +// detector, asserting the map never ends holding a disconnected router. +func TestConnectDisconnectRace(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + var wg sync.WaitGroup + for i := 0; i < 64; i++ { + wg.Add(1) + go func(connect bool) { + defer wg.Done() + if connect { + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + _ = network.ConnectRouter(r) + } else if cur := network.Router.GetConnected("r1"); cur != nil { + network.DisconnectRouter(cur) + } + }(i%2 == 0) + } + wg.Wait() + + if cur := network.Router.GetConnected("r1"); cur != nil { + require.True(t, cur.Connected.Load(), "map must not hold a disconnected router") + } +} + +// fakeConnectChannel is a channel.MultiChannel double reporting a fixed router id and a real +// ListenerCtrlChannel as its underlay handler, which is what the accept path records on the router. It +// embeds the interface, so any method these tests do not exercise panics rather than returning a zero value. +type fakeConnectChannel struct { + channel.MultiChannel + id string + underlayHandler channel.UnderlayHandler +} + +func (f *fakeConnectChannel) Id() string { return f.id } +func (f *fakeConnectChannel) GetUnderlayHandler() channel.UnderlayHandler { return f.underlayHandler } +func (f *fakeConnectChannel) SetLogicalName(string) {} + +func newFakeConnectChannel(routerId string) *fakeConnectChannel { + ctrlCh := ctrlchan.NewListenerCtrlChannel() + ch := &fakeConnectChannel{id: routerId, underlayHandler: ctrlCh} + // The channel framework hands the ctrl channel its channel on creation; do the same here so the + // double records it the way a real connection would. + ctrlCh.ChannelCreated(ch) + return ch +} + +// newPersistedRouter stores a router so the connect path can load it. Create publishes the instance it was +// given into the router cache, which is the instance a connection must not be handed. +func newPersistedRouter(t *testing.T, network *Network, addr transport.Address, id string) *model.Router { + t.Helper() + router := model.NewRouterForTest(id, "", addr, nil, 0, false) + require.NoError(t, network.Router.Create(router, change.New())) + return router +} + +// TestNewCtrlChanRouter_InstanceIsNotShared is the guard on the assumption every currency check in the +// connect and disconnect paths rests on: each connection gets its own Router instance. Two connections +// sharing one instance are indistinguishable to those checks, so a connect into an occupied slot is not +// rejected and the first connection's teardown dismantles the second's registration, leaving a live +// control channel whose router is not registered and can never re-register. +func TestNewCtrlChanRouter_InstanceIsNotShared(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + cached := newPersistedRouter(t, network, addr, "r1") + + first, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + require.NoError(t, err) + second, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + require.NoError(t, err) + + require.NotSame(t, first, second, "each connection must get its own router instance") + require.NotSame(t, cached, first, "a connection must not be handed the cached instance") + require.NotSame(t, cached, second, "a connection must not be handed the cached instance") + + // The connection's instance must not become the cached one either, or the next connection would be + // handed it. + readBack, err := network.Router.Read("r1") + require.NoError(t, err) + require.NotSame(t, first, readBack, "a connection's instance must stay out of the router cache") + require.NotSame(t, second, readBack, "a connection's instance must stay out of the router cache") +} + +// TestNewCtrlChanRouter_ConcurrentConnectsAreNotShared covers the way instances came to be shared: the +// load was an eviction followed by a read-through read, so two connects racing could both evict and the +// later read could then hit the instance the earlier one had just published. +func TestNewCtrlChanRouter_ConcurrentConnectsAreNotShared(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + newPersistedRouter(t, network, addr, "r1") + + const connects = 16 + start := make(chan struct{}) + results := make([]*model.Router, connects) + var wg sync.WaitGroup + + for i := 0; i < connects; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + <-start + r, err := network.NewCtrlChanRouter(newFakeConnectChannel("r1")) + if err == nil { + results[idx] = r + } + }(i) + } + + close(start) + wg.Wait() + + seen := map[*model.Router]int{} + for idx, r := range results { + require.NotNil(t, r, "connect %d failed to load a router", idx) + seen[r]++ + } + require.Len(t, seen, connects, "every concurrent connect must get its own router instance") +} + +// TestNewCtrlChanRouter_RecordsTheChannel: the instance arrives carrying its connection, so no caller has +// to remember to attach it, and the channel it carries is the one it was built for. +func TestNewCtrlChanRouter_RecordsTheChannel(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + newPersistedRouter(t, network, addr, "r1") + + ch := newFakeConnectChannel("r1") + r, err := network.NewCtrlChanRouter(ch) + require.NoError(t, err) + + require.NotNil(t, r.Control, "the connection's channel must be recorded") + require.Same(t, ch, r.Control.GetChannel(), "the recorded channel must be the one the instance was built for") + require.False(t, r.ConnectTime.IsZero(), "connect time must be recorded") + require.False(t, r.Connected.Load(), "loading a router must not register it as connected") +} + +// TestNewCtrlChanRouter_UnknownRouter: a channel from a router the controller has no record of is refused +// rather than yielding an empty instance. +func TestNewCtrlChanRouter_UnknownRouter(t *testing.T) { + _, network, _ := newConnectTestNetwork(t) + + r, err := network.NewCtrlChanRouter(newFakeConnectChannel("nope")) + require.Error(t, err) + require.Nil(t, r) +} + +// TestNotifyExistingLink_RaceDisconnect is the invariant that link publication and disconnect teardown +// cannot interleave: once a router is disconnected, no link may remain naming it as its source. +// +// Checking currency and then publishing without holding the router's connect stripe is a check-then-act. +// A report can find the connection current, and by the time it reaches the link manager the teardown has +// already taken its snapshot of the router's links and cleared them, so the link is recreated after +// everything that would have removed it has run. It is then invisible to the router (its index was +// cleared) while still in the controller's link table with a disconnected source, and a reconnect +// reporting the same iteration can adopt that stale source rather than rebuilding the link. +// +// Run under -race, and repeated, since the window is small. +func TestNotifyExistingLink_RaceDisconnect(t *testing.T) { + _, network, addr := newConnectTestNetwork(t) + + // The destination is left unconnected: only the source router's disconnect can recreate a stale link, + // so connecting a second router would add the peer-state sync to the race for no extra coverage. + for i := 0; i < 200; i++ { + r := model.NewRouterForTest("r1", "", addr, &fakeCtrlChannel{}, 0, false) + require.NoError(t, network.ConnectRouter(r)) + + reported := &ctrl_pb.RouterLinks_RouterLink{ + Id: fmt.Sprintf("l-%d", i), + DestRouterId: "r2", + LinkProtocol: "tls", + DialAddress: "tcp:localhost:1234", + Iteration: 1, + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + network.NotifyExistingLink(r, reported) + }() + go func() { + defer wg.Done() + network.DisconnectRouter(r) + }() + wg.Wait() + + require.Nil(t, network.Router.GetConnected("r1"), "the router must be disconnected") + for _, l := range network.Link.All() { + require.NotEqual(t, "r1", l.Src.Id, + "iteration %d: a link published by a router that has been disconnected must not survive", i) + } + } +} diff --git a/controller/network/router_messaging.go b/controller/network/router_messaging.go index 22f7a264f..f4d4640e2 100644 --- a/controller/network/router_messaging.go +++ b/controller/network/router_messaging.go @@ -201,6 +201,13 @@ func (self *RouterMessaging) syncStates() { continue } + // Sending on a closed channel fails immediately, so keeping these queued retries as fast as the + // event loop spins. Reconnecting resyncs everything, so discard them. + if ch := notifyRouter.Control; ch == nil || ch.IsClosed() { + delete(self.routerUpdates, k) + continue + } + if v.sendInProgress.Load() { continue } @@ -243,15 +250,15 @@ func (self *RouterMessaging) syncStates() { currentStatesVersion := updates.version queueErr := self.routerCommPool.QueueOrError(func() { - ch := notifyRouter.Control - if ch == nil { - return - } - - success := true - if err := protobufs.MarshalTyped(changes).WithTimeout(time.Second * 1).SendAndWaitForWire(ch.GetDefaultSender()); err != nil { - pfxlog.Logger().WithError(err).WithField("routerId", notifyRouter.Id).Error("failed to send peer state changes to router") - success = false + // The done event must be queued on every path, including a missing channel: it is what clears + // sendInProgress, and without it this router's updates would never be attempted again. + success := false + if ch := notifyRouter.Control; ch != nil { + if err := protobufs.MarshalTyped(changes).WithTimeout(time.Second * 1).SendAndWaitForWire(ch.GetDefaultSender()); err != nil { + pfxlog.Logger().WithError(err).WithField("routerId", notifyRouter.Id).Error("failed to send peer state changes to router") + } else { + success = true + } } self.queueEvent(&routerPeerChangesSendDone{ diff --git a/controller/network/router_messaging_test.go b/controller/network/router_messaging_test.go new file mode 100644 index 000000000..03f727b12 --- /dev/null +++ b/controller/network/router_messaging_test.go @@ -0,0 +1,58 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "testing" + + "github.com/openziti/ziti/v2/controller/model" + "github.com/stretchr/testify/require" +) + +// TestSyncStates_DiscardsUpdatesForClosedChannel covers the retry behaviour for peer state changes bound +// for a router whose control channel has closed while it is still in the connected map. Sending on a +// closed channel fails immediately instead of blocking, and a failed send is retried as soon as the event +// loop turns, so retaining the updates spins the loop and floods the log. Reconnecting resyncs everything, +// so the pending changes are discarded instead. Updates for a router with a live channel must still be +// retained. +func TestSyncStates_DiscardsUpdatesForClosedChannel(t *testing.T) { + ctx, network, addr := newConnectTestNetwork(t) + + // A standalone instance, so syncStates can be driven directly; the Network's own RouterMessaging runs + // an event loop goroutine that would race these map accesses. + rm := NewRouterMessaging(ctx, network.RouterMessaging.routerCommPool) + + deadCh := &fakeCtrlChannel{} + dead := model.NewRouterForTest("r1", "", addr, deadCh, 0, false) + network.Router.MarkConnected(dead) + // The channel closes without any teardown running, so it stays in the connected map. + deadCh.closed.Store(true) + rm.routerUpdates["r1"] = &routerUpdates{changedRouters: map[string]struct{}{"other": {}}} + + // A live router with a send already in flight, which syncStates skips without queueing another. + liveCh := &fakeCtrlChannel{} + live := model.NewRouterForTest("r2", "", addr, liveCh, 0, false) + network.Router.MarkConnected(live) + liveUpdates := &routerUpdates{changedRouters: map[string]struct{}{"other": {}}} + liveUpdates.sendInProgress.Store(true) + rm.routerUpdates["r2"] = liveUpdates + + rm.syncStates() + + require.NotContains(t, rm.routerUpdates, "r1", "updates for a closed channel must be discarded, not retried") + require.Contains(t, rm.routerUpdates, "r2", "updates for a live channel must be retained") +} diff --git a/controller/sync_strats/rtx.go b/controller/sync_strats/rtx.go index 19560bd56..15f383025 100644 --- a/controller/sync_strats/rtx.go +++ b/controller/sync_strats/rtx.go @@ -335,8 +335,18 @@ type routerTxMap struct { internalMap cmap.ConcurrentMap[string, *RouterSender] //id -> RouterSender } +// Add installs routerMessageTxer as the sender for id, stopping any sender it replaces. Stopping the +// replaced sender here (rather than relying on RouterDisconnected) is required because the broker +// dispatches the old connection's RouterDisconnected asynchronously: on a reconnect/takeover the new +// connection's RouterConnected can install its sender before that async cleanup runs, which would +// otherwise orphan the old sender's goroutine. func (m *routerTxMap) Add(id string, routerMessageTxer *RouterSender) { - m.internalMap.Set(id, routerMessageTxer) + m.internalMap.Upsert(id, routerMessageTxer, func(exists bool, old *RouterSender, newValue *RouterSender) *RouterSender { + if exists && old != nil && old != newValue { + old.Stop() + } + return newValue + }) } func (m *routerTxMap) Get(id string) *RouterSender { diff --git a/controller/sync_strats/rtx_add_test.go b/controller/sync_strats/rtx_add_test.go new file mode 100644 index 000000000..f18dd164c --- /dev/null +++ b/controller/sync_strats/rtx_add_test.go @@ -0,0 +1,53 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package sync_strats + +import ( + "testing" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" +) + +// TestRouterTxMap_AddStopsReplaced verifies routerTxMap.Add stops the RouterSender it replaces. This is +// required under reject-if-busy: on a reconnect/takeover the new connection's RouterConnected can Add its +// sender before the old connection's RouterDisconnected runs (the broker dispatches it asynchronously), +// so without this the replaced sender's goroutine would be orphaned. +func TestRouterTxMap_AddStopsReplaced(t *testing.T) { + m := &routerTxMap{internalMap: cmap.New[*RouterSender]()} + + old := &RouterSender{closeNotify: make(chan struct{})} + old.running.Store(true) + m.Add("r1", old) + + newRtx := &RouterSender{closeNotify: make(chan struct{})} + newRtx.running.Store(true) + m.Add("r1", newRtx) + + require.False(t, old.running.Load(), "replaced sender should be stopped") + select { + case <-old.closeNotify: + default: + t.Fatal("replaced sender's closeNotify should be closed") + } + require.True(t, newRtx.running.Load(), "installed sender should still be running") + require.Equal(t, newRtx, m.Get("r1")) + + // Re-adding the same instance must not stop it. + m.Add("r1", newRtx) + require.True(t, newRtx.running.Load(), "re-adding the same sender must not stop it") +} From c3712949cbc27b904dda4d7075c2cdf3ce1c2b63 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Sat, 8 Aug 2026 19:48:50 -0400 Subject: [PATCH 62/73] Make a router's control channel registration single-owner. For #4264 Overlapping control channels to one controller share its id, so the registration for a controller could be held, given up, or judged by the wrong channel. Four distinct races followed from that, each found by fixing the one before it. A close handler removed the registration by id alone, so it could delete its replacement's entry. Nothing re-registers an already established channel, and the reconnect path treats a present entry as connected, so the router neither noticed nor redialed and stayed invisible to that controller. The check for an existing registration and the registration itself were separate unsynchronized steps, while UpdateControllerDetails decides whether to dial from the same state under a lock. Two dials to one controller do race: the initial endpoint dials carry no controller id, so idsBeingDialed cannot keep them apart. Both could find no registration, both register, and whichever lost was left with a live channel that nothing tracked or reconnected. A registration whose channel has been closed, or which has lost every underlay, satisfied a presence check while carrying nothing. The reconnect path asked only whether a registration existed, so an entry left behind by a channel that died convinced the router it was connected and suppressed the reconnect that would have fixed it. Refusing a duplicate channel was a permanent backoff error, which ended the retry loop for that controller for the life of the process. Losing a race is ordinary, and if the channel that won it later died there was nothing left to redial. - returns the created entry from Add so a close handler can present it as the identity to give up, and adds removeIfCurrent, which gives up a registration only when both the controller id and the entry match - adds handleChannelClose, replacing the duplicated close-handler bodies on the dial and accept paths, parameterized by reconnect delay - stops reporting ControllerDisconnected for a superseded close, which previously announced the replacement as disconnected, and stops notifying and reconnecting when the registration is already gone - takes the existing lock across the check and the registration in Add, which also closes the race against UpdateControllerDetails - hands the registration to the new channel before closing the one it displaces, so the displaced channel's close handler sees itself superseded rather than observing an empty registration and starting a reconnect that races the dial displacing it, and closes the displaced channel outside the lock, since closing runs close handlers on the calling goroutine - adds isUsable, requiring the channel to be open and to still hold an underlay, and uses it wherever the router decides whether to dial; both terms are needed, since a dial-side ctrl channel reports connectivity from its underlay count alone and says nothing about being closed - adds errDuplicateChannel carrying the id of the controller the dial reached, and duplicateResolved, which ends retries only while that controller's registration is still usable, so a winner that dies afterwards leaves the dial its work; the refused dial is where a controller id first becomes known to a dial started from a bare endpoint, so the refusal has to carry the id itself - logs controller registration and unregistration with the controller id and channel Leaves the channel selection helpers alone, since they choose which established channel to send on rather than whether the router is connected, and leaves the permanent error for a controller removed from the cluster, which genuinely cannot be dialed again. Original issue: #4196. (cherry picked from commit d50ff07db9a586d5392e461d7454325b962cb141) --- router/env/ctrls.go | 172 +++++++++++++----- router/env/ctrls_test.go | 364 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 494 insertions(+), 42 deletions(-) diff --git a/router/env/ctrls.go b/router/env/ctrls.go index 9f0369e13..c409d5b51 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -176,9 +176,9 @@ func (self *networkControllers) UpdateControllerDetails(controllers []*ctrl_pb.C } } - // Start dialing new controllers that aren't already dialing or connected + // Start dialing new controllers that aren't already dialing or usably connected for ctrlId, detail := range newIdSet { - if !self.idsBeingDialed.Has(ctrlId) && self.ctrls.Get(ctrlId) == nil { + if !self.idsBeingDialed.Has(ctrlId) && !isUsable(self.ctrls.Get(ctrlId)) { log.WithField("ctrlId", ctrlId).WithField("endpoints", detail.Endpoints).Info("adding new ctrl") changed = true self.connectToControllerWithBackoff(detail) @@ -236,8 +236,9 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C return backoff.Permanent(errors.New("controller removed before connection established")) } - // Already connected by ID - if self.ctrls.Get(detail.Id) != nil { + // Already connected by id. An unusable registration is not a reason to stop: the dial displaces it, + // where treating it as connected would end the retries and leave the router holding nothing. + if isUsable(self.ctrls.Get(detail.Id)) { log.Info("already connected to controller, exiting retry") return nil } @@ -245,7 +246,7 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C // Already connected by address for _, ep := range detail.Endpoints { for _, v := range self.ctrls.AsMap() { - if v.Address() == ep.Address { + if v.Address() == ep.Address && isUsable(v) { log.WithField("endpoint", ep.Address).Info("already connected to controller by address, exiting retry") return nil } @@ -279,6 +280,11 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C err = self.connectToController(ep.Address, addr) if err != nil { + if ctrlId, ok := self.duplicateResolved(err); ok { + log.WithField("endpoint", ep.Address).WithField("ctrlId", ctrlId). + Info("endpoint reached an already connected controller, exiting retry") + return nil + } log.WithField("endpoint", ep.Address).WithError(err).Error("unable to connect controller") } return err @@ -386,21 +392,13 @@ func (self *networkControllers) connectToController(endpoint string, addr transp id := binding.GetChannel().Id() binding.AddReceiveHandlerF(int32(edge_ctrl_pb.ContentType_CurrentIndexMessageType), self.handleRouterDataModelIndexUpdate) - if err = self.Add(endpoint, dialCtrlChan, binding.GetChannel(), underlay); err != nil { + ctrl, err := self.Add(endpoint, dialCtrlChan, binding.GetChannel(), underlay) + if err != nil { return err } - binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) { - ctrl := self.GetNetworkController(id) - self.ctrls.Delete(id) - if ctrl != nil { - self.notifyOfChange(ctrl, ControllerDisconnected) - } - if detail := self.getControllerDetail(id); detail != nil { - time.AfterFunc(time.Second, func() { - self.connectToControllerWithBackoff(detail) - }) - } + binding.AddCloseHandler(channel.CloseHandlerF(func(channel.Channel) { + self.handleChannelClose(id, ctrl, time.Second) })) return nil @@ -444,55 +442,145 @@ func (self *networkControllers) handleRouterDataModelIndexUpdate(m *channel.Mess } } -func (self *networkControllers) Add(address string, ctrlCh ctrlchan.CtrlChannel, ch channel.Channel, underlay channel.Underlay) error { +// Add registers a newly established control channel and returns the entry created for it. The returned +// entry is the identity a close handler must present to give the registration up again, so callers that +// wire a close handler have to hold on to it. +func (self *networkControllers) Add(address string, ctrlCh ctrlchan.CtrlChannel, ch channel.Channel, underlay channel.Underlay) (NetworkController, error) { ctrl := newNetworkCtrl(ctrlCh, address, self.heartbeatOptions) if versionValue, found := underlay.Headers()[channel.HelloVersionHeader]; found { if versionInfo, err := versions.StdVersionEncDec.Decode(versionValue); err == nil { ctrl.versionInfo = versionInfo } else { - return fmt.Errorf("could not parse version info from controller hello, closing connection (%w)", err) + return nil, fmt.Errorf("could not parse version info from controller hello, closing connection (%w)", err) } } else { - return errors.New("no version header provided") + return nil, errors.New("no version header provided") } - if existing := self.ctrls.Get(ch.Id()); existing != nil { - if !existing.Channel().IsClosed() && existing.IsConnected() { - // if an existing channel exists and is connected, reject the duplicate - return backoff.Permanent(fmt.Errorf("duplicate channel with id %v", ctrl.Channel().Id())) - } - // existing channel is closed or disconnected (0 underlays) — close it and accept new one - if !existing.Channel().IsClosed() { - if closeErr := existing.Channel().Close(); closeErr != nil { - pfxlog.Logger().WithError(closeErr).WithField("ch", existing.Channel().Label()).Error("error closing control channel") - } + log := pfxlog.Logger(). + WithField("ctrlId", ch.Id()). + WithField("ch", ch.Label()). + WithField("address", address) + + // Atomic against a concurrent Add for the same controller and against UpdateControllerDetails, which + // decides whether to dial from the same state. Two dials do race: initial endpoint dials carry no + // controller id, so idsBeingDialed cannot keep them apart. + self.lock.Lock() + + existing := self.ctrls.Get(ch.Id()) + if isUsable(existing) { + self.lock.Unlock() + return nil, &errDuplicateChannel{ctrlId: ch.Id()} + } + + // Hand the registration over before closing what it displaced, so the displaced close handler finds + // itself superseded rather than observing an empty registration and racing this dial with a redial. + self.ctrls.Put(ch.Id(), ctrl) + + self.lock.Unlock() + + log.Info("controller registered") + + // Closing a channel runs its close handlers on this goroutine, so it must happen outside the lock. + if existing != nil && !existing.Channel().IsClosed() { + if closeErr := existing.Channel().Close(); closeErr != nil { + log.WithError(closeErr).WithField("displacedCh", existing.Channel().Label()). + Error("error closing displaced control channel") } } - self.ctrls.Put(ch.Id(), ctrl) self.notifyOfChange(ctrl, ControllerAdded) - return nil + return ctrl, nil +} + +// errDuplicateChannel reports that a control channel was refused because the controller it reached is +// already usably connected. It carries the controller's id because a dial started from a bare endpoint has +// none of its own, so this is the only way the retry loop can learn which controller the endpoint resolved +// to and stop dialing it. +type errDuplicateChannel struct { + ctrlId string +} + +func (self *errDuplicateChannel) Error() string { + return fmt.Sprintf("controller %v is already connected on another channel", self.ctrlId) +} + +// duplicateResolved reports the controller id when err is a refusal by a controller whose registration is +// still usable, meaning another channel already did what this dial set out to do. A dial that only reaches +// an endpoint learns the controller's id here and nowhere else, which is why the refusal has to carry it. +// The registration is rechecked because the channel that won the race may since have died, leaving the +// dial's work to do after all. +func (self *networkControllers) duplicateResolved(err error) (string, bool) { + var duplicate *errDuplicateChannel + if errors.As(err, &duplicate) && isUsable(self.ctrls.Get(duplicate.ctrlId)) { + return duplicate.ctrlId, true + } + return "", false +} + +// isUsable reports whether a controller registration can still carry traffic. A registration whose channel +// has been closed, or which has lost every underlay, satisfies a presence check while carrying nothing. +// Every decision about whether the router is connected to a controller has to ask this rather than whether +// a registration exists, or an entry left behind by a channel that died leaves the router believing it is +// connected and suppresses the reconnect that would fix it. +func isUsable(ctrl NetworkController) bool { + return ctrl != nil && !ctrl.CtrlChannel().IsClosed() && ctrl.IsConnected() +} + +// removeIfCurrent gives up ctrl's registration for ctrlId, and reports whether it was still the +// registered entry. Overlapping channels to one controller share its id, so removing by id alone lets a +// superseded channel's close delete its replacement's registration. Nothing re-registers a channel that +// is already established, so the surviving channel would stay unreachable through the map and the router +// would be invisible to that controller until the channel happened to close. +func (self *networkControllers) removeIfCurrent(ctrlId string, ctrl NetworkController) bool { + return self.ctrls.DeleteIf(func(key string, val NetworkController) bool { + return key == ctrlId && val == ctrl + }) +} + +// handleChannelClose releases ctrl's registration and starts reconnecting to the controller. It is a +// no-op when ctrl is no longer the registered entry, either because a newer channel has taken over or +// because the controller was removed from the cluster. Reconnects are delayed by redialDelay, which +// paces a dial the controller rejects outright. +func (self *networkControllers) handleChannelClose(ctrlId string, ctrl NetworkController, redialDelay time.Duration) { + log := pfxlog.Logger().WithField("ctrlId", ctrlId).WithField("ch", ctrl.Channel().Label()) + + if !self.removeIfCurrent(ctrlId, ctrl) { + log.Info("superseded control channel closed, leaving the current registration in place") + return + } + + log.Info("control channel closed, controller unregistered") + self.notifyOfChange(ctrl, ControllerDisconnected) + + detail := self.getControllerDetail(ctrlId) + if detail == nil { + log.Info("controller is no longer known, not reconnecting") + return + } + + if redialDelay > 0 { + time.AfterFunc(redialDelay, func() { + self.connectToControllerWithBackoff(detail) + }) + } else { + self.connectToControllerWithBackoff(detail) + } } func (self *networkControllers) AcceptCtrlChannel(address string, ctrlCh ctrlchan.CtrlChannel, binding channel.Binding, underlay channel.Underlay) error { id := binding.GetChannel().Id() binding.AddReceiveHandlerF(int32(edge_ctrl_pb.ContentType_CurrentIndexMessageType), self.handleRouterDataModelIndexUpdate) - if err := self.Add(address, ctrlCh, binding.GetChannel(), underlay); err != nil { + ctrl, err := self.Add(address, ctrlCh, binding.GetChannel(), underlay) + if err != nil { return err } - binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) { - ctrl := self.GetNetworkController(id) - self.ctrls.Delete(id) - if ctrl != nil { - self.notifyOfChange(ctrl, ControllerDisconnected) - } - if detail := self.getControllerDetail(id); detail != nil { - self.connectToControllerWithBackoff(detail) - } + binding.AddCloseHandler(channel.CloseHandlerF(func(channel.Channel) { + self.handleChannelClose(id, ctrl, 0) })) return nil diff --git a/router/env/ctrls_test.go b/router/env/ctrls_test.go index 4838c0a96..b5a8337bf 100644 --- a/router/env/ctrls_test.go +++ b/router/env/ctrls_test.go @@ -17,7 +17,19 @@ package env import ( + "errors" + "fmt" + "sync" + "sync/atomic" "testing" + "time" + + "github.com/cenkalti/backoff/v4" + + "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/pb/ctrl_pb" + + cmap "github.com/orcaman/concurrent-map/v2" "github.com/openziti/channel/v4" "github.com/openziti/ziti/v2/common/ctrlchan" @@ -51,3 +63,355 @@ func Test_firstUnderlayHeaders(t *testing.T) { chType, _ := first.GetStringHeader(channel.TypeHeader) req.Equal(ctrlchan.ChannelTypeDefault, chType, "channel type preserved on the initial underlay copy") } + +// ctrlsTestChannel is a channel.Channel double reporting a fixed id and label. It embeds the interface, so +// any method these tests do not exercise panics rather than silently returning a zero value. +type ctrlsTestChannel struct { + channel.Channel + id string + label string + closed atomic.Bool + // underlaysLost models a channel that has lost every underlay without having been closed, which is the + // state Add treats as displaceable. + underlaysLost atomic.Bool +} + +func (self *ctrlsTestChannel) Id() string { return self.id } +func (self *ctrlsTestChannel) Label() string { return self.label } +func (self *ctrlsTestChannel) IsClosed() bool { return self.closed.Load() } +func (self *ctrlsTestChannel) Close() error { self.closed.Store(true); return nil } +func (self *ctrlsTestChannel) IsConnected() bool { + return !self.closed.Load() && !self.underlaysLost.Load() +} + +// ctrlsTestCtrlChannel is a ctrlchan.CtrlChannel double wrapping a ctrlsTestChannel. +type ctrlsTestCtrlChannel struct { + ctrlchan.CtrlChannel + ch *ctrlsTestChannel +} + +func (self *ctrlsTestCtrlChannel) GetChannel() channel.Channel { return self.ch } +func (self *ctrlsTestCtrlChannel) IsClosed() bool { return self.ch.IsClosed() } +func (self *ctrlsTestCtrlChannel) IsConnected() bool { return self.ch.IsConnected() } +func (self *ctrlsTestCtrlChannel) Close() error { return self.ch.Close() } + +// ctrlsTestUnderlay is a channel.Underlay double supplying only the hello headers Add reads. +type ctrlsTestUnderlay struct { + channel.Underlay + headers channel.Headers +} + +func (self *ctrlsTestUnderlay) Headers() map[int32][]byte { return self.headers } + +// newTestUnderlay supplies the version header Add requires of a controller hello. +func newTestUnderlay(t *testing.T) channel.Underlay { + t.Helper() + encoded, err := versions.StdVersionEncDec.Encode(&versions.VersionInfo{Version: "v1.0.0"}) + require.NoError(t, err) + return &ctrlsTestUnderlay{headers: channel.Headers{channel.HelloVersionHeader: encoded}} +} + +// newTestNetworkControllers builds a networkControllers with no DialEnv. Tests using it must not reach a +// path that dials, which for these tests means leaving controllerDetails empty so no reconnect is started. +func newTestNetworkControllers() *networkControllers { + return &networkControllers{ + heartbeatOptions: NewDefaultHeartbeatOptions(), + idsBeingDialed: cmap.New[struct{}](), + } +} + +// newTestNetworkCtrl returns a registration and the test channel backing it, so tests can drive the +// channel into the states the usability check distinguishes. +func newTestNetworkCtrl(nc *networkControllers, ctrlId string, label string) (*networkCtrl, *ctrlsTestChannel) { + ch := &ctrlsTestChannel{id: ctrlId, label: label} + return newNetworkCtrl(&ctrlsTestCtrlChannel{ch: ch}, "tls:"+ctrlId+":6262", nc.heartbeatOptions), ch +} + +// collectCtrlEvents registers a listener and returns a function draining the events seen so far. Listeners +// are notified on their own goroutine, so the buffer carries them back to the test. +func collectCtrlEvents(nc *networkControllers) func() []CtrlEvent { + received := make(chan CtrlEvent, 16) + nc.AddChangeListener(CtrlEventListenerFunc(func(event CtrlEvent) { + received <- event + })) + + return func() []CtrlEvent { + var result []CtrlEvent + for { + select { + case e := <-received: + result = append(result, e) + default: + return result + } + } + } +} + +// TestHandleChannelClose_SupersededLeavesCurrentRegistered guards against a router going permanently +// invisible to a controller. Overlapping channels to one controller share its id, so a close that gives up +// the registration by id alone can delete the live channel's entry. Nothing re-registers an already +// established channel, and the reconnect path treats a present entry as connected, so the router would +// neither notice nor redial. +func TestHandleChannelClose_SupersededLeavesCurrentRegistered(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + superseded, _ := newTestNetworkCtrl(nc, "ctrl1", "old") + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + + nc.ctrls.Put("ctrl1", current) + + // The superseded channel's close lands after its replacement has registered. + nc.handleChannelClose("ctrl1", superseded, 0) + + require.Same(t, current, nc.ctrls.Get("ctrl1"), + "a superseded channel's close must not unregister the channel that replaced it") + + require.Never(t, func() bool { + return len(drain()) > 0 + }, 100*time.Millisecond, 10*time.Millisecond, + "a superseded channel's close must not report the current channel as disconnected") +} + +// TestHandleChannelClose_CurrentUnregisters covers the ordinary case: the registered channel closing gives +// up its registration and reports the disconnect. +func TestHandleChannelClose_CurrentUnregisters(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", current) + + nc.handleChannelClose("ctrl1", current, 0) + + require.Nil(t, nc.ctrls.Get("ctrl1"), "the registered channel closing must unregister the controller") + + var events []CtrlEvent + require.Eventually(t, func() bool { + events = append(events, drain()...) + return len(events) > 0 + }, time.Second, 10*time.Millisecond, "expected a controller change event") + + require.Len(t, events, 1) + require.Equal(t, ControllerDisconnected, events[0].Type) + require.Same(t, current, events[0].Controller) +} + +// TestHandleChannelClose_AlreadyUnregistered: closeAndRemoveById drops the entry before closing the +// channel, so the close handler that follows must not report a second disconnect. +func TestHandleChannelClose_AlreadyUnregistered(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + + nc.handleChannelClose("ctrl1", current, 0) + + require.Never(t, func() bool { + return len(drain()) > 0 + }, 100*time.Millisecond, 10*time.Millisecond, + "closing a channel that is already unregistered must not report a disconnect") +} + +// TestRemoveIfCurrent_LeavesOtherControllers guards the key comparison: giving up one controller's +// registration must not touch another's. +func TestRemoveIfCurrent_LeavesOtherControllers(t *testing.T) { + nc := newTestNetworkControllers() + + ctrl1, _ := newTestNetworkCtrl(nc, "ctrl1", "ctrl1") + ctrl2, _ := newTestNetworkCtrl(nc, "ctrl2", "ctrl2") + nc.ctrls.Put("ctrl1", ctrl1) + nc.ctrls.Put("ctrl2", ctrl2) + + require.True(t, nc.removeIfCurrent("ctrl1", ctrl1)) + + require.Nil(t, nc.ctrls.Get("ctrl1")) + require.Same(t, ctrl2, nc.ctrls.Get("ctrl2")) + + require.False(t, nc.removeIfCurrent("ctrl1", ctrl1), "removing an absent registration must report no match") +} + +// TestIsUsable covers what counts as being connected to a controller. A registration is not evidence of +// connectivity on its own: the channel behind it may have been closed, or may have lost every underlay +// without closing, which is the state a control channel is left in when its group dies but nothing detects +// it. +func TestIsUsable(t *testing.T) { + nc := newTestNetworkControllers() + + require.False(t, isUsable(nil), "an absent registration is not usable") + + healthy, _ := newTestNetworkCtrl(nc, "ctrl1", "healthy") + require.True(t, isUsable(healthy)) + + closed, closedCh := newTestNetworkCtrl(nc, "ctrl2", "closed") + closedCh.closed.Store(true) + require.False(t, isUsable(closed), "a registration whose channel is closed is not usable") + + underlayless, underlaylessCh := newTestNetworkCtrl(nc, "ctrl3", "no-underlays") + underlaylessCh.underlaysLost.Store(true) + require.False(t, isUsable(underlayless), "a registration with no underlays left is not usable") +} + +// TestUpdateControllerDetails_ReconnectsWhenRegistrationUnusable: a registration left behind by a channel +// that died must not suppress the reconnect. Treating presence as connectivity is what leaves a router +// permanently invisible to a controller, since nothing else in this path would notice. +func TestUpdateControllerDetails_ReconnectsWhenRegistrationUnusable(t *testing.T) { + nc := newTestNetworkControllers() + + stale, staleCh := newTestNetworkCtrl(nc, "ctrl1", "stale") + staleCh.underlaysLost.Store(true) + nc.ctrls.Put("ctrl1", stale) + + // The detail carries no endpoints, so the reconnect gives up before reaching the network. + require.True(t, nc.UpdateControllerDetails([]*ctrl_pb.CtrlDetail{{Id: "ctrl1"}}), + "an unusable registration must not suppress the reconnect") +} + +// TestUpdateControllerDetails_LeavesUsableRegistrationAlone is the other half: a working channel must not +// be redialed on every controller detail update. +func TestUpdateControllerDetails_LeavesUsableRegistrationAlone(t *testing.T) { + nc := newTestNetworkControllers() + + current, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", current) + + require.False(t, nc.UpdateControllerDetails([]*ctrl_pb.CtrlDetail{{Id: "ctrl1"}}), + "a usable registration must not be redialed") + require.Same(t, current, nc.ctrls.Get("ctrl1")) +} + +// TestAdd_RejectsDuplicateOfUsableChannel: one usable channel per controller. The duplicate is refused and +// the established channel is left alone. +func TestAdd_RejectsDuplicateOfUsableChannel(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + established := &ctrlsTestChannel{id: "ctrl1", label: "established"} + first, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: established}, established, underlay) + require.NoError(t, err) + + duplicate := &ctrlsTestChannel{id: "ctrl1", label: "duplicate"} + second, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: duplicate}, duplicate, underlay) + require.Error(t, err) + require.Nil(t, second) + + require.Same(t, first, nc.ctrls.Get("ctrl1"), "the established channel must keep its registration") + require.False(t, established.IsClosed(), "the established channel must not be closed to admit a duplicate") +} + +// TestAdd_DuplicateErrorIsRetryable: losing a dial race is an ordinary race, not a permanent condition. A +// permanent refusal ends the retry loop for that controller for the life of the process, so if the winner +// later turns out to be unusable there is nothing left to redial. +func TestAdd_DuplicateErrorIsRetryable(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + established := &ctrlsTestChannel{id: "ctrl1", label: "established"} + _, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: established}, established, underlay) + require.NoError(t, err) + + duplicate := &ctrlsTestChannel{id: "ctrl1", label: "duplicate"} + _, err = nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: duplicate}, duplicate, underlay) + require.Error(t, err) + + var permanent *backoff.PermanentError + require.False(t, errors.As(err, &permanent), "a lost dial race must not stop the retry loop") + + var dup *errDuplicateChannel + require.True(t, errors.As(err, &dup), "the refusal must identify itself as a duplicate") + require.Equal(t, "ctrl1", dup.ctrlId, + "the refusal must carry the controller id, which a dial started from a bare endpoint has no other way to learn") +} + +// TestDuplicateResolved covers how the retry loop reacts to losing a race. The id checks at the top of a +// retry cannot help a dial started from a bare endpoint, since it has no controller id to check with, so +// the refusal itself is what ends the retries. +func TestDuplicateResolved(t *testing.T) { + nc := newTestNetworkControllers() + + winner, winnerCh := newTestNetworkCtrl(nc, "ctrl1", "winner") + nc.ctrls.Put("ctrl1", winner) + + // Wrapped the way connectToController wraps a dial failure. + refusal := fmt.Errorf("error connecting ctrl (%w)", &errDuplicateChannel{ctrlId: "ctrl1"}) + + ctrlId, ok := nc.duplicateResolved(refusal) + require.True(t, ok, "a refusal by a usably connected controller leaves this dial nothing to do") + require.Equal(t, "ctrl1", ctrlId) + + // The channel that won the race then dies, so the work is this dial's after all. + winnerCh.closed.Store(true) + _, ok = nc.duplicateResolved(refusal) + require.False(t, ok, "a refusal by a controller that is no longer usable must not end the retries") + + _, ok = nc.duplicateResolved(errors.New("connection refused")) + require.False(t, ok, "an unrelated failure must not be read as a lost race") +} + +// TestAdd_DisplacesUnusableChannel: a registration whose channel has no underlays left is not a reason to +// refuse a new one. The new channel takes the registration over and the old one is closed. +func TestAdd_DisplacesUnusableChannel(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + stale := &ctrlsTestChannel{id: "ctrl1", label: "stale"} + staleCtrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: stale}, stale, underlay) + require.NoError(t, err) + + // Simulate the channel losing its underlays without having been closed. + stale.underlaysLost.Store(true) + + replacement := &ctrlsTestChannel{id: "ctrl1", label: "replacement"} + replacementCtrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: replacement}, replacement, underlay) + require.NoError(t, err) + require.NotSame(t, staleCtrl, replacementCtrl) + + require.Same(t, replacementCtrl, nc.ctrls.Get("ctrl1"), "the new channel must hold the registration") + require.True(t, stale.IsClosed(), "the displaced channel must be closed") +} + +// TestAdd_ConcurrentForOneControllerRegistersOne is the check-then-put race. Initial endpoint dials carry +// no controller id, so idsBeingDialed cannot keep two dials to one controller apart; without the +// registration decision being atomic, both see no entry, both register, and whichever loses is left with a +// live channel that nothing tracks or reconnects. +func TestAdd_ConcurrentForOneControllerRegistersOne(t *testing.T) { + for i := 0; i < 50; i++ { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + const dials = 8 + start := make(chan struct{}) + var wg sync.WaitGroup + var registered atomic.Int32 + results := make([]NetworkController, dials) + + for j := 0; j < dials; j++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + ch := &ctrlsTestChannel{id: "ctrl1", label: fmt.Sprintf("dial-%d", idx)} + <-start + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + if err == nil { + results[idx] = ctrl + registered.Add(1) + } + }(j) + } + + close(start) + wg.Wait() + + require.Equal(t, int32(1), registered.Load(), + "exactly one concurrent dial to a controller may take the registration") + + var winner NetworkController + for _, ctrl := range results { + if ctrl != nil { + winner = ctrl + } + } + require.Same(t, winner, nc.ctrls.Get("ctrl1"), "the registration must belong to the dial that took it") + } +} From 6c817dbbcb9bd19d4f85a7a8a64b303984633a50 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Fri, 31 Jul 2026 11:21:07 -0400 Subject: [PATCH 63/73] Close a control channel whose controller stops answering heartbeats. For #4264 - closes the channel from the router's heartbeat check once no heartbeat response has arrived within closeUnresponsiveTimeout, which the controller already does for its side but the router did not - recovers a channel whose underlays are dead but not yet detected: until it closes, the channel keeps its group and dials only additional underlays, which the controller refuses once it has torn its side of the group down, so nothing re-establishes the group until the operating system abandons the connection, which takes roughly fifteen minutes - leaves being unresponsive as purely a selection signal, so a controller that is merely slow is deprioritized rather than disconnected, and a zero timeout disables the teardown - seeds the last-response time when the channel is created, so a channel is not judged before its first heartbeat could be answered - tests that a silent controller's channel is closed, that a slow but answering one is not, and that a zero timeout disables it Original issue: #4247. (cherry picked from commit db39bd0a86b82c3baa6175c9078db56922d6869c) --- router/env/ctrl.go | 25 +++++++- router/env/ctrl_heartbeat_test.go | 97 +++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 router/env/ctrl_heartbeat_test.go diff --git a/router/env/ctrl.go b/router/env/ctrl.go index 8d6971534..4b00e8ab8 100644 --- a/router/env/ctrl.go +++ b/router/env/ctrl.go @@ -20,6 +20,7 @@ import ( "sync/atomic" "time" + "github.com/michaelquigley/pfxlog" "github.com/openziti/foundation/v2/versions" "github.com/openziti/ziti/v2/common/ctrlchan" @@ -47,7 +48,10 @@ func newNetworkCtrl(ch ctrlchan.CtrlChannel, address string, heartbeatOptions *H address: address, heartbeatOptions: heartbeatOptions, } - result.lastContact.Store(time.Now().UnixMilli()) + // Seed the response time so a channel is not judged unresponsive before its first heartbeat has had a + // chance to be answered. + result.lastRx = time.Now().UnixMilli() + result.lastContact.Store(result.lastRx) return result } @@ -149,6 +153,25 @@ func (self *networkCtrl) CheckHeartBeat() { } else { self.unresponsive.Store(false) } + + // Unresponsive alone only deprioritizes. A channel whose underlays are dead but undetected keeps its + // group and dials only additional underlays, which the controller refuses once its side is gone, so + // nothing re-establishes the group until the OS abandons the connection minutes later. + if timeout := self.heartbeatOptions.CloseUnresponsiveTimeout; timeout > 0 && self.timeSinceLastResponse() > timeout { + log := pfxlog.Logger(). + WithField("address", self.address). + WithField("timeSinceLastResponse", self.timeSinceLastResponse()) + log.Error("no heartbeat response from controller in time, closing control channel") + if err := self.ch.Close(); err != nil { + log.WithError(err).Error("error closing unresponsive control channel") + } + } +} + +// timeSinceLastResponse reports how long it has been since the controller last answered a heartbeat. It is +// only read on the heartbeat goroutine, which is also the only writer of lastRx. +func (self *networkCtrl) timeSinceLastResponse() time.Duration { + return time.Duration(time.Now().UnixMilli()-self.lastRx) * time.Millisecond } func (self *networkCtrl) IsConnected() bool { diff --git a/router/env/ctrl_heartbeat_test.go b/router/env/ctrl_heartbeat_test.go new file mode 100644 index 000000000..e9314566d --- /dev/null +++ b/router/env/ctrl_heartbeat_test.go @@ -0,0 +1,97 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package env + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/stretchr/testify/require" +) + +// heartbeatTestChannel is a ctrlchan.CtrlChannel double that records Close calls. It embeds the interface, +// so any method the heartbeat path does not use panics rather than silently returning a zero value. +type heartbeatTestChannel struct { + ctrlchan.CtrlChannel + closed atomic.Bool +} + +func (self *heartbeatTestChannel) Close() error { + self.closed.Store(true) + return nil +} + +func (self *heartbeatTestChannel) IsClosed() bool { return self.closed.Load() } + +func (self *heartbeatTestChannel) IsConnected() bool { return !self.closed.Load() } + +func newHeartbeatTestCtrl(t *testing.T) (*networkCtrl, *heartbeatTestChannel) { + t.Helper() + ch := &heartbeatTestChannel{} + options := NewDefaultHeartbeatOptions() + options.CloseUnresponsiveTimeout = time.Minute + options.UnresponsiveAfter = 5 * time.Second + return newNetworkCtrl(ch, "tls:localhost:6262", options), ch +} + +// TestCheckHeartBeat_ClosesWhenNoResponse covers the recovery path for a control channel whose peer has +// stopped answering. Until it is closed, the channel keeps its group and only ever dials additional +// underlays, which the controller refuses once it has torn its side of the group down, so the router can +// never re-establish it. +func TestCheckHeartBeat_ClosesWhenNoResponse(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + + // A fresh channel is not judged unresponsive before its first heartbeat could be answered. + ctrl.CheckHeartBeat() + require.False(t, ch.IsClosed(), "a channel that has just connected must not be closed") + + // No response for longer than the timeout. + ctrl.lastRx = time.Now().Add(-2 * time.Minute).UnixMilli() + ctrl.CheckHeartBeat() + require.True(t, ch.IsClosed(), "a channel with no heartbeat response in time must be closed") +} + +// TestCheckHeartBeat_SlowButAnsweringIsNotClosed guards the control channel's preference for staying up: +// high latency only deprioritizes a controller when choosing between them. Closing a channel that is +// merely slow would add control-plane downtime, so only a peer that has stopped answering entirely is +// torn down. +func TestCheckHeartBeat_SlowButAnsweringIsNotClosed(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + + // Answering, but slowly enough to count as unresponsive for selection purposes. + ctrl.lastRx = time.Now().UnixMilli() + ctrl.latency.Store(int64(30 * time.Second)) + + ctrl.CheckHeartBeat() + + require.True(t, ctrl.IsUnresponsive(), "high latency should mark the controller unresponsive") + require.False(t, ch.IsClosed(), "a channel that is still answering must not be closed") +} + +// TestCheckHeartBeat_ZeroTimeoutDisablesClose: a zero timeout turns the teardown off, for deployments that +// would rather keep a silent channel than have it recycled. +func TestCheckHeartBeat_ZeroTimeoutDisablesClose(t *testing.T) { + ctrl, ch := newHeartbeatTestCtrl(t) + ctrl.heartbeatOptions.CloseUnresponsiveTimeout = 0 + + ctrl.lastRx = time.Now().Add(-time.Hour).UnixMilli() + ctrl.CheckHeartBeat() + + require.False(t, ch.IsClosed(), "a zero close timeout must disable the teardown") +} From e9194e8754edff090e0162585e60c59793ce5932 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 4 Aug 2026 19:45:17 -0400 Subject: [PATCH 64/73] Fail router startup only if no controller was ever reached. For #4264 The startup check sampled the current controller count once, when its one-shot timer landed. A router momentarily between control channels at that instant was killed as having failed to start, which under control channel churn is a routine state rather than a failure. Observed on a router that held registrations to two controllers until 25ms before the check fired, made no connection attempts during the whole timeout window because it had no need to, and was terminated anyway. Raising the timeout does not help, since it only moves the instant sampled. - adds NetworkControllers.EverConnected, set when a control channel is established and never cleared, so it answers whether a controller was ever reached rather than whether one is reachable now - sets it once the whole bind has succeeded rather than when the registration is created. Add runs in the first of several bind handlers and a later one can still refuse the channel, so registering is not the same as having established one: a router that only ever reached a controller it cannot talk to, an older one lacking a required capability for instance, would otherwise pass this check and retry forever instead of exiting - fails startup on that instead of the current controller count, preserving the intent that a router which can never reach a controller still exits - logs what the check saw, including whether a controller was ever reached and the current count, and logs the passing case too, so the outcome is recorded either way - tests that having reached a controller once survives losing every controller Original issue: #4248. (cherry picked from commit 62fbc6357dc58db63944546793308a4d91e22845) --- router/accepter.go | 4 ++++ router/env/ctrls.go | 24 +++++++++++++++++++++ router/env/ctrls_mock.go | 3 +++ router/env/ctrls_test.go | 46 ++++++++++++++++++++++++++++++++++++++++ router/router.go | 24 ++++++++++++++++----- router/router_test.go | 3 +++ 6 files changed, 99 insertions(+), 5 deletions(-) diff --git a/router/accepter.go b/router/accepter.go index 5ceb144cc..6a1721ae8 100644 --- a/router/accepter.go +++ b/router/accepter.go @@ -67,6 +67,10 @@ func (self *ctrlChannelAcceptor) HandleGroupedUnderlay(underlay channel.Underlay return nil, err } + // Only now is the channel fully bound: AcceptCtrlChannel registered it in the first bind handler, but + // a later one can still refuse it. + self.router.ctrls.MarkChannelEstablished() + self.router.NotifyOfReconnect(listenerCtrlChan) return mc, nil } diff --git a/router/env/ctrls.go b/router/env/ctrls.go index c409d5b51..92404414b 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -72,6 +72,8 @@ type DialEnv interface { } type NetworkControllers interface { + MarkChannelEstablished() + EverConnected() bool GetControllerDetails() map[string]*ctrl_pb.CtrlDetail UpdateControllerDetails(controllers []*ctrl_pb.CtrlDetail) bool ConnectToInitialEndpoints(endpoints []string) @@ -120,6 +122,26 @@ type networkControllers struct { leaderId concurrenz.AtomicValue[string] ctrlChangeListeners concurrenz.CopyOnWriteSlice[CtrlEventListener] controllerDetails concurrenz.AtomicValue[map[string]*ctrl_pb.CtrlDetail] + // everConnected records that a control channel was established at some point. It is never cleared, so it + // answers whether the router has ever reached a controller rather than whether it is reachable now. + everConnected atomic.Bool +} + +// MarkChannelEstablished records that a control channel was fully established. It must be called only +// once the whole bind has succeeded, not when the registration is created: Add runs in the first of +// several bind handlers, and a later one can still refuse the channel, at which point it is closed and +// unregistered. Setting this from Add would mean a router that only ever reached a controller it cannot +// talk to, an older one lacking a required capability for instance, counted as having connected, and so +// never failed its startup check and never exited. +func (self *networkControllers) MarkChannelEstablished() { + self.everConnected.Store(true) +} + +// EverConnected reports whether this router has established a control channel to any controller since +// starting. It stays true once set, so unlike the current controller count it is not affected by a router +// being momentarily between channels. +func (self *networkControllers) EverConnected() bool { + return self.everConnected.Load() } func (self *networkControllers) ControllersHaveMinVersion(version string) bool { @@ -426,6 +448,8 @@ func (self *networkControllers) connectToController(endpoint string, addr transp return fmt.Errorf("error connecting ctrl (%w)", err) } + self.MarkChannelEstablished() + // If there are multiple controllers we may have to catch up the controllers that connected later // with things that have already happened because we had state from other controllers, such as // links diff --git a/router/env/ctrls_mock.go b/router/env/ctrls_mock.go index 4938ae327..ef1a2ec32 100644 --- a/router/env/ctrls_mock.go +++ b/router/env/ctrls_mock.go @@ -50,6 +50,9 @@ func (m *MockNetworkControllers) UpdateControllerDetails(controllers []*ctrl_pb. return false } +func (m *MockNetworkControllers) MarkChannelEstablished() {} +func (m *MockNetworkControllers) EverConnected() bool { return true } + func (m *MockNetworkControllers) ConnectToInitialEndpoints(endpoints []string) { } diff --git a/router/env/ctrls_test.go b/router/env/ctrls_test.go index b5a8337bf..6b6e95682 100644 --- a/router/env/ctrls_test.go +++ b/router/env/ctrls_test.go @@ -415,3 +415,49 @@ func TestAdd_ConcurrentForOneControllerRegistersOne(t *testing.T) { require.Same(t, winner, nc.ctrls.Get("ctrl1"), "the registration must belong to the dial that took it") } } + +// TestEverConnected_NotSetByRegistrationAlone guards the difference between registering a control channel +// and having established one. Add runs in the first of several bind handlers, and a later one can still +// refuse the channel, after which it is closed and unregistered. If registering were enough to count, a +// router that only ever reached a controller it cannot talk to, an older one lacking a required capability +// for instance, would pass its startup check and retry forever instead of exiting. +func TestEverConnected_NotSetByRegistrationAlone(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + require.False(t, nc.EverConnected(), "a router that has not connected yet must report so") + + ch := &ctrlsTestChannel{id: "ctrl1", label: "first"} + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + require.NoError(t, err) + require.False(t, nc.EverConnected(), + "a registration is not an established channel; the bind can still be refused after Add returns") + + // Losing it without the bind ever completing must leave the router still having never connected, so + // the startup check can do its job. + nc.handleChannelClose("ctrl1", ctrl, 0) + require.False(t, nc.EverConnected(), "a channel that was never established must not count as one") +} + +// TestEverConnected_StaysTrueAcrossLosingEveryController is the distinction the startup check depends on. +// The check fires once, so asking whether a controller is reachable at that instant kills a router that +// happens to be between control channels, which is routine rather than a failure to start. Asking whether +// one was ever reached answers the question the check is actually for. +func TestEverConnected_StaysTrueAcrossLosingEveryController(t *testing.T) { + nc := newTestNetworkControllers() + underlay := newTestUnderlay(t) + + ch := &ctrlsTestChannel{id: "ctrl1", label: "first"} + ctrl, err := nc.Add("tls:ctrl1:6262", &ctrlsTestCtrlChannel{ch: ch}, ch, underlay) + require.NoError(t, err) + + // What the dial and accept paths call once the whole bind has succeeded. + nc.MarkChannelEstablished() + require.True(t, nc.EverConnected()) + + // Lose it, leaving no controllers at all, which is what the instant-count check used to trip on. + nc.handleChannelClose("ctrl1", ctrl, 0) + require.Empty(t, nc.GetAll(), "no controllers should remain") + require.True(t, nc.EverConnected(), + "having reached a controller once must not be forgotten when the connection is lost") +} diff --git a/router/router.go b/router/router.go index f729d6910..3f58e5699 100644 --- a/router/router.go +++ b/router/router.go @@ -886,12 +886,26 @@ func (self *Router) startControlPlane() error { if self.config.Ctrl.StartupTimeout > 0 { time.AfterFunc(self.config.Ctrl.StartupTimeout, func() { - if !self.isShutdown.Load() && len(self.ctrls.GetAll()) == 0 { - if os.Getenv("STACKDUMP_ON_FAILED_STARTUP") == "true" { - debugz.DumpStack() - } - pfxlog.Logger().Fatal("unable to connect to any controllers before timeout") + if self.isShutdown.Load() { + return } + + log := pfxlog.Logger(). + WithField("startupTimeout", self.config.Ctrl.StartupTimeout). + WithField("everConnected", self.ctrls.EverConnected()). + WithField("connectedCount", len(self.ctrls.GetAll())) + + // This fires once, so sampling the current count kills a router that happens to be between + // control channels when it lands, which is routine rather than a failure to start. + if self.ctrls.EverConnected() { + log.Info("startup connection check passed") + return + } + + if os.Getenv("STACKDUMP_ON_FAILED_STARTUP") == "true" { + debugz.DumpStack() + } + log.Fatal("unable to connect to any controllers before timeout") }) } diff --git a/router/router_test.go b/router/router_test.go index f351ac2e0..600e0dedc 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -51,6 +51,9 @@ func (m *mockNetworkControllers) UpdateControllerDetails(controllers []*ctrl_pb. return changed } +func (m *mockNetworkControllers) MarkChannelEstablished() {} +func (m *mockNetworkControllers) EverConnected() bool { return true } + func (m *mockNetworkControllers) ConnectToInitialEndpoints(endpoints []string) { for _, ep := range endpoints { m.endpoints[ep] = struct{}{} From 0563a59d66ee4544f3473e739db9731829e6a6d8 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 10 Aug 2026 17:14:45 -0400 Subject: [PATCH 65/73] Document and test what the router connect churn limit now guards. For #4264 routerConnectChurnLimit predates the per-router connect lock. It was added alongside the ability for a new control channel to take over from an established one, as the guard on how often that may happen, and it was also the only thing keeping two connections for one router out of the connected map. That second job is gone: at most one connection per router is now enforced under the per-router lock, where the decision is atomic. The check in the accept path runs against the connected map with no lock held, so it can only refuse a connection early that would be refused there anyway. Its first job remains, and is now the only thing doing it. ConnectRouter always displaces an occupant it does not recognise, so without the limit a spurious first-connection hello would tear down a healthy control channel and make the router redial. Nothing said so, and the field carried no godoc at all. - documents on the option what it protects, that it is churn policy rather than the uniqueness guarantee, and that zero always allows takeover - extracts the decision so it can be tested without standing up a network, and tests it: protected when just established, protected part way through the window, displaceable once past it, and never protected at zero The struct's field alignment shifts because a comment ends gofmt's alignment group; that part of the diff is whitespace only. Behaviour is unchanged. Worth noting for readers of the option: past the window, the established connection is now displaced and the connect refused, so the router redials into the freed slot, where previously the arriving connection took over directly. Same end state, one extra round trip, and nothing unvetted is registered on the way. Original issue: #4196. (cherry picked from commit ecbdb92ecb34cab9e9fd9db7afc5657940431902) --- controller/config/config_network.go | 24 +++++++++++------ controller/handler_ctrl/connect.go | 17 +++++++++++- controller/handler_ctrl/connect_test.go | 36 +++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/controller/config/config_network.go b/controller/config/config_network.go index 8a2a9e44c..66eb67bee 100644 --- a/controller/config/config_network.go +++ b/controller/config/config_network.go @@ -46,14 +46,22 @@ const ( ) type NetworkConfig struct { - CreateCircuitRetries uint32 - CycleSeconds uint32 - InitialLinkLatency time.Duration - IntervalAgeThreshold time.Duration - MetricsReportInterval time.Duration - MinRouterCost uint16 - PendingLinkTimeout time.Duration - RouteTimeout time.Duration + CreateCircuitRetries uint32 + CycleSeconds uint32 + InitialLinkLatency time.Duration + IntervalAgeThreshold time.Duration + MetricsReportInterval time.Duration + MinRouterCost uint16 + PendingLinkTimeout time.Duration + RouteTimeout time.Duration + // RouterConnectChurnLimit is how long an established router control channel is protected from being + // displaced by a new connection for the same router. A new connection arriving inside the window is + // refused; after it, the established connection is displaced and the router redials into the freed + // slot. Zero always allows takeover. + // + // This is churn policy, not the guarantee that a router has one connection: that is enforced under the + // per-router lock in Network.ConnectRouter. Its purpose is to stop a flapping router from repeatedly + // tearing down a working channel, since displacement is not free. RouterConnectChurnLimit time.Duration RouterComm struct { QueueSize uint32 diff --git a/controller/handler_ctrl/connect.go b/controller/handler_ctrl/connect.go index d53a1ea68..4108937e9 100644 --- a/controller/handler_ctrl/connect.go +++ b/controller/handler_ctrl/connect.go @@ -26,6 +26,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/ziti/v2/common/cert" + "github.com/openziti/ziti/v2/controller/model" "github.com/openziti/ziti/v2/controller/network" ) @@ -77,6 +78,20 @@ func isFirstCtrlConnection(hello *channel.Hello) bool { return first } +// withinChurnLimit reports whether an established connection is too new to be displaced by a new one. +// +// This is admission policy, not the uniqueness guarantee. At most one connection per router is enforced +// under the per-router lock in Network.ConnectRouter; this runs against the connected map with no lock +// held, so it can only avoid paying for a bind that would be refused there anyway. +// +// Displacing an established connection costs a round trip: the occupant's teardown runs, the connect is +// refused, and the router redials into the freed slot. A connection that has only just been established +// is therefore protected for churnLimit, so a flapping router cannot thrash a working channel. A zero +// limit disables the protection, making every new connection able to displace the current one. +func withinChurnLimit(connected *model.Router, churnLimit time.Duration) bool { + return time.Since(connected.ConnectTime) < churnLimit +} + func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates []*x509.Certificate) error { // Connections whose channel type is handled by a separate, self-validating acceptor (e.g. the raft // mesh) are validated there, so skip them. Everything else - router control channel types, @@ -111,7 +126,7 @@ func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates // connected and must not be rejected here. if isFirstCtrlConnection(hello) { if router := self.network.GetConnectedRouter(id); router != nil { - if time.Since(router.ConnectTime) < self.network.GetOptions().RouterConnectChurnLimit { + if withinChurnLimit(router, self.network.GetOptions().RouterConnectChurnLimit) { log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met") return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name) } diff --git a/controller/handler_ctrl/connect_test.go b/controller/handler_ctrl/connect_test.go index 749a3d074..d4636f07b 100644 --- a/controller/handler_ctrl/connect_test.go +++ b/controller/handler_ctrl/connect_test.go @@ -29,6 +29,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/identity" "github.com/openziti/ziti/v2/common/ctrlchan" + "github.com/openziti/ziti/v2/controller/model" "github.com/stretchr/testify/require" ) @@ -176,3 +177,38 @@ func Test_ConnectHandler_HandleConnection_SkipsSeparatelyValidated(t *testing.T) req.NoError(handler.HandleConnection(helloWithType(meshChannelType), []*x509.Certificate{forged.cert}), "mesh-typed connection is validated by its own acceptor and must be skipped here") } + +// Test_withinChurnLimit pins the admission policy that decides whether an already-connected router's +// channel may be displaced by a new connection. +// +// It is the only thing rate-limiting displacement. Network.ConnectRouter always displaces an occupant it +// does not recognise, so without this a spurious first-connection hello would tear down a healthy control +// channel and force the router to redial. Uniqueness itself is guaranteed under the per-router lock in +// ConnectRouter, not here, so this check exists purely to protect a working connection from churn. +func Test_withinChurnLimit(t *testing.T) { + connectedAt := func(d time.Duration) *model.Router { + r := &model.Router{ConnectTime: time.Now().Add(-d)} + r.Id = "r1" + return r + } + + tests := []struct { + name string + since time.Duration + churnLimit time.Duration + protected bool + }{ + {"a connection just established is protected", 0, time.Minute, true}, + {"still protected part way through the window", 30 * time.Second, time.Minute, true}, + {"displaceable once the window has passed", 2 * time.Minute, time.Minute, false}, + // A zero limit is a supported setting and means "always allow takeover", which is what the option + // existed to make configurable in the first place. + {"a zero limit protects nothing", 0, 0, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.protected, withinChurnLimit(connectedAt(tt.since), tt.churnLimit)) + }) + } +} From 7ffb8ef2bdc032fb09fbe6de6b9bcd79bd3bf078 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Tue, 11 Aug 2026 16:54:05 -0400 Subject: [PATCH 66/73] Retry the reconnect link announcement and wait for the wire. For #4264 A router announces its full link set once per controller reconnect and nothing re-offers or re-asks. That announcement is also the only message the controller treats as authoritative, so it is the only thing that prunes. A controller that misses it can neither route over the router's links nor drop the ones it should have, and neither side can tell: the links stay healthy on both ends. The announcement was sent with no deadline while holding the registry lock, so it waited for a send-queue slot indefinitely, stalling link accept and dial-succeeded for as long as the wait. Bounding it alone is not safe. Send returns once the message is queued and the deadline stays live, so the tx loop discards it if the queue drains too slowly, and nothing is told: a plain send's listener ignores the error. The announcement would have been reported sent, and the links marked synchronized, for a message that never left. - waits for the wire rather than the queue, so a discarded announcement is reported as the failure it is. This matches how the periodic notification and fault paths already send - retries the announcement, bounded, giving up if the channel closes since whatever replaces it announces again - logs giving up with what it costs: that controller cannot route over or prune this router's links until it reconnects Retrying the announcement itself, rather than clearing the marks that record links as announced and letting the periodic path re-offer them, is what makes this correct. That path sends without FullRefresh, so the controller cannot prune, and it sends nothing at all when there are no established links, which is exactly when stale controller state most needs clearing. The send timeout and retry delay are fields rather than constants because the timeout is the only way to distinguish a message that was queued and then discarded from one that reached the wire, so a test has to be able to reach it. Does not help when the reconnect announcement is never attempted at all. That needs the controller to ask for link state when it holds none. Original issue: #4196. (cherry picked from commit f36a62d10b0b4e95f53452f03a158859ad07ddac) --- router/link/link_registry.go | 95 ++++++++++++++++++----- router/link/link_registry_test.go | 125 ++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 20 deletions(-) diff --git a/router/link/link_registry.go b/router/link/link_registry.go index 31036ab9b..7461413f3 100644 --- a/router/link/link_registry.go +++ b/router/link/link_registry.go @@ -50,14 +50,16 @@ type Env interface { func NewLinkRegistry(routerEnv Env) xlink.Registry { result := &linkRegistryImpl{ - linkMap: map[string]xlink.Xlink{}, - linkByIdMap: map[string]xlink.Xlink{}, - ctrls: routerEnv.GetNetworkControllers(), - events: make(chan event, 16), - env: routerEnv, - destinations: map[string]*linkDest{}, - linkStateQueue: &linkStateHeap{}, - triggerNotifyC: make(chan struct{}, 1), + linkMap: map[string]xlink.Xlink{}, + linkByIdMap: map[string]xlink.Xlink{}, + ctrls: routerEnv.GetNetworkControllers(), + events: make(chan event, 16), + env: routerEnv, + destinations: map[string]*linkDest{}, + linkStateQueue: &linkStateHeap{}, + triggerNotifyC: make(chan struct{}, 1), + fullRefreshSendTimeout: fullRefreshSendTimeout, + fullRefreshRetryDelay: fullRefreshRetryDelay, } go result.run() @@ -73,12 +75,17 @@ type linkRegistryImpl struct { sync.Mutex ctrls env.NetworkControllers - env Env - destinations map[string]*linkDest - linkStateQueue *linkStateHeap - events chan event - triggerNotifyC chan struct{} - notifyInProgress atomic.Bool + env Env + destinations map[string]*linkDest + // fullRefreshSendTimeout and fullRefreshRetryDelay bound and pace the reconnect announcement. Fields so + // tests can reach the timeout, which is the only way to tell a message that was queued and then discarded + // from one that reached the wire. + fullRefreshSendTimeout time.Duration + fullRefreshRetryDelay time.Duration + linkStateQueue *linkStateHeap + events chan event + triggerNotifyC chan struct{} + notifyInProgress atomic.Bool } func (self *linkRegistryImpl) runGcLinkMetricsLoop() { @@ -388,7 +395,49 @@ func (self *linkRegistryImpl) Iter() <-chan xlink.Xlink { return result } +const ( + // fullRefreshSendTimeout bounds one attempt at the reconnect announcement. The registry lock is held for + // the attempt, so this is also how long link accept and dial-succeeded can stall. + fullRefreshSendTimeout = 5 * time.Second + + // fullRefreshSendAttempts bounds the retries. A router announces its full link set once per reconnect and + // nothing re-asks, so an announcement that never lands leaves the controller unable to route over those + // links and unable to prune the ones it should have dropped. + fullRefreshSendAttempts = 3 + + fullRefreshRetryDelay = time.Second +) + func (self *linkRegistryImpl) NotifyOfReconnect(ch channel.Channel) { + // Retried here rather than by clearing the announced marks and letting the periodic path pick it up: that + // path sends without FullRefresh, so the controller cannot prune, and it sends nothing at all when there + // are no established links, which is exactly when stale controller state most needs clearing. + // + // Called on its own goroutine (Router.NotifyOfReconnect), so waiting between attempts blocks nothing. + for attempt := 1; attempt <= fullRefreshSendAttempts; attempt++ { + if self.sendFullRefresh(ch) { + return + } + + if ch.IsClosed() { + return // whatever replaces this channel announces again + } + + if attempt < fullRefreshSendAttempts { + select { + case <-time.After(self.fullRefreshRetryDelay): + case <-self.env.GetCloseNotify(): + return + } + } + } + + pfxlog.Logger().WithField("ctrlId", ch.Id()). + Error("gave up announcing link states after reconnect; this controller cannot route over or prune this router's links until it reconnects") +} + +// sendFullRefresh announces every dialed link to one controller, reporting whether the announcement got out. +func (self *linkRegistryImpl) sendFullRefresh(ch channel.Channel) bool { self.Lock() defer self.Unlock() @@ -420,13 +469,19 @@ func (self *linkRegistryImpl) NotifyOfReconnect(ch channel.Channel) { } } - if err := protobufs.MarshalTyped(routerLinks).Send(ch); err != nil { - logrus.WithError(err).Error("failed to send router links on reconnect") - } else { - for _, f := range onComplete { - f() - } + // SendAndWaitForWire, not Send: Send returns once the message is queued, and the deadline stays live, so + // the tx loop discards it if the queue drains too slowly. Nothing is told, since a plain send's listener + // ignores the error, and this would report success and mark the links synchronized for an announcement + // that never left. Matches how the periodic path sends. + if err := protobufs.MarshalTyped(routerLinks).WithTimeout(self.fullRefreshSendTimeout).SendAndWaitForWire(ch); err != nil { + logrus.WithError(err).WithField("ctrlId", ch.Id()).Error("failed to send router links on reconnect") + return false } + + for _, f := range onComplete { + f() + } + return true } func (self *linkRegistryImpl) GetTraceDecoders() []channel.TraceMessageDecoder { diff --git a/router/link/link_registry_test.go b/router/link/link_registry_test.go index 291b1d6de..c5477e132 100644 --- a/router/link/link_registry_test.go +++ b/router/link/link_registry_test.go @@ -17,6 +17,8 @@ package link import ( + "math" + "sync/atomic" "testing" "time" @@ -31,6 +33,7 @@ import ( "github.com/openziti/ziti/v2/controller/idgen" "github.com/openziti/ziti/v2/router/env" "github.com/openziti/ziti/v2/router/xlink" + "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -356,3 +359,125 @@ func Test_gcLinkMetrics(t *testing.T) { checkLinkMetricsDoesntHave(linkId2, getRegistryMetrics()) checkLinkMetricsDoesntHave(linkId5, getRegistryMetrics()) } + +// flakySendChannel is a channel.Channel double whose sends fail a set number of times before succeeding. It +// embeds the interface, so any method these tests do not exercise panics rather than returning a zero value. +type flakySendChannel struct { + channel.Channel + failures int + sends atomic.Int32 + closed atomic.Bool + closeNotify chan struct{} +} + +func newFlakySendChannel(failures int) *flakySendChannel { + return &flakySendChannel{failures: failures, closeNotify: make(chan struct{})} +} + +func (self *flakySendChannel) Id() string { return "ctrl1" } +func (self *flakySendChannel) Label() string { return "ctrl1" } +func (self *flakySendChannel) IsClosed() bool { return self.closed.Load() } + +func (self *flakySendChannel) CloseNotify() <-chan struct{} { return self.closeNotify } + +func (self *flakySendChannel) Send(s channel.Sendable) error { + if int(self.sends.Add(1)) <= self.failures { + return errors.New("timeout waiting for space in send queue") + } + // What the tx loop does once the message is actually written. + s.SendListener().NotifyAfterWrite() + return nil +} + +// newReconnectTestRegistry builds a registry with no links and no event loop. An empty link set is a case +// worth covering rather than avoiding: the reconnect announcement is the only message that prunes, so a +// router with nothing to report still has to send one to clear stale controller state. +func newReconnectTestRegistry(t *testing.T) (*linkRegistryImpl, *testEnv) { + t.Helper() + routerEnv := newTestEnv() + t.Cleanup(func() { close(routerEnv.closeNotify) }) + + return &linkRegistryImpl{ + env: routerEnv, + ctrls: routerEnv.ctrls, + destinations: map[string]*linkDest{}, + linkMap: map[string]xlink.Xlink{}, + triggerNotifyC: make(chan struct{}, 1), + // Production's pacing is not this test's concern; the timeout has to stay reachable, though, since it + // is what distinguishes a discarded message from a delivered one. + fullRefreshSendTimeout: 20 * time.Millisecond, + fullRefreshRetryDelay: 0, + }, routerEnv +} + +// Test_NotifyOfReconnect_RetriesUntilTheAnnouncementLands covers the retry. A router announces its full link +// set once per controller reconnect and nothing re-asks, so an announcement lost to send-queue back-pressure +// leaves that controller unable to route over the router's links, and unable to prune the ones it should have +// dropped, until the next reconnect. +func Test_NotifyOfReconnect_RetriesUntilTheAnnouncementLands(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(2) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(3), ch.sends.Load(), + "the announcement should have been retried until it reached the wire") +} + +// Test_NotifyOfReconnect_GivesUpAfterItsAttempts: the retry is bounded, so a channel that never drains does +// not hold a goroutine indefinitely. +func Test_NotifyOfReconnect_GivesUpAfterItsAttempts(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(math.MaxInt32) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(fullRefreshSendAttempts), ch.sends.Load(), + "the retry must stop after its attempts rather than looping") +} + +// Test_NotifyOfReconnect_StopsWhenTheChannelCloses: a closed channel cannot be re-announced to, and whatever +// replaces it announces again, so retrying against it only delays the goroutine's exit. +func Test_NotifyOfReconnect_StopsWhenTheChannelCloses(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := newFlakySendChannel(math.MaxInt32) + ch.closed.Store(true) + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(1), ch.sends.Load(), + "a closed channel should be abandoned after the first failure, not retried") +} + +// acceptThenDiscardChannel models what a real channel does to a message queued behind a backlog: the send is +// accepted, and the message is dropped when its deadline expires before the queue drains. Nothing calls back, +// because a plain send's listener ignores the error. +type acceptThenDiscardChannel struct { + channel.Channel + sends atomic.Int32 + closeNotify chan struct{} +} + +func (self *acceptThenDiscardChannel) Id() string { return "ctrl1" } +func (self *acceptThenDiscardChannel) Label() string { return "ctrl1" } +func (self *acceptThenDiscardChannel) IsClosed() bool { return false } +func (self *acceptThenDiscardChannel) CloseNotify() <-chan struct{} { return self.closeNotify } + +func (self *acceptThenDiscardChannel) Send(channel.Sendable) error { + self.sends.Add(1) + return nil // queued, and never written +} + +// Test_NotifyOfReconnect_TreatsADiscardedAnnouncementAsFailure is the guard on how the announcement is sent. +// A plain Send returns once the message is queued, and the deadline stays live afterwards, so the tx loop can +// drop it with nobody told: a plain send's listener ignores the error. Reporting that as success marks the +// links announced and leaves the controller permanently without them. +func Test_NotifyOfReconnect_TreatsADiscardedAnnouncementAsFailure(t *testing.T) { + reg, _ := newReconnectTestRegistry(t) + ch := &acceptThenDiscardChannel{closeNotify: make(chan struct{})} + + reg.NotifyOfReconnect(ch) + + require.Equal(t, int32(fullRefreshSendAttempts), ch.sends.Load(), + "an announcement accepted into the queue but never written must count as a failure and be retried") +} From 22646c74874a8c35c1629e6d98ced328569cc0e2 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Wed, 12 Aug 2026 12:45:35 -0400 Subject: [PATCH 67/73] Report a control channel reconnect, not just re-offer its state. For #4264 A grouped control channel survives losing every underlay, so on a blip the registration stays put and neither Add nor the close handler runs. The underlay count reaching zero and coming back is the only signal that anything happened, and only half of it was sent: the reconnect re-offered state to the controller but never emitted ControllerReconnected, leaving networkControllers.NotifyOfReconnect with no callers at all. The disconnect side did emit its event, so the two were asymmetric. Nothing else covers the gap. A fresh dial reports itself through Add and ControllerAdded, but a flap reaches neither, so every listener keyed on connectivity sat waiting for an event that could not arrive. Hosted service re-establishment is the consumer on this branch, and it recovers only if some unrelated controller event happens to arrive. - emits ControllerReconnected alongside the state re-offer, giving the dead notifier its caller - moves the transition out of the dial closure so it can be tested without a network, keeping the channel liveness guard at the call site - swaps rather than stores the disconnected flag on the way back up, so simultaneous underlay changes report the edge once, matching the disconnect side Not a regression in this branch's rework: the same asymmetry is on main. It sits here because it is part of handling a router's control channel correctly, and because the fix is only safe to reason about alongside the connect and disconnect paths this branch already reworked. Original issue: #4196. (cherry picked from commit 8b123f30bc30957906eadd068c2c4fdc737bdda0) --- router/env/ctrls.go | 34 ++++++++++---- router/env/ctrls_test.go | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/router/env/ctrls.go b/router/env/ctrls.go index 92404414b..c10d25679 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -386,19 +386,14 @@ func (self *networkControllers) connectToController(endpoint string, addr transp // Track connectivity transitions for reconnect/disconnect notifications var wasDisconnected atomic.Bool - changeCallback := func(ch *ctrlchan.DialCtrlChannel, oldCount, newCount uint32) { + changeCallback := func(ch *ctrlchan.DialCtrlChannel, _, newCount uint32) { multiCh := ch.GetChannel() if multiCh == nil || multiCh.IsClosed() { return } - if wasDisconnected.Load() && newCount > 0 { + self.notifyOfConnectivityChange(ch.PeerId(), &wasDisconnected, newCount, func() { self.dialEnv.NotifyOfReconnect(ch) - wasDisconnected.Store(false) - } else if newCount == 0 { - if wasDisconnected.CompareAndSwap(false, true) { - self.NotifyOfDisconnect(ch.PeerId()) - } - } + }) } dialCtrlChan := ctrlchan.NewDialCtrlChannel(ctrlchan.DialCtrlChannelConfig{ @@ -610,6 +605,29 @@ func (self *networkControllers) AcceptCtrlChannel(address string, ctrlCh ctrlcha return nil } +// notifyOfConnectivityChange reports a control channel losing and regaining its last +// underlay. A grouped channel survives losing every underlay, so the registration stays +// and neither Add nor the close handler runs: these notifications are the only thing that +// tells anything the channel went away and came back. +// +// Both halves of the reconnect matter and are easy to confuse. notifyReconnect re-offers +// the state a controller may have missed while the channel was down, while the +// ControllerReconnected ctrl event is what listeners keyed on connectivity wait for. +// Sending only the first leaves those listeners believing the controller is still gone. +// +// wasDisconnected belongs to the one channel and is swapped rather than stored, so each +// edge is reported exactly once however many underlays come or go at the same moment. +func (self *networkControllers) notifyOfConnectivityChange(ctrlId string, wasDisconnected *atomic.Bool, newCount uint32, notifyReconnect func()) { + if newCount > 0 { + if wasDisconnected.CompareAndSwap(true, false) { + notifyReconnect() + self.NotifyOfReconnect(ctrlId) + } + } else if wasDisconnected.CompareAndSwap(false, true) { + self.NotifyOfDisconnect(ctrlId) + } +} + func (self *networkControllers) NotifyOfDisconnect(ctrlId string) { if ctrl := self.GetNetworkController(ctrlId); ctrl != nil { self.notifyOfChange(ctrl, ControllerDisconnected) diff --git a/router/env/ctrls_test.go b/router/env/ctrls_test.go index 6b6e95682..baad3d771 100644 --- a/router/env/ctrls_test.go +++ b/router/env/ctrls_test.go @@ -461,3 +461,100 @@ func TestEverConnected_StaysTrueAcrossLosingEveryController(t *testing.T) { require.True(t, nc.EverConnected(), "having reached a controller once must not be forgotten when the connection is lost") } + +// awaitCtrlEvents collects n controller events, failing if they do not arrive in time. +// Collection happens on the calling goroutine: testify runs Eventually and Never conditions +// on their own goroutine, so accumulating into a slice the test body also reads would race. +func awaitCtrlEvents(t *testing.T, drain func() []CtrlEvent, n int) []CtrlEvent { + t.Helper() + + var events []CtrlEvent + deadline := time.Now().Add(2 * time.Second) + for len(events) < n && time.Now().Before(deadline) { + events = append(events, drain()...) + if len(events) < n { + time.Sleep(5 * time.Millisecond) + } + } + + require.GreaterOrEqual(t, len(events), n, "expected %d controller events, got %v", n, events) + return events +} + +// TestNotifyOfConnectivityChange_ReconnectReportsBothHalves guards the reconnect half of a +// grouped control channel losing every underlay and getting one back. The registration +// survives that, so nothing else reports it: a reconnect that re-offers state without +// emitting ControllerReconnected leaves every listener keyed on connectivity believing the +// controller is still gone. +func TestNotifyOfConnectivityChange_ReconnectReportsBothHalves(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + ctrl, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", ctrl) + + var wasDisconnected atomic.Bool + var reconnectNotifications int + notifyReconnect := func() { reconnectNotifications++ } + + // Last underlay lost, then one back. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + + events := awaitCtrlEvents(t, drain, 2) + + // Listeners are notified on their own goroutine per event, so the two can arrive in + // either order; what matters is that both were reported, once each. + require.Len(t, events, 2) + types := []CtrlEventType{events[0].Type, events[1].Type} + require.ElementsMatch(t, []CtrlEventType{ControllerDisconnected, ControllerReconnected}, types, + "regaining an underlay must report the controller as reconnected") + for _, e := range events { + require.Same(t, ctrl, e.Controller) + } + require.Equal(t, 1, reconnectNotifications, "the reconnect must also re-offer state to the controller") +} + +// TestNotifyOfConnectivityChange_ReportsEachEdgeOnce covers underlays coming and going +// without connectivity changing. Only the edges are transitions; every other change is +// noise that listeners must not see as a reconnect. +func TestNotifyOfConnectivityChange_ReportsEachEdgeOnce(t *testing.T) { + nc := newTestNetworkControllers() + drain := collectCtrlEvents(nc) + + ctrl, _ := newTestNetworkCtrl(nc, "ctrl1", "current") + nc.ctrls.Put("ctrl1", ctrl) + + var wasDisconnected atomic.Bool + var reconnectNotifications int + notifyReconnect := func() { reconnectNotifications++ } + + // A second underlay arriving and leaving while one remains is not a transition. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 2, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + + require.Never(t, func() bool { return len(drain()) > 0 }, 100*time.Millisecond, 10*time.Millisecond, + "underlay churn that never cost connectivity must not be reported") + require.Zero(t, reconnectNotifications) + + // Two reports of the count reaching zero are one disconnect. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 0, notifyReconnect) + + events := awaitCtrlEvents(t, drain, 1) + + // Same for the count coming back. + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 1, notifyReconnect) + nc.notifyOfConnectivityChange("ctrl1", &wasDisconnected, 2, notifyReconnect) + + events = append(events, awaitCtrlEvents(t, drain, 1)...) + + // Waiting for each edge before triggering the next fixes their order here, unlike the + // back-to-back case, where the per-event goroutines can deliver in either order. + require.Equal(t, ControllerDisconnected, events[0].Type) + require.Equal(t, ControllerReconnected, events[1].Type) + + require.Never(t, func() bool { return len(drain()) > 0 }, 100*time.Millisecond, 10*time.Millisecond, + "each edge must be reported once") + require.Equal(t, 1, reconnectNotifications) +} From 332e0b80522f0be5f030fd1cd05b345e95188474 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 24 Aug 2026 15:34:17 -0400 Subject: [PATCH 68/73] Make the control channel heartbeat cadence load-time checked. For #4264 The close-unresponsive teardown is only reachable if its timeout survives config loading, and only correct if the cadence around it can keep the channel up. channel/v4 loaded closeUnresponsiveTimeout into CheckInterval, so the timeout held its default and setting it repurposed the check interval. Fixed in channel v4.3.12. CheckHeartBeat runs only from the heartbeat pulse, so the check interval is the sampling rate for the timeout and the response time it reads can be a full interval stale. At or above the timeout the check condemns a controller that is answering, which reconnects and repeats. Since closeUnresponsiveTimeout defaults to 30s, raising checkInterval alone is enough to reach that, with no second setting involved. - pins channel/v4 at v4.3.12 in both modules, the release that makes the timeout settable - adds config.ValidateHeartbeatOptions, requiring a positive check interval, and both the check and send intervals to be under the close timeout when the teardown is enabled - rejects an unworkable cadence when loading the router's ctrl heartbeats, naming the keys and what goes wrong, rather than accepting it and closing healthy channels later - tests that each heartbeat key lands in its own field, that the teardown can be disabled from config, and that the rejected relations are rejected through the config path Leaves link.heartbeats alone: its cadence never reaches ConfigureHeartbeat, which is called with constants, so validating it would reject values that have no effect. The controller's routerHeartbeats and peerHeartbeats have the same exposure and predate this work, so they are left for their own change rather than widened into a backport. The tests fail against v4.3.11, so they also pin the minimum channel version behaviorally: a downgrade or a repeat of the loader bug fails here rather than leaving the option inert. --- common/config/heartbeats.go | 63 +++++++++++++++ common/config/heartbeats_test.go | 107 ++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- router/env/ctrl.go | 8 ++ router/env/ctrl_heartbeat_test.go | 123 ++++++++++++++++++++++++++++++ zititest/go.mod | 2 +- zititest/go.sum | 4 +- 8 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 common/config/heartbeats.go create mode 100644 common/config/heartbeats_test.go diff --git a/common/config/heartbeats.go b/common/config/heartbeats.go new file mode 100644 index 000000000..b7aa6d27b --- /dev/null +++ b/common/config/heartbeats.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package config + +import ( + "fmt" + + "github.com/openziti/channel/v4" +) + +// ValidateHeartbeatOptions reports whether options describe a cadence that can keep a channel up. +// Callers pass loaded configuration, so the returned error names the configuration keys and is +// suitable for returning straight out of config loading. +// +// Apply this wherever the heartbeat callback closes the channel after CloseUnresponsiveTimeout +// without a response. A callback that only records latency has no deadline to sample and does not +// need it. A zero CloseUnresponsiveTimeout disables the teardown, and only the check interval is +// constrained. +func ValidateHeartbeatOptions(options *channel.HeartbeatOptions) error { + if options == nil { + return nil + } + + // ConfigureHeartbeat hands this straight to time.NewTicker on a bare goroutine, which panics on + // a non-positive interval and takes the process with it. + if options.CheckInterval <= 0 { + return fmt.Errorf("heartbeat checkInterval (%v) must be greater than zero", options.CheckInterval) + } + + if options.CloseUnresponsiveTimeout <= 0 { + return nil + } + + // The check interval is the sampling rate for the timeout, and the response time it reads can be + // a full interval stale, so at or above the timeout it condemns channels that are answering. + if options.CheckInterval >= options.CloseUnresponsiveTimeout { + return fmt.Errorf("heartbeat checkInterval (%v) must be less than closeUnresponsiveTimeout (%v), "+ + "otherwise the check reads a stale response time and closes healthy channels", + options.CheckInterval, options.CloseUnresponsiveTimeout) + } + + if options.SendInterval >= options.CloseUnresponsiveTimeout { + return fmt.Errorf("heartbeat sendInterval (%v) must be less than closeUnresponsiveTimeout (%v), "+ + "otherwise a channel is closed between scheduled heartbeats", + options.SendInterval, options.CloseUnresponsiveTimeout) + } + + return nil +} diff --git a/common/config/heartbeats_test.go b/common/config/heartbeats_test.go new file mode 100644 index 000000000..4aa09a64d --- /dev/null +++ b/common/config/heartbeats_test.go @@ -0,0 +1,107 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package config + +import ( + "testing" + "time" + + "github.com/openziti/channel/v4" + "github.com/stretchr/testify/require" +) + +func TestValidateHeartbeatOptions(t *testing.T) { + options := func(send, check, closeTimeout time.Duration) *channel.HeartbeatOptions { + return &channel.HeartbeatOptions{ + SendInterval: send, + CheckInterval: check, + CloseUnresponsiveTimeout: closeTimeout, + } + } + + tests := []struct { + name string + options *channel.HeartbeatOptions + errIs string + }{ + { + name: "the library defaults are accepted", + options: channel.DefaultHeartbeatOptions(), + }, + { + name: "nil is accepted, since an absent config is the caller's default", + options: nil, + }, + { + name: "a cadence well inside the timeout is accepted", + options: options(10*time.Second, time.Second, 30*time.Second), + }, + { + name: "a zero check interval is rejected, since the pulse ticker panics on it", + options: options(10*time.Second, 0, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a negative check interval is rejected", + options: options(10*time.Second, -time.Second, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a check interval above the timeout is rejected", + options: options(10*time.Second, time.Minute, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a check interval equal to the timeout is rejected", + options: options(10*time.Second, 30*time.Second, 30*time.Second), + errIs: "checkInterval", + }, + { + name: "a send interval above the timeout is rejected", + options: options(time.Minute, time.Second, 30*time.Second), + errIs: "sendInterval", + }, + { + name: "a send interval equal to the timeout is rejected", + options: options(30*time.Second, time.Second, 30*time.Second), + errIs: "sendInterval", + }, + { + // A zero timeout disables the teardown, so there is no deadline to outpace and only the + // ticker's own requirement is left to enforce. + name: "a disabled teardown leaves the intervals unconstrained", + options: options(time.Hour, time.Minute, 0), + }, + { + name: "a disabled teardown still rejects a zero check interval", + options: options(10*time.Second, 0, 0), + errIs: "checkInterval", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateHeartbeatOptions(test.options) + if test.errIs == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), test.errIs) + }) + } +} diff --git a/go.mod b/go.mod index 719d8a5d4..2e3489d27 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/natefinch/lumberjack v2.0.0+incompatible github.com/openziti/agent v1.0.33 - github.com/openziti/channel/v4 v4.3.11 + github.com/openziti/channel/v4 v4.3.12 github.com/openziti/cobra-to-md v1.0.1 github.com/openziti/edge-api v0.31.0 github.com/openziti/foundation/v2 v2.0.91 diff --git a/go.sum b/go.sum index 236a73efc..054cb1ac9 100644 --- a/go.sum +++ b/go.sum @@ -528,8 +528,8 @@ github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/openziti/agent v1.0.33 h1:W6LXs+dWIzg/V8KNJq501zUxFSjtDQTaBFdqOcUFCTg= github.com/openziti/agent v1.0.33/go.mod h1:pjQ9jSOl+9ZR/0Y9+wOwJhGekYwyai2uKB3YCd0nulI= -github.com/openziti/channel/v4 v4.3.11 h1:ugezUhTEuSQnVFKUioN1gh6f+ZF4eBqx6mOrT5DrY5U= -github.com/openziti/channel/v4 v4.3.11/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= +github.com/openziti/channel/v4 v4.3.12 h1:kpox7z7aILokmzejUlOobORzQutH6q3t4E5abuZcOPA= +github.com/openziti/channel/v4 v4.3.12/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQfVXxE= github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c= github.com/openziti/edge-api v0.31.0 h1:QdaaPnKQj4B40q404+c8VZxMsoVpfGpfOhzs3LLCd/A= diff --git a/router/env/ctrl.go b/router/env/ctrl.go index 4b00e8ab8..225136c17 100644 --- a/router/env/ctrl.go +++ b/router/env/ctrl.go @@ -22,6 +22,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/foundation/v2/versions" + "github.com/openziti/ziti/v2/common/config" "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/openziti/channel/v4" @@ -185,11 +186,18 @@ func NewDefaultHeartbeatOptions() *HeartbeatOptions { } } +// NewHeartbeatOptions returns the control channel heartbeat options described by options, overlaying +// this package's unresponsiveAfter setting. It rejects a cadence that cannot keep the channel up, +// since CheckHeartBeat tears the channel down once CloseUnresponsiveTimeout passes without a +// response. func NewHeartbeatOptions(options *channel.HeartbeatOptions) (*HeartbeatOptions, error) { unresponsiveAfter, err := options.GetDuration("unresponsiveAfter") if err != nil { return nil, err } + if err := config.ValidateHeartbeatOptions(options); err != nil { + return nil, err + } result := NewDefaultHeartbeatOptions() result.HeartbeatOptions = *options if unresponsiveAfter != nil { diff --git a/router/env/ctrl_heartbeat_test.go b/router/env/ctrl_heartbeat_test.go index e9314566d..bdc61b6c6 100644 --- a/router/env/ctrl_heartbeat_test.go +++ b/router/env/ctrl_heartbeat_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/openziti/channel/v4" "github.com/openziti/ziti/v2/common/ctrlchan" "github.com/stretchr/testify/require" ) @@ -95,3 +96,125 @@ func TestCheckHeartBeat_ZeroTimeoutDisablesClose(t *testing.T) { require.False(t, ch.IsClosed(), "a zero close timeout must disable the teardown") } + +// TestNewHeartbeatOptions_KeysLandInOwnFields covers the overlay this package puts on top of the channel +// heartbeat options. The close timeout is only meaningful if it survives config loading, and a value that +// lands in checkInterval instead makes the pulse sample a stale last-response time, which closes healthy +// channels rather than unresponsive ones. +func TestNewHeartbeatOptions_KeysLandInOwnFields(t *testing.T) { + load := func(t *testing.T, src map[interface{}]interface{}) *HeartbeatOptions { + t.Helper() + chOptions, err := channel.LoadHeartbeatOptions(src) + require.NoError(t, err) + options, err := NewHeartbeatOptions(chOptions) + require.NoError(t, err) + return options + } + + t.Run("each key lands in its own field", func(t *testing.T) { + options := load(t, map[interface{}]interface{}{ + "sendInterval": "3s", + "checkInterval": "4s", + "closeUnresponsiveTimeout": "5s", + "unresponsiveAfter": "6s", + }) + require.Equal(t, 3*time.Second, options.SendInterval) + require.Equal(t, 4*time.Second, options.CheckInterval) + require.Equal(t, 5*time.Second, options.CloseUnresponsiveTimeout) + require.Equal(t, 6*time.Second, options.UnresponsiveAfter) + }) + + t.Run("the close timeout leaves the check interval alone", func(t *testing.T) { + defaults := NewDefaultHeartbeatOptions() + options := load(t, map[interface{}]interface{}{ + "closeUnresponsiveTimeout": "90s", + }) + require.Equal(t, 90*time.Second, options.CloseUnresponsiveTimeout) + require.Equal(t, defaults.CheckInterval, options.CheckInterval) + require.Equal(t, defaults.UnresponsiveAfter, options.UnresponsiveAfter) + }) + + t.Run("the teardown can be disabled from config", func(t *testing.T) { + options := load(t, map[interface{}]interface{}{ + "closeUnresponsiveTimeout": "0s", + }) + require.Zero(t, options.CloseUnresponsiveTimeout) + require.Positive(t, options.CheckInterval, "a zero close timeout must not zero the check interval") + }) + + t.Run("defaults apply when nothing is set", func(t *testing.T) { + defaults := NewDefaultHeartbeatOptions() + options := load(t, map[interface{}]interface{}{}) + require.Equal(t, defaults.SendInterval, options.SendInterval) + require.Equal(t, defaults.CheckInterval, options.CheckInterval) + require.Equal(t, defaults.CloseUnresponsiveTimeout, options.CloseUnresponsiveTimeout) + require.Equal(t, defaults.UnresponsiveAfter, options.UnresponsiveAfter) + }) +} + +// TestNewHeartbeatOptions_RejectsUnworkableCadence covers the cadence relation the loader enforces. +// CheckHeartBeat only runs from the heartbeat pulse, so the check interval is the sampling rate for +// the close timeout: at or above it the check reads a response time up to a full interval stale and +// tears down a controller that is answering, which then reconnects and repeats. +func TestNewHeartbeatOptions_RejectsUnworkableCadence(t *testing.T) { + load := func(src map[interface{}]interface{}) (*HeartbeatOptions, error) { + chOptions, err := channel.LoadHeartbeatOptions(src) + require.NoError(t, err) + return NewHeartbeatOptions(chOptions) + } + + // The close timeout defaults to 30s, so raising only the check interval is enough to break it. + t.Run("a check interval above the default timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("a check interval above an explicit timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "closeUnresponsiveTimeout": "30s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("a send interval above the timeout is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "sendInterval": "60s", + "closeUnresponsiveTimeout": "30s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "sendInterval") + }) + + t.Run("a zero check interval is rejected", func(t *testing.T) { + _, err := load(map[interface{}]interface{}{ + "checkInterval": "0s", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "checkInterval") + }) + + t.Run("widening the timeout alongside the check interval is accepted", func(t *testing.T) { + options, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "sendInterval": "60s", + "closeUnresponsiveTimeout": "5m", + }) + require.NoError(t, err) + require.Equal(t, time.Minute, options.CheckInterval) + require.Equal(t, 5*time.Minute, options.CloseUnresponsiveTimeout) + }) + + t.Run("disabling the teardown accepts any positive check interval", func(t *testing.T) { + options, err := load(map[interface{}]interface{}{ + "checkInterval": "60s", + "closeUnresponsiveTimeout": "0s", + }) + require.NoError(t, err) + require.Zero(t, options.CloseUnresponsiveTimeout) + }) +} diff --git a/zititest/go.mod b/zititest/go.mod index 5797fc0c1..4778e1639 100644 --- a/zititest/go.mod +++ b/zititest/go.mod @@ -18,7 +18,7 @@ require ( github.com/google/uuid v1.6.0 github.com/michaelquigley/pfxlog v1.0.0 github.com/openziti/agent v1.0.33 - github.com/openziti/channel/v4 v4.3.11 + github.com/openziti/channel/v4 v4.3.12 github.com/openziti/edge-api v0.31.0 github.com/openziti/fablab v0.6.16 github.com/openziti/foundation/v2 v2.0.91 diff --git a/zititest/go.sum b/zititest/go.sum index 7f6d6281d..92b4f6978 100644 --- a/zititest/go.sum +++ b/zititest/go.sum @@ -599,8 +599,8 @@ github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/openziti/agent v1.0.33 h1:W6LXs+dWIzg/V8KNJq501zUxFSjtDQTaBFdqOcUFCTg= github.com/openziti/agent v1.0.33/go.mod h1:pjQ9jSOl+9ZR/0Y9+wOwJhGekYwyai2uKB3YCd0nulI= -github.com/openziti/channel/v4 v4.3.11 h1:ugezUhTEuSQnVFKUioN1gh6f+ZF4eBqx6mOrT5DrY5U= -github.com/openziti/channel/v4 v4.3.11/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= +github.com/openziti/channel/v4 v4.3.12 h1:kpox7z7aILokmzejUlOobORzQutH6q3t4E5abuZcOPA= +github.com/openziti/channel/v4 v4.3.12/go.mod h1:WZpOeuPliA7mJ3m7Pal8WdYu74eHMzkM/1Z9AMLmUAg= github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQfVXxE= github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c= github.com/openziti/edge-api v0.31.0 h1:QdaaPnKQj4B40q404+c8VZxMsoVpfGpfOhzs3LLCd/A= From 3d48c13817ed62c76a08e1f65559d5e7737132d4 Mon Sep 17 00:00:00 2001 From: Paul Lorenz Date: Mon, 24 Aug 2026 15:55:51 -0400 Subject: [PATCH 69/73] Add the 2.0.4 CHANGELOG section. For #4264 v2.0.3 is released, so the control-channel connect/disconnect backport lands in the next patch release rather than that one. - adds a Release 2.0.4 section listing the backport tracking issue --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99ea3b64..41ff3230f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Release 2.0.4 + +## What's New + +* Bug fixes + +## Component Updates and Bug Fixes + +* github.com/openziti/ziti/v2: [v2.0.3 -> v2.0.4](https://github.com/openziti/ziti/compare/v2.0.3...v2.0.4) + * [Issue #4264](https://github.com/openziti/ziti/issues/4264) - [Backport-2.0] Router control-channel connect/disconnect race can leave a reconnected router de-registered + # Release 2.0.3 ## What's New From 11040b9295f402d660d9e7c92ec5416f921e1d6f Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 26 Aug 2026 15:08:14 +0100 Subject: [PATCH 70/73] backport openziti/ziti#4304 to release-v2.0.x build flags on the version response (#4306) - adds a package-level buildFlags string the linker sets at build time, parsed into a capped list of [A-Z0-9_] names with blanks, duplicates, and malformed tokens dropped - returns those names in the new buildFlags field on /version, separate from capabilities, and prints them under ziti version -v - bumps edge-api to v0.36.0 for the buildFlags field, which moves no other dependency --- common/build/flags.go | 63 ++++++++++++++ common/build/flags_test.go | 87 ++++++++++++++++++++ controller/internal/routes/version_router.go | 1 + go.mod | 2 +- go.sum | 2 + ziti/cmd/common/version.go | 6 ++ 6 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 common/build/flags.go create mode 100644 common/build/flags_test.go diff --git a/common/build/flags.go b/common/build/flags.go new file mode 100644 index 000000000..dc91cb596 --- /dev/null +++ b/common/build/flags.go @@ -0,0 +1,63 @@ +package build + +import ( + "regexp" + "strings" +) + +const ( + // maxBuildFlags is the greatest number of flag names accepted from buildFlags. + maxBuildFlags = 32 + + // maxBuildFlagNameLength is the greatest length accepted for a single flag name. + maxBuildFlagNameLength = 64 +) + +// buildFlagNamePattern matches a well formed build flag name. +var buildFlagNamePattern = regexp.MustCompile(`^[A-Z0-9_]+$`) + +// buildFlags is a comma separated list of flag names supplied at build time by the linker, empty +// in a stock build: +// +// -X github.com/openziti/ziti/v2/common/build.buildFlags=ALPHA,BRAVO +// +// The symbol path is a contract with downstream builds. The linker silently ignores -X against a +// symbol it cannot resolve, so renaming this variable or moving it to another package produces a +// binary with no build flags rather than a build error. A second -X against this symbol replaces +// the first rather than adding to it: one build owner composes the whole list. +var buildFlags string + +// GetBuildFlags returns the well formed flag names supplied at build time, in the order they were +// given. It returns an empty slice for a stock build. +func GetBuildFlags() []string { + return parseBuildFlags(buildFlags) +} + +// parseBuildFlags splits raw on commas and returns the well formed names in first seen order, +// dropping blanks, duplicates, and anything outside [A-Z0-9_]. The result is never nil, so it +// serializes as an empty JSON array rather than null. The count of names and the length of each +// are capped, because the result is served over an unauthenticated API. +func parseBuildFlags(raw string) []string { + result := []string{} + seen := map[string]struct{}{} + + for _, token := range strings.Split(raw, ",") { + if len(result) == maxBuildFlags { + break + } + + name := strings.TrimSpace(token) + if len(name) > maxBuildFlagNameLength || !buildFlagNamePattern.MatchString(name) { + continue + } + + if _, ok := seen[name]; ok { + continue + } + + seen[name] = struct{}{} + result = append(result, name) + } + + return result +} diff --git a/common/build/flags_test.go b/common/build/flags_test.go new file mode 100644 index 000000000..0645dc027 --- /dev/null +++ b/common/build/flags_test.go @@ -0,0 +1,87 @@ +package build + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseBuildFlags(t *testing.T) { + tests := []struct { + name string + raw string + expected []string + }{ + { + name: "empty string yields an empty, non nil slice", + raw: "", + expected: []string{}, + }, + { + name: "single name", + raw: "ALPHA", + expected: []string{"ALPHA"}, + }, + { + name: "multiple names keep their order", + raw: "ALPHA,BRAVO,CHARLIE", + expected: []string{"ALPHA", "BRAVO", "CHARLIE"}, + }, + { + name: "surrounding whitespace is trimmed", + raw: " ALPHA , BRAVO\t,\nCHARLIE ", + expected: []string{"ALPHA", "BRAVO", "CHARLIE"}, + }, + { + name: "blank tokens are dropped", + raw: ",ALPHA,, ,BRAVO,", + expected: []string{"ALPHA", "BRAVO"}, + }, + { + name: "duplicates are dropped, first occurrence wins", + raw: "ALPHA,BRAVO,ALPHA", + expected: []string{"ALPHA", "BRAVO"}, + }, + { + name: "digits and underscores are accepted", + raw: "ALPHA_2,BRAVO_MODE,3", + expected: []string{"ALPHA_2", "BRAVO_MODE", "3"}, + }, + { + name: "malformed tokens are dropped, well formed ones survive", + raw: "alpha,BRAVO,Char-lie,DELTA ECHO,FOXTROT!,GOLF", + expected: []string{"BRAVO", "GOLF"}, + }, + { + name: "names longer than the cap are dropped", + raw: "ALPHA," + strings.Repeat("B", maxBuildFlagNameLength+1) + ",CHARLIE", + expected: []string{"ALPHA", "CHARLIE"}, + }, + { + name: "names exactly at the length cap are kept", + raw: strings.Repeat("B", maxBuildFlagNameLength), + expected: []string{strings.Repeat("B", maxBuildFlagNameLength)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := parseBuildFlags(test.raw) + + require.Equal(t, test.expected, result) + }) + } +} + +func TestParseBuildFlagsStopsAtTheCountCap(t *testing.T) { + names := make([]string, 0, maxBuildFlags+5) + for i := 0; i < maxBuildFlags+5; i++ { + names = append(names, "NAME_"+strings.Repeat("X", i)) + } + + result := parseBuildFlags(strings.Join(names, ",")) + + require.Len(t, result, maxBuildFlags, "accepted names must be capped") + require.Equal(t, names[:maxBuildFlags], result, "the first names up to the cap are the ones kept") +} diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go index 25f5ec4f7..2a32745db 100644 --- a/controller/internal/routes/version_router.go +++ b/controller/internal/routes/version_router.go @@ -97,6 +97,7 @@ func (ir *VersionRouter) buildVersions(ae *env.AppEnv) *rest_model.Version { Version: buildInfo.Version(), APIVersions: map[string]map[string]rest_model.APIVersion{}, Capabilities: []string{}, + BuildFlags: build.GetBuildFlags(), } for apiBinding, apiVersionToPathMap := range webapis.AllApiBindingVersions { diff --git a/go.mod b/go.mod index 2e3489d27..8e84bae01 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/openziti/agent v1.0.33 github.com/openziti/channel/v4 v4.3.12 github.com/openziti/cobra-to-md v1.0.1 - github.com/openziti/edge-api v0.31.0 + github.com/openziti/edge-api v0.36.0 github.com/openziti/foundation/v2 v2.0.91 github.com/openziti/identity v1.0.129 github.com/openziti/jwks v1.0.6 diff --git a/go.sum b/go.sum index 054cb1ac9..e9cd49828 100644 --- a/go.sum +++ b/go.sum @@ -534,6 +534,8 @@ github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQ github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c= github.com/openziti/edge-api v0.31.0 h1:QdaaPnKQj4B40q404+c8VZxMsoVpfGpfOhzs3LLCd/A= github.com/openziti/edge-api v0.31.0/go.mod h1:pDtCR6Mq0h5e8ulJhmuhPshEBa39hpQy+yTtLpUSBOs= +github.com/openziti/edge-api v0.36.0 h1:adp0gCDbxee3r4tPBCXm7IyLTcIGhDcWmQ9QJMO+AwY= +github.com/openziti/edge-api v0.36.0/go.mod h1:m1oAQ6+fnkEO0NOAkDy6WpnXDxPhj2sko4eBUvIMxkE= github.com/openziti/foundation/v2 v2.0.91 h1:gjXFu+fxKKMCs1hCkfLNI9jfdiSBW2gXAnBCy5xG/UI= github.com/openziti/foundation/v2 v2.0.91/go.mod h1:vbAudlD59QTMR+IifKXLL9c8W/OyL8DVsbYqGXWV2dY= github.com/openziti/go-term-markdown v1.0.1 h1:9uzMpK4tav6OtvRxRt99WwPTzAzCh+Pj9zWU2FBp3Qg= diff --git a/ziti/cmd/common/version.go b/ziti/cmd/common/version.go index ae1b53655..8066de7af 100644 --- a/ziti/cmd/common/version.go +++ b/ziti/cmd/common/version.go @@ -18,7 +18,9 @@ package common import ( "fmt" + "strings" + "github.com/openziti/ziti/v2/common/build" "github.com/openziti/ziti/v2/common/version" "github.com/spf13/cobra" ) @@ -36,6 +38,10 @@ func NewVersionCmd() *cobra.Command { fmt.Printf("Build Date: %s\n", version.GetBuildDate()) fmt.Printf("Go Version: %s\n", version.GetGoVersion()) fmt.Printf("OS/Arch: %s/%s\n", version.GetOS(), version.GetArchitecture()) + + if buildFlags := build.GetBuildFlags(); len(buildFlags) > 0 { + fmt.Printf("Build Flags: %s\n", strings.Join(buildFlags, ", ")) + } } else { fmt.Println(version.GetVersion()) } From 8c919dfbe237507d108003b4024ff5410c526a02 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 26 Aug 2026 19:43:34 +0100 Subject: [PATCH 71/73] backport openziti/ziti#4094 to release-v2.0.x accept first-party certs issued by a separate edge signing CA (#4153) - publishes FirstPartyX509CertValidation/ThirdPartyX509CertValidation usages and intermediates on router data model public keys, deprecating ClientX509CertValidation - builds the router first-party cert pool from RDM first-party keys unioned with ctrl-channel roots; TLS and VerifyClientCert paths share buildClientCertRoots with fallback to the deprecated usage for old controllers - trusts the edge enrollment signing CA when verifying the certificate a router presents on the control channel, so a signing CA outside the controller's own trust bundle no longer refuses every router; the anchors go into a clone of the identity's pool, never the pool its live tls.Configs share - propagates full controller signing cert chains over the mesh via SigningCertChainHeader and persists them in Controller store CertPem - sends stored public keys during router sync instead of rebuilding them; publishes controller certs leaf-only - stops router controller reconnect loops after shutdown - gives each in-process controller its own command decoder registry - adds the ha-3 three-controller harness and first-party cert integration tests - drains the cli test stdout pipe while commands run; anchors the totp token issued-at assertion to the test clock - backports the SPIFFE-capable test PKI from openziti/ziti#3947: --not-before on ziti pki create, tests/testdata/create-pki.sh/.ps1, and the generated PKI under tests/testdata/pki including the separate edge signing root and per-controller signing intermediates; existing config sets stay on the testdata/ca PKI - skips *.pem, *.cert and *.key files in codespell --- .github/workflows/codespell.yml | 2 +- common/cert/verify.go | 13 +- common/cert/verify_test.go | 42 ++ common/pb/edge_ctrl_pb/edge_ctrl.pb.go | 38 +- common/pb/edge_ctrl_pb/edge_ctrl.proto | 5 +- common/public_key_validate_roundtrip_test.go | 59 +++ controller/command/command.go | 18 +- controller/controller.go | 13 +- controller/handler_ctrl/connect.go | 25 +- controller/model/command.go | 3 +- controller/model/controller_manager.go | 34 +- controller/raft/mesh/mesh.go | 60 ++- controller/raft/mesh/signing_certs_test.go | 124 ++++++ controller/raft/raft.go | 13 +- controller/sync_strats/public_key_test.go | 50 +++ controller/sync_strats/sync_instant.go | 94 +++-- router/env/ctrls.go | 11 + router/state/cert_origin.go | 78 +++- router/state/cert_origin_test.go | 381 ++++++++++++++++++ router/state/cert_validating_identity.go | 81 +++- router/state/manager.go | 19 +- tests/api_session_totp_token_test.go | 6 +- tests/cli_tests/cli_test.go | 11 +- tests/configsets.go | 32 +- tests/context.go | 48 ++- tests/ha_cluster.go | 225 +++++++++++ tests/ha_cluster_formation_test.go | 15 + tests/ha_first_party_cert_test.go | 245 +++++++++++ tests/testdata/configs/README.md | 42 ++ tests/testdata/configs/ha-3/ctrl1.yml | 60 +++ tests/testdata/configs/ha-3/ctrl2.yml | 57 +++ tests/testdata/configs/ha-3/ctrl3.yml | 57 +++ tests/testdata/configs/ha-3/edge-router.yml | 40 ++ tests/testdata/create-pki.ps1 | 94 +++++ tests/testdata/create-pki.sh | 106 +++++ .../testdata/pki/ctrl1/certs/001-client.cert | 34 ++ .../pki/ctrl1/certs/001-client.chain.pem | 102 +++++ .../testdata/pki/ctrl1/certs/001-server.cert | 34 ++ .../pki/ctrl1/certs/001-server.chain.pem | 102 +++++ .../testdata/pki/ctrl1/certs/002-client.cert | 34 ++ .../pki/ctrl1/certs/002-client.chain.pem | 102 +++++ .../testdata/pki/ctrl1/certs/002-server.cert | 34 ++ .../pki/ctrl1/certs/002-server.chain.pem | 102 +++++ tests/testdata/pki/ctrl1/certs/client.cert | 34 ++ .../testdata/pki/ctrl1/certs/client.chain.pem | 102 +++++ .../pki/ctrl1/certs/ctrl1-wildcard.cert | 34 ++ .../pki/ctrl1/certs/ctrl1-wildcard.chain.pem | 102 +++++ tests/testdata/pki/ctrl1/certs/ctrl1.cert | 34 ++ .../testdata/pki/ctrl1/certs/ctrl1.chain.pem | 68 ++++ tests/testdata/pki/ctrl1/certs/server.cert | 34 ++ .../testdata/pki/ctrl1/certs/server.chain.pem | 102 +++++ tests/testdata/pki/ctrl1/crlnumber | 1 + tests/testdata/pki/ctrl1/index.txt | 7 + tests/testdata/pki/ctrl1/index.txt.attr | 1 + tests/testdata/pki/ctrl1/keys/001-client.key | 52 +++ tests/testdata/pki/ctrl1/keys/001-server.key | 52 +++ tests/testdata/pki/ctrl1/keys/001.key | 52 +++ tests/testdata/pki/ctrl1/keys/002-client.key | 52 +++ tests/testdata/pki/ctrl1/keys/002-server.key | 52 +++ tests/testdata/pki/ctrl1/keys/002.key | 52 +++ tests/testdata/pki/ctrl1/keys/client.key | 52 +++ .../pki/ctrl1/keys/ctrl1-wildcard.key | 52 +++ tests/testdata/pki/ctrl1/keys/ctrl1.key | 52 +++ tests/testdata/pki/ctrl1/keys/server.key | 52 +++ tests/testdata/pki/ctrl1/serial | 1 + tests/testdata/pki/ctrl2/certs/client.cert | 34 ++ .../testdata/pki/ctrl2/certs/client.chain.pem | 102 +++++ tests/testdata/pki/ctrl2/certs/ctrl2.cert | 34 ++ .../testdata/pki/ctrl2/certs/ctrl2.chain.pem | 68 ++++ tests/testdata/pki/ctrl2/certs/server.cert | 34 ++ .../testdata/pki/ctrl2/certs/server.chain.pem | 102 +++++ tests/testdata/pki/ctrl2/crlnumber | 1 + tests/testdata/pki/ctrl2/index.txt | 2 + tests/testdata/pki/ctrl2/index.txt.attr | 1 + tests/testdata/pki/ctrl2/keys/client.key | 52 +++ tests/testdata/pki/ctrl2/keys/ctrl2.key | 52 +++ tests/testdata/pki/ctrl2/keys/server.key | 52 +++ tests/testdata/pki/ctrl2/serial | 1 + tests/testdata/pki/ctrl3/certs/client.cert | 34 ++ .../testdata/pki/ctrl3/certs/client.chain.pem | 102 +++++ tests/testdata/pki/ctrl3/certs/ctrl3.cert | 34 ++ .../testdata/pki/ctrl3/certs/ctrl3.chain.pem | 68 ++++ tests/testdata/pki/ctrl3/certs/server.cert | 34 ++ .../testdata/pki/ctrl3/certs/server.chain.pem | 102 +++++ tests/testdata/pki/ctrl3/crlnumber | 1 + tests/testdata/pki/ctrl3/index.txt | 2 + tests/testdata/pki/ctrl3/index.txt.attr | 1 + tests/testdata/pki/ctrl3/keys/client.key | 52 +++ tests/testdata/pki/ctrl3/keys/ctrl3.key | 52 +++ tests/testdata/pki/ctrl3/keys/server.key | 52 +++ tests/testdata/pki/ctrl3/serial | 1 + tests/testdata/pki/root/certs/ctrl1.cert | 34 ++ tests/testdata/pki/root/certs/ctrl2.cert | 34 ++ tests/testdata/pki/root/certs/ctrl3.cert | 34 ++ tests/testdata/pki/root/certs/root.cert | 34 ++ tests/testdata/pki/root/crlnumber | 1 + tests/testdata/pki/root/index.txt | 4 + tests/testdata/pki/root/index.txt.attr | 1 + tests/testdata/pki/root/keys/ctrl1.key | 52 +++ tests/testdata/pki/root/keys/ctrl2.key | 52 +++ tests/testdata/pki/root/keys/ctrl3.key | 52 +++ tests/testdata/pki/root/keys/root.key | 52 +++ tests/testdata/pki/root/serial | 1 + .../pki/signing-root/certs/signing-bundle.pem | 136 +++++++ .../pki/signing-root/certs/signing-root.cert | 34 ++ .../pki/signing-root/certs/signing1.cert | 34 ++ .../pki/signing-root/certs/signing2.cert | 34 ++ .../pki/signing-root/certs/signing3.cert | 34 ++ tests/testdata/pki/signing-root/crlnumber | 1 + tests/testdata/pki/signing-root/index.txt | 4 + .../testdata/pki/signing-root/index.txt.attr | 1 + .../pki/signing-root/keys/signing-root.key | 52 +++ .../pki/signing-root/keys/signing1.key | 52 +++ .../pki/signing-root/keys/signing2.key | 52 +++ .../pki/signing-root/keys/signing3.key | 52 +++ tests/testdata/pki/signing-root/serial | 1 + .../testdata/pki/signing1/certs/signing1.cert | 34 ++ .../pki/signing1/certs/signing1.chain.pem | 68 ++++ tests/testdata/pki/signing1/crlnumber | 1 + tests/testdata/pki/signing1/index.txt | 0 tests/testdata/pki/signing1/index.txt.attr | 1 + tests/testdata/pki/signing1/keys/signing1.key | 52 +++ tests/testdata/pki/signing1/serial | 1 + .../testdata/pki/signing2/certs/signing2.cert | 34 ++ .../pki/signing2/certs/signing2.chain.pem | 68 ++++ tests/testdata/pki/signing2/crlnumber | 1 + tests/testdata/pki/signing2/index.txt | 0 tests/testdata/pki/signing2/index.txt.attr | 1 + tests/testdata/pki/signing2/keys/signing2.key | 52 +++ tests/testdata/pki/signing2/serial | 1 + .../testdata/pki/signing3/certs/signing3.cert | 34 ++ .../pki/signing3/certs/signing3.chain.pem | 68 ++++ tests/testdata/pki/signing3/crlnumber | 1 + tests/testdata/pki/signing3/index.txt | 0 tests/testdata/pki/signing3/index.txt.attr | 1 + tests/testdata/pki/signing3/keys/signing3.key | 52 +++ tests/testdata/pki/signing3/serial | 1 + ziti/cmd/pki/pki.go | 1 + ziti/cmd/pki/pki_create.go | 26 +- ziti/cmd/pki/pki_create_ca.go | 5 +- ziti/cmd/pki/pki_create_client.go | 5 +- ziti/cmd/pki/pki_create_intermediate.go | 5 +- ziti/cmd/pki/pki_create_key.go | 5 +- ziti/cmd/pki/pki_create_server.go | 5 +- ziti/pki/pki/template.go | 4 +- 145 files changed, 6053 insertions(+), 167 deletions(-) create mode 100644 common/public_key_validate_roundtrip_test.go create mode 100644 controller/raft/mesh/signing_certs_test.go create mode 100644 controller/sync_strats/public_key_test.go create mode 100644 router/state/cert_origin_test.go create mode 100644 tests/ha_cluster.go create mode 100644 tests/ha_cluster_formation_test.go create mode 100644 tests/ha_first_party_cert_test.go create mode 100644 tests/testdata/configs/ha-3/ctrl1.yml create mode 100644 tests/testdata/configs/ha-3/ctrl2.yml create mode 100644 tests/testdata/configs/ha-3/ctrl3.yml create mode 100644 tests/testdata/configs/ha-3/edge-router.yml create mode 100644 tests/testdata/create-pki.ps1 create mode 100644 tests/testdata/create-pki.sh create mode 100644 tests/testdata/pki/ctrl1/certs/001-client.cert create mode 100644 tests/testdata/pki/ctrl1/certs/001-client.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/001-server.cert create mode 100644 tests/testdata/pki/ctrl1/certs/001-server.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/002-client.cert create mode 100644 tests/testdata/pki/ctrl1/certs/002-client.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/002-server.cert create mode 100644 tests/testdata/pki/ctrl1/certs/002-server.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/client.cert create mode 100644 tests/testdata/pki/ctrl1/certs/client.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert create mode 100644 tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/ctrl1.cert create mode 100644 tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem create mode 100644 tests/testdata/pki/ctrl1/certs/server.cert create mode 100644 tests/testdata/pki/ctrl1/certs/server.chain.pem create mode 100644 tests/testdata/pki/ctrl1/crlnumber create mode 100644 tests/testdata/pki/ctrl1/index.txt create mode 100644 tests/testdata/pki/ctrl1/index.txt.attr create mode 100644 tests/testdata/pki/ctrl1/keys/001-client.key create mode 100644 tests/testdata/pki/ctrl1/keys/001-server.key create mode 100644 tests/testdata/pki/ctrl1/keys/001.key create mode 100644 tests/testdata/pki/ctrl1/keys/002-client.key create mode 100644 tests/testdata/pki/ctrl1/keys/002-server.key create mode 100644 tests/testdata/pki/ctrl1/keys/002.key create mode 100644 tests/testdata/pki/ctrl1/keys/client.key create mode 100644 tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key create mode 100644 tests/testdata/pki/ctrl1/keys/ctrl1.key create mode 100644 tests/testdata/pki/ctrl1/keys/server.key create mode 100644 tests/testdata/pki/ctrl1/serial create mode 100644 tests/testdata/pki/ctrl2/certs/client.cert create mode 100644 tests/testdata/pki/ctrl2/certs/client.chain.pem create mode 100644 tests/testdata/pki/ctrl2/certs/ctrl2.cert create mode 100644 tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem create mode 100644 tests/testdata/pki/ctrl2/certs/server.cert create mode 100644 tests/testdata/pki/ctrl2/certs/server.chain.pem create mode 100644 tests/testdata/pki/ctrl2/crlnumber create mode 100644 tests/testdata/pki/ctrl2/index.txt create mode 100644 tests/testdata/pki/ctrl2/index.txt.attr create mode 100644 tests/testdata/pki/ctrl2/keys/client.key create mode 100644 tests/testdata/pki/ctrl2/keys/ctrl2.key create mode 100644 tests/testdata/pki/ctrl2/keys/server.key create mode 100644 tests/testdata/pki/ctrl2/serial create mode 100644 tests/testdata/pki/ctrl3/certs/client.cert create mode 100644 tests/testdata/pki/ctrl3/certs/client.chain.pem create mode 100644 tests/testdata/pki/ctrl3/certs/ctrl3.cert create mode 100644 tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem create mode 100644 tests/testdata/pki/ctrl3/certs/server.cert create mode 100644 tests/testdata/pki/ctrl3/certs/server.chain.pem create mode 100644 tests/testdata/pki/ctrl3/crlnumber create mode 100644 tests/testdata/pki/ctrl3/index.txt create mode 100644 tests/testdata/pki/ctrl3/index.txt.attr create mode 100644 tests/testdata/pki/ctrl3/keys/client.key create mode 100644 tests/testdata/pki/ctrl3/keys/ctrl3.key create mode 100644 tests/testdata/pki/ctrl3/keys/server.key create mode 100644 tests/testdata/pki/ctrl3/serial create mode 100644 tests/testdata/pki/root/certs/ctrl1.cert create mode 100644 tests/testdata/pki/root/certs/ctrl2.cert create mode 100644 tests/testdata/pki/root/certs/ctrl3.cert create mode 100644 tests/testdata/pki/root/certs/root.cert create mode 100644 tests/testdata/pki/root/crlnumber create mode 100644 tests/testdata/pki/root/index.txt create mode 100644 tests/testdata/pki/root/index.txt.attr create mode 100644 tests/testdata/pki/root/keys/ctrl1.key create mode 100644 tests/testdata/pki/root/keys/ctrl2.key create mode 100644 tests/testdata/pki/root/keys/ctrl3.key create mode 100644 tests/testdata/pki/root/keys/root.key create mode 100644 tests/testdata/pki/root/serial create mode 100644 tests/testdata/pki/signing-root/certs/signing-bundle.pem create mode 100644 tests/testdata/pki/signing-root/certs/signing-root.cert create mode 100644 tests/testdata/pki/signing-root/certs/signing1.cert create mode 100644 tests/testdata/pki/signing-root/certs/signing2.cert create mode 100644 tests/testdata/pki/signing-root/certs/signing3.cert create mode 100644 tests/testdata/pki/signing-root/crlnumber create mode 100644 tests/testdata/pki/signing-root/index.txt create mode 100644 tests/testdata/pki/signing-root/index.txt.attr create mode 100644 tests/testdata/pki/signing-root/keys/signing-root.key create mode 100644 tests/testdata/pki/signing-root/keys/signing1.key create mode 100644 tests/testdata/pki/signing-root/keys/signing2.key create mode 100644 tests/testdata/pki/signing-root/keys/signing3.key create mode 100644 tests/testdata/pki/signing-root/serial create mode 100644 tests/testdata/pki/signing1/certs/signing1.cert create mode 100644 tests/testdata/pki/signing1/certs/signing1.chain.pem create mode 100644 tests/testdata/pki/signing1/crlnumber create mode 100644 tests/testdata/pki/signing1/index.txt create mode 100644 tests/testdata/pki/signing1/index.txt.attr create mode 100644 tests/testdata/pki/signing1/keys/signing1.key create mode 100644 tests/testdata/pki/signing1/serial create mode 100644 tests/testdata/pki/signing2/certs/signing2.cert create mode 100644 tests/testdata/pki/signing2/certs/signing2.chain.pem create mode 100644 tests/testdata/pki/signing2/crlnumber create mode 100644 tests/testdata/pki/signing2/index.txt create mode 100644 tests/testdata/pki/signing2/index.txt.attr create mode 100644 tests/testdata/pki/signing2/keys/signing2.key create mode 100644 tests/testdata/pki/signing2/serial create mode 100644 tests/testdata/pki/signing3/certs/signing3.cert create mode 100644 tests/testdata/pki/signing3/certs/signing3.chain.pem create mode 100644 tests/testdata/pki/signing3/crlnumber create mode 100644 tests/testdata/pki/signing3/index.txt create mode 100644 tests/testdata/pki/signing3/index.txt.attr create mode 100644 tests/testdata/pki/signing3/keys/signing3.key create mode 100644 tests/testdata/pki/signing3/serial diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 166d5bc24..7b1b5c30f 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -15,4 +15,4 @@ jobs: uses: codespell-project/actions-codespell@v2 with: ignore_words_list: actieve,allos,ans,dne,noe,referr,ssudo,te,tranfer,ue - skip: go.*,zititest/go.*,./controller/storage/zitiql/zitiql_parser.go + skip: go.*,zititest/go.*,./controller/storage/zitiql/zitiql_parser.go,*.pem,*.cert,*.key diff --git a/common/cert/verify.go b/common/cert/verify.go index c569fb4b7..31efdc0b4 100644 --- a/common/cert/verify.go +++ b/common/cert/verify.go @@ -34,7 +34,11 @@ import ( // how the node's TLS configuration establishes trust. No extended-key-usage restriction is applied: // certificates issued by an external PKI with arbitrary or absent EKUs are accepted as long as the leaf // chains to a trusted CA. -func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate) (*x509.Certificate, error) { +// +// additionalRoots are trust anchors the caller trusts for this check but which are not in the node's TLS +// pool, such as a CA the node itself issues peer certificates from. They are added to a copy of roots, so +// the caller's pool - which the identity shares with its live tls.Configs - is never modified. +func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate, additionalRoots ...*x509.Certificate) (*x509.Certificate, error) { if roots == nil { return nil, errors.New("no ca pool provided") } @@ -43,6 +47,13 @@ func VerifyLeafCertChain(roots *x509.CertPool, certs []*x509.Certificate) (*x509 return nil, errors.New("no certificates presented") } + if len(additionalRoots) > 0 { + roots = roots.Clone() + for _, additionalRoot := range additionalRoots { + roots.AddCert(additionalRoot) + } + } + intermediates := x509.NewCertPool() for _, intermediate := range certs[1:] { intermediates.AddCert(intermediate) diff --git a/common/cert/verify_test.go b/common/cert/verify_test.go index 7b64688b3..91f789eef 100644 --- a/common/cert/verify_test.go +++ b/common/cert/verify_test.go @@ -210,3 +210,45 @@ func TestVerifyLeafCertChain_EmptyInputs(t *testing.T) { _, err = VerifyLeafCertChain(roots, nil) req.Error(err, "no certs rejected") } + +// TestVerifyLeafCertChain_AdditionalRoots covers a controller whose edge signing CA sits outside its own +// trust bundle: a router presents an enrollment certificate issued by that CA, so the leaf chains only +// once the signing bundle is supplied as an additional anchor. +func TestVerifyLeafCertChain_AdditionalRoots(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signingRoot := vMkCA(t, "signing-root", nil) + signingInter := vMkCA(t, "signing-int", signingRoot) + roots := vPoolOf(ctrlRoot.cert) + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signingInter) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signingInter.cert}) + req.Error(err, "without the signing bundle the leaf chains to nothing trusted") + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert, signingInter.cert}, signingRoot.cert) + req.NoError(err, "the signing root supplied as an additional anchor lets the leaf verify") + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}, signingInter.cert) + req.NoError(err, "an intermediate from the signing bundle is a terminus like any other anchor") + + ctrlLeaf := vMkLeaf(t, "ctrl-signed", "/identity/r2", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, ctrlRoot) + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{ctrlLeaf.cert}, signingRoot.cert) + req.NoError(err, "additional anchors do not displace the caller's own pool") +} + +// TestVerifyLeafCertChain_AdditionalRootsDoNotMutateCallerPool guards the caller's pool, which an identity +// shares with the live tls.Configs it has handed out: anchors passed for one check must not become +// permanently trusted. +func TestVerifyLeafCertChain_AdditionalRootsDoNotMutateCallerPool(t *testing.T) { + req := require.New(t) + ctrlRoot := vMkCA(t, "ctrl-root", nil) + signingRoot := vMkCA(t, "signing-root", nil) + roots := vPoolOf(ctrlRoot.cert) + leaf := vMkLeaf(t, "router", "/identity/r1", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, signingRoot) + + _, err := VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}, signingRoot.cert) + req.NoError(err) + + _, err = VerifyLeafCertChain(roots, []*x509.Certificate{leaf.cert}) + req.Error(err, "the signing root must not have been added to the caller's pool") +} diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go index 07ceff7d4..94fd9c9cb 100644 --- a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go +++ b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.21.12 +// protoc v4.23.4 // source: edge_ctrl.proto package edge_ctrl_pb @@ -627,8 +627,11 @@ func (DataState_Action) EnumDescriptor() ([]byte, []int) { type DataState_PublicKey_Usage int32 const ( - DataState_PublicKey_JWTValidation DataState_PublicKey_Usage = 0 - DataState_PublicKey_ClientX509CertValidation DataState_PublicKey_Usage = 1 + DataState_PublicKey_JWTValidation DataState_PublicKey_Usage = 0 + // Deprecated: Marked as deprecated in edge_ctrl.proto. + DataState_PublicKey_ClientX509CertValidation DataState_PublicKey_Usage = 1 //superseded by FirstPartyX509CertValidation and ThirdPartyX509CertValidation + DataState_PublicKey_FirstPartyX509CertValidation DataState_PublicKey_Usage = 2 //network-internal CAs: controller certs, edge signing CA roots + DataState_PublicKey_ThirdPartyX509CertValidation DataState_PublicKey_Usage = 3 //externally registered CAs ) // Enum value maps for DataState_PublicKey_Usage. @@ -636,10 +639,14 @@ var ( DataState_PublicKey_Usage_name = map[int32]string{ 0: "JWTValidation", 1: "ClientX509CertValidation", + 2: "FirstPartyX509CertValidation", + 3: "ThirdPartyX509CertValidation", } DataState_PublicKey_Usage_value = map[string]int32{ - "JWTValidation": 0, - "ClientX509CertValidation": 1, + "JWTValidation": 0, + "ClientX509CertValidation": 1, + "FirstPartyX509CertValidation": 2, + "ThirdPartyX509CertValidation": 3, } ) @@ -4683,6 +4690,7 @@ type DataState_PublicKey struct { Kid string `protobuf:"bytes,2,opt,name=kid,proto3" json:"kid,omitempty"` //key id/fingerprint Usages []DataState_PublicKey_Usage `protobuf:"varint,3,rep,packed,name=usages,proto3,enum=ziti.edge_ctrl.pb.DataState_PublicKey_Usage" json:"usages,omitempty"` // what the public key in data is used for Format DataState_PublicKey_Format `protobuf:"varint,4,opt,name=format,proto3,enum=ziti.edge_ctrl.pb.DataState_PublicKey_Format" json:"format,omitempty"` //the format of the public key in data and chain + Intermediates [][]byte `protobuf:"bytes,5,rep,name=intermediates,proto3" json:"intermediates,omitempty"` //intermediate CA certs chaining to the anchor in data, same format unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4745,6 +4753,13 @@ func (x *DataState_PublicKey) GetFormat() DataState_PublicKey_Format { return DataState_PublicKey_X509CertDer } +func (x *DataState_PublicKey) GetIntermediates() [][]byte { + if x != nil { + return x.Intermediates + } + return nil +} + type DataState_PostureCheck struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -5451,7 +5466,7 @@ const file_edge_ctrl_proto_rawDesc = "" + "\x04data\x18\x01 \x03(\v2\".ziti.edge_ctrl.pb.Cache.DataEntryR\x04data\x1a7\n" + "\tDataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xb5$\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xa4%\n" + "\tDataState\x12:\n" + "\x06events\x18\x01 \x03(\v2\".ziti.edge_ctrl.pb.DataState.EventR\x06events\x12\x1a\n" + "\bendIndex\x18\x02 \x01(\x04R\bendIndex\x12\x1e\n" + @@ -5540,15 +5555,18 @@ const file_edge_ctrl_proto_rawDesc = "" + "configType\x18\x11 \x01(\v2'.ziti.edge_ctrl.pb.DataState.ConfigTypeH\x00R\n" + "configType\x12=\n" + "\x06config\x18\x12 \x01(\v2#.ziti.edge_ctrl.pb.DataState.ConfigH\x00R\x06configB\a\n" + - "\x05Model\x1a\xa6\x02\n" + + "\x05Model\x1a\x95\x03\n" + "\tPublicKey\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data\x12\x10\n" + "\x03kid\x18\x02 \x01(\tR\x03kid\x12D\n" + "\x06usages\x18\x03 \x03(\x0e2,.ziti.edge_ctrl.pb.DataState.PublicKey.UsageR\x06usages\x12E\n" + - "\x06format\x18\x04 \x01(\x0e2-.ziti.edge_ctrl.pb.DataState.PublicKey.FormatR\x06format\"8\n" + + "\x06format\x18\x04 \x01(\x0e2-.ziti.edge_ctrl.pb.DataState.PublicKey.FormatR\x06format\x12$\n" + + "\rintermediates\x18\x05 \x03(\fR\rintermediates\"\x80\x01\n" + "\x05Usage\x12\x11\n" + - "\rJWTValidation\x10\x00\x12\x1c\n" + - "\x18ClientX509CertValidation\x10\x01\",\n" + + "\rJWTValidation\x10\x00\x12 \n" + + "\x18ClientX509CertValidation\x10\x01\x1a\x02\b\x01\x12 \n" + + "\x1cFirstPartyX509CertValidation\x10\x02\x12 \n" + + "\x1cThirdPartyX509CertValidation\x10\x03\",\n" + "\x06Format\x12\x0f\n" + "\vX509CertDer\x10\x00\x12\x11\n" + "\rPKIXPublicKey\x10\x01\x1a\xa3\t\n" + diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.proto b/common/pb/edge_ctrl_pb/edge_ctrl.proto index d42c643bf..e5247f3d7 100644 --- a/common/pb/edge_ctrl_pb/edge_ctrl.proto +++ b/common/pb/edge_ctrl_pb/edge_ctrl.proto @@ -258,10 +258,13 @@ message DataState { string kid = 2; //key id/fingerprint repeated Usage usages = 3; // what the public key in data is used for Format format = 4; //the format of the public key in data and chain + repeated bytes intermediates = 5; //intermediate CA certs chaining to the anchor in data, same format enum Usage { JWTValidation = 0; - ClientX509CertValidation = 1; + ClientX509CertValidation = 1 [deprecated = true]; //superseded by FirstPartyX509CertValidation and ThirdPartyX509CertValidation + FirstPartyX509CertValidation = 2; //network-internal CAs: controller certs, edge signing CA roots + ThirdPartyX509CertValidation = 3; //externally registered CAs } enum Format { diff --git a/common/public_key_validate_roundtrip_test.go b/common/public_key_validate_roundtrip_test.go new file mode 100644 index 000000000..da13e339b --- /dev/null +++ b/common/public_key_validate_roundtrip_test.go @@ -0,0 +1,59 @@ +package common + +import ( + "testing" + + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// Test_PublicKeyIntermediatesSurviveValidateRoundTrip mirrors the router-data-model validate +// flow: a router's live model and a bare model rebuilt from a proto-round-tripped controller +// snapshot must agree on a public key that carries intermediates. +func Test_PublicKeyIntermediatesSurviveValidateRoundTrip(t *testing.T) { + req := require.New(t) + + key := &edge_ctrl_pb.DataState_PublicKey{ + Kid: "kid1", + Data: []byte("anchor-der"), + Format: edge_ctrl_pb.DataState_PublicKey_X509CertDer, + Usages: []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }, + Intermediates: [][]byte{[]byte("intermediate-der")}, + } + evt := &edge_ctrl_pb.DataState_Event{ + Action: edge_ctrl_pb.DataState_Create, + Model: &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: key}, + IsSynthetic: true, + } + + current := NewBareRouterDataModel() + current.WhileLocked(func(u uint64) { + current.Handle(1, evt) + current.SetCurrentIndex(1) + }) + + state := &edge_ctrl_pb.DataState{Events: []*edge_ctrl_pb.DataState_Event{evt}, EndIndex: 1} + request := &edge_ctrl_pb.RouterDataModelValidateRequest{State: state} + wire, err := proto.Marshal(request) + req.NoError(err) + parsed := &edge_ctrl_pb.RouterDataModelValidateRequest{} + req.NoError(proto.Unmarshal(wire, parsed)) + + model := NewBareRouterDataModel() + model.WhileLocked(func(u uint64) { + for _, e := range parsed.State.Events { + model.Handle(parsed.State.EndIndex, e) + } + model.SetCurrentIndex(parsed.State.EndIndex) + }) + + var diffs []string + current.Validate(model, func(entityType string, id string, diffType DiffType, detail string) { + diffs = append(diffs, entityType+" "+id+" "+detail) + }) + req.Empty(diffs) +} diff --git a/controller/command/command.go b/controller/command/command.go index 843e0a135..01b7aeaa6 100644 --- a/controller/command/command.go +++ b/controller/command/command.go @@ -18,6 +18,7 @@ package command import ( "reflect" + "sync" "github.com/michaelquigley/pfxlog" "github.com/openziti/channel/v4" @@ -67,12 +68,27 @@ type Dispatcher interface { GetRateLimiter() rate.RateLimiter Bootstrap() error CtrlAddresses() (uint64, []string, []*ctrl_pb.CtrlDetail) + + // GetDecoders returns the command decoder registry used to decode commands dispatched + // through this dispatcher. Each dispatcher owns its own registry so multiple controllers + // in one process don't decode each other's commands. + GetDecoders() Decoders } // LocalDispatcher should be used when running a non-clustered system type LocalDispatcher struct { EncodeDecodeCommands bool Limiter rate.RateLimiter + + decodersInit sync.Once + decoders Decoders +} + +func (self *LocalDispatcher) GetDecoders() Decoders { + self.decodersInit.Do(func() { + self.decoders = NewDecoders() + }) + return self.decoders } func (self *LocalDispatcher) Bootstrap() error { @@ -125,7 +141,7 @@ func (self *LocalDispatcher) Dispatch(command Command) error { if err != nil { return err } - cmd, err := GetDefaultDecoders().Decode(bytes) + cmd, err := self.GetDecoders().Decode(bytes) if err != nil { return err } diff --git a/controller/controller.go b/controller/controller.go index e1cd5c119..8fb08ef30 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -33,6 +33,7 @@ import ( "github.com/openziti/channel/v4" "github.com/openziti/channel/v4/protobufs" "github.com/openziti/foundation/v2/concurrenz" + nfpem "github.com/openziti/foundation/v2/pem" "github.com/openziti/foundation/v2/versions" "github.com/openziti/identity" "github.com/openziti/metrics" @@ -717,11 +718,21 @@ func (c *Controller) registerXts() { } func (c *Controller) registerComponents() error { - c.ctrlConnectHandler = handler_ctrl.NewConnectHandler(c.config.Id, c.network) + c.ctrlConnectHandler = handler_ctrl.NewConnectHandler(c.config.Id, c.network, c.signingCertRoots()) c.eventDispatcher.AddClusterEventHandler(event.ClusterEventHandlerF(c.routerDispatchCallback)) return nil } +// signingCertRoots returns the trust anchors from the edge enrollment signing CA bundle, which issues the +// certificates routers present on the control channel. It is empty when no signing CA bundle is +// configured, in which case a router is trusted only through the controller's own CA bundle. +func (c *Controller) signingCertRoots() []*x509.Certificate { + if c.config.Edge == nil { + return nil + } + return nfpem.PemBytesToCertificates(c.config.Edge.Enrollment.SigningCertCaPem) +} + func (c *Controller) RegisterXctrl(x xctrl.Xctrl) error { if err := c.config.Configure(x); err != nil { return err diff --git a/controller/handler_ctrl/connect.go b/controller/handler_ctrl/connect.go index 4108937e9..22f3d782c 100644 --- a/controller/handler_ctrl/connect.go +++ b/controller/handler_ctrl/connect.go @@ -34,15 +34,24 @@ type ConnectHandler struct { identity identity.Identity network *network.Network + // signingCertRoots holds the edge enrollment signing CA bundle. A router presents its enrollment + // certificate as its control channel client certificate, so a deployment whose signing CA sits + // outside the controller's own trust bundle would otherwise have every router refused here. + signingCertRoots []*x509.Certificate + // separatelyValidatedTypes holds the control-channel type headers that are dispatched to a // separate, self-validating acceptor (currently the raft mesh, when clustering is enabled). separatelyValidatedTypes map[string]struct{} } -func NewConnectHandler(identity identity.Identity, network *network.Network) *ConnectHandler { +// NewConnectHandler returns a ConnectHandler that admits routers whose leaf certificate chains either to +// the controller's own CA bundle or to signingCertRoots, the edge enrollment signing CA bundle. +// signingCertRoots may be empty, in which case only the controller's bundle is trusted. +func NewConnectHandler(identity identity.Identity, network *network.Network, signingCertRoots []*x509.Certificate) *ConnectHandler { return &ConnectHandler{ - identity: identity, - network: network, + identity: identity, + network: network, + signingCertRoots: signingCertRoots, } } @@ -110,11 +119,11 @@ func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates } // Verify the peer's leaf certificate (certificates[0], the certificate whose private key the TLS - // handshake proved) chains to the controller CA, and bind the router fingerprint check to that - // verified leaf. Matching the enrolled fingerprint against any presented certificate would let a - // peer present its own leaf followed by a target router's public certificate and pass without that - // router's private key. - leaf, err := cert.VerifyLeafCertChain(self.identity.CA(), certificates) + // handshake proved) chains to the controller CA or the edge signing CA, and bind the router + // fingerprint check to that verified leaf. Matching the enrolled fingerprint against any presented + // certificate would let a peer present its own leaf followed by a target router's public + // certificate and pass without that router's private key. + leaf, err := cert.VerifyLeafCertChain(self.identity.CA(), certificates, self.signingCertRoots...) if err != nil { return fmt.Errorf("unable to verify dialer, routerId: %v: %w", id, err) } diff --git a/controller/model/command.go b/controller/model/command.go index 9ad0a08ab..9e31bdbf1 100644 --- a/controller/model/command.go +++ b/controller/model/command.go @@ -42,11 +42,10 @@ const ( ) func newCommandManager(env Env, registry ioc.Registry) *CommandManager { - command.GetDefaultDecoders().Clear() result := &CommandManager{ env: env, registry: registry, - Decoders: command.GetDefaultDecoders(), + Decoders: env.GetCommandDispatcher().GetDecoders(), backgroundDelayThreshold: env.GetConfig().Command.Background.DelayThreshold, backgroundWorkTimer: env.GetMetricsRegistry().Timer(backgroundQueueMetricsBase + ".work_timer"), } diff --git a/controller/model/controller_manager.go b/controller/model/controller_manager.go index 9993403a9..6261763a5 100644 --- a/controller/model/controller_manager.go +++ b/controller/model/controller_manager.go @@ -19,11 +19,11 @@ package model import ( "crypto/x509" "fmt" + "strings" "time" "github.com/michaelquigley/pfxlog" nfpem "github.com/openziti/foundation/v2/pem" - "github.com/openziti/ziti/v2/controller/storage/boltz" "github.com/openziti/ziti/v2/common/pb/edge_cmd_pb" "github.com/openziti/ziti/v2/controller/change" "github.com/openziti/ziti/v2/controller/command" @@ -31,6 +31,7 @@ import ( "github.com/openziti/ziti/v2/controller/event" "github.com/openziti/ziti/v2/controller/fields" "github.com/openziti/ziti/v2/controller/models" + "github.com/openziti/ziti/v2/controller/storage/boltz" "google.golang.org/protobuf/proto" ) @@ -291,7 +292,7 @@ func (self *ControllerManager) UpdateControllerState(peers []*event.ClusterPeer, Id: peer.Id, }, Name: peer.ServerCert[0].Subject.CommonName, - CertPem: nfpem.EncodeToString(peer.ServerCert[0]), + CertPem: certChainPem(peer.ServerCert), Fingerprint: nfpem.FingerprintFromCertificate(peer.ServerCert[0]), CtrlAddress: peer.Addr, IsOnline: true, @@ -403,7 +404,7 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { Id: peer.Id, }, Name: peer.ServerCert[0].Subject.CommonName, - CertPem: nfpem.EncodeToString(peer.ServerCert[0]), + CertPem: certChainPem(peer.ServerCert), Fingerprint: nfpem.FingerprintFromCertificate(peer.ServerCert[0]), CtrlAddress: peer.Addr, IsOnline: true, @@ -411,15 +412,15 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { ApiAddresses: apiAddressesFromPeer(peer), } disconnectFields := fields.UpdatedFieldsMap{ - db.FieldControllerIsOnline: struct{}{}, - db.FieldControllerCertPem: struct{}{}, - db.FieldControllerFingerprint: struct{}{}, - db.FieldControllerCtrlAddress: struct{}{}, - db.FieldControllerApiAddresses: struct{}{}, - db.FieldControllerApiAddressUrl: struct{}{}, - db.FieldControllerApiAddressVersion: struct{}{}, - db.FieldControllerIsPreferredLeader: struct{}{}, - db.FieldName: struct{}{}, + db.FieldControllerIsOnline: struct{}{}, + db.FieldControllerCertPem: struct{}{}, + db.FieldControllerFingerprint: struct{}{}, + db.FieldControllerCtrlAddress: struct{}{}, + db.FieldControllerApiAddresses: struct{}{}, + db.FieldControllerApiAddressUrl: struct{}{}, + db.FieldControllerApiAddressVersion: struct{}{}, + db.FieldControllerIsPreferredLeader: struct{}{}, + db.FieldName: struct{}{}, } changeCtx := change.New() @@ -432,6 +433,15 @@ func (self *ControllerManager) UpdateSelfOnNewLeader() { } // apiAddressFromPeer converts event.ClusterPeer API Addresses to model API Addresses +// certChainPem encodes certs (leaf first) as concatenated PEM. +func certChainPem(certs []*x509.Certificate) string { + sb := strings.Builder{} + for _, cert := range certs { + sb.WriteString(nfpem.EncodeToString(cert)) + } + return sb.String() +} + func apiAddressesFromPeer(peer *event.ClusterPeer) map[string][]ApiAddress { result := map[string][]ApiAddress{} diff --git a/controller/raft/mesh/mesh.go b/controller/raft/mesh/mesh.go index cfe489136..ddc7495d1 100644 --- a/controller/raft/mesh/mesh.go +++ b/controller/raft/mesh/mesh.go @@ -43,12 +43,13 @@ import ( const ( // New header IDs, starting at 2000 to avoid conflicts with channel base headers (0-12+) - PeerAddrHeader = 2000 - SigningCertHeader = 2001 - ApiAddressesHeader = 2002 - RaftConnIdHeader = 2003 - ClusterIdHeader = 2004 - PreferredLeaderHeader = 2005 + PeerAddrHeader = 2000 + SigningCertHeader = 2001 + ApiAddressesHeader = 2002 + RaftConnIdHeader = 2003 + ClusterIdHeader = 2004 + PreferredLeaderHeader = 2005 + SigningCertChainHeader = 2006 // full signing cert chain, leaf first, as concatenated DER // Legacy header IDs, used as fallback when reading from older peers LegacyPeerAddrHeader = 11 @@ -458,8 +459,10 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer tlsCert := self.nodeId.ServerCert() var serverCert []byte + var serverCertChain []byte if len(tlsCert) != 0 && len(tlsCert[0].Certificate) != 0 { serverCert = tlsCert[0].Certificate[0] + serverCertChain = ConcatDer(tlsCert[0].Certificate) } headers := map[int32][]byte{ @@ -469,6 +472,7 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer LegacyPeerAddrHeader: []byte(self.raftAddr), SigningCertHeader: serverCert, LegacySigningCertHeader: serverCert, + SigningCertChainHeader: serverCertChain, ClusterIdHeader: []byte(self.env.GetClusterId()), LegacyClusterIdHeader: []byte(self.env.GetClusterId()), } @@ -478,14 +482,16 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer } // Check if a recent dial to this address failed (e.g., hello too large for an old peer). - // If so, strip the new signing cert header so only the legacy header is sent, allowing + // If so, strip the new signing cert headers so only the legacy header is sent, allowing // the connection to succeed with old controllers that enforce the smaller hello limit. self.lock.RLock() if rec := self.dialRecords[address]; rec != nil && time.Since(rec.lastAttempt) < RecentDialInterval { if rec.peerVersion == nil { delete(headers, SigningCertHeader) + delete(headers, SigningCertChainHeader) } else if hasMin, _ := rec.peerVersion.HasMinimumVersion("v2.0.0"); !hasMin { delete(headers, SigningCertHeader) + delete(headers, SigningCertChainHeader) } } self.lock.RUnlock() @@ -551,7 +557,10 @@ func (self *impl) GetOrConnectPeer(address string, timeout time.Duration) (*Peer } peer.Version = versionInfo - peer.SigningCerts = []*x509.Certificate{underlay.Certificates()[0]} + peer.SigningCerts = signingCertsFromHeaders(peer.Channel.Underlay().Headers()) + if len(peer.SigningCerts) == 0 { + peer.SigningCerts = underlay.Certificates() + } self.lock.Lock() if rec := self.dialRecords[address]; rec != nil { @@ -931,13 +940,9 @@ func (self *impl) AcceptUnderlay(underlay channel.Underlay) error { peer.ClusterId = getPeerClusterId(peer.Channel) peer.Version = versionInfo - if certHeader, found := headerWithFallback(ch.Underlay().Headers(), SigningCertHeader, LegacySigningCertHeader); found { - if cert, err := x509.ParseCertificate(certHeader); err == nil { - peer.SigningCerts = []*x509.Certificate{cert} - } - } + peer.SigningCerts = signingCertsFromHeaders(ch.Underlay().Headers()) if len(peer.SigningCerts) == 0 { - peer.SigningCerts = []*x509.Certificate{underlay.Certificates()[0]} + peer.SigningCerts = underlay.Certificates() } binding.AddReceiveHandlerF(RaftDataType, peer.handleReceiveData) @@ -1024,6 +1029,33 @@ func headerWithFallback(headers map[int32][]byte, key int32, legacyKey int32) ([ return nil, false } +// ConcatDer concatenates DER-encoded certificates into a single byte slice, parseable +// with x509.ParseCertificates. +func ConcatDer(certs [][]byte) []byte { + var result []byte + for _, der := range certs { + result = append(result, der...) + } + return result +} + +// signingCertsFromHeaders extracts a peer's signing certificates from hello headers, +// preferring the full chain header (leaf first) over the older single-cert headers. +// Returns nil when no header yields a certificate. +func signingCertsFromHeaders(headers map[int32][]byte) []*x509.Certificate { + if chainHeader, found := headers[SigningCertChainHeader]; found { + if certs, err := x509.ParseCertificates(chainHeader); err == nil && len(certs) > 0 { + return certs + } + } + if certHeader, found := headerWithFallback(headers, SigningCertHeader, LegacySigningCertHeader); found { + if signingCert, err := x509.ParseCertificate(certHeader); err == nil { + return []*x509.Certificate{signingCert} + } + } + return nil +} + func getUint32HeaderWithFallback(m *channel.Message, key int32, legacyKey int32) (uint32, bool) { if val, ok := m.GetUint32Header(key); ok { return val, true diff --git a/controller/raft/mesh/signing_certs_test.go b/controller/raft/mesh/signing_certs_test.go new file mode 100644 index 000000000..e7f60b52b --- /dev/null +++ b/controller/raft/mesh/signing_certs_test.go @@ -0,0 +1,124 @@ +package mesh + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// newTestCertChain creates a self-signed root and a leaf issued by it, returning [leaf, root]. +func newTestCertChain(name string) ([]*x509.Certificate, error) { + rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + rootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: name + "-root"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + rootDer, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + rootCert, err := x509.ParseCertificate(rootDer) + if err != nil { + return nil, err + } + + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: name + "-leaf"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + leafDer, err := x509.CreateCertificate(rand.Reader, leafTemplate, rootCert, &leafKey.PublicKey, rootKey) + if err != nil { + return nil, err + } + leafCert, err := x509.ParseCertificate(leafDer) + if err != nil { + return nil, err + } + + return []*x509.Certificate{leafCert, rootCert}, nil +} + +func Test_signingCertsFromHeaders(t *testing.T) { + chain, err := newTestCertChain("mesh-test") + require.NoError(t, err) + + t.Run("chain header yields the full chain", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertChainHeader: ConcatDer([][]byte{chain[0].Raw, chain[1].Raw}), + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 2) + req.True(certs[0].Equal(chain[0])) + req.True(certs[1].Equal(chain[1])) + }) + + t.Run("falls back to the single-cert header when no chain header present", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("falls back to the legacy single-cert header", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + LegacySigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("unparsable chain header falls back to the single-cert header", func(t *testing.T) { + req := require.New(t) + headers := map[int32][]byte{ + SigningCertChainHeader: []byte("not a certificate"), + SigningCertHeader: chain[0].Raw, + } + + certs := signingCertsFromHeaders(headers) + req.Len(certs, 1) + req.True(certs[0].Equal(chain[0])) + }) + + t.Run("no cert headers yields nil", func(t *testing.T) { + req := require.New(t) + certs := signingCertsFromHeaders(map[int32][]byte{}) + req.Nil(certs) + }) +} diff --git a/controller/raft/raft.go b/controller/raft/raft.go index 52f75945e..abfdb6cbf 100644 --- a/controller/raft/raft.go +++ b/controller/raft/raft.go @@ -138,6 +138,7 @@ func NewController(env Env, migrationMgr MigrationManager) *Controller { clusterEvents: make(chan raft.Observation, 16), raftRateLimiter: command.NewAdaptiveRateLimitTracker(env.GetRaftRateLimiterConfig(), env.GetMetricsRegistry(), env.GetCloseNotify()), errorMappers: map[string]func(map[string]any) error{}, + decoders: command.NewDecoders(), } result.initErrorMappers() return result @@ -162,6 +163,13 @@ type Controller struct { clusterEvents chan raft.Observation raftRateLimiter rate.AdaptiveRateLimitTracker errorMappers map[string]func(map[string]any) error + decoders command.Decoders +} + +// GetDecoders returns the command decoder registry this controller's raft FSM uses to decode +// replicated log entries. It is per-controller so multiple in-process controllers stay isolated. +func (self *Controller) GetDecoders() command.Decoders { + return self.decoders } func (self *Controller) GetNodeId() *identity.TokenId { @@ -200,6 +208,9 @@ func (self *Controller) GetListenerHeaders() map[int32][]byte { if self.Config.PreferredLeader { headers[mesh.PreferredLeaderHeader] = []byte{1} } + if serverCerts := self.env.GetId().ServerCert(); len(serverCerts) > 0 && len(serverCerts[0].Certificate) > 0 { + headers[mesh.SigningCertChainHeader] = mesh.ConcatDer(serverCerts[0].Certificate) + } return headers } @@ -635,7 +646,7 @@ func (self *Controller) Init() error { self.clusterEvents <- obs }) - self.Fsm = NewFsm(raftConfig.DataDir, raftConfig.RestartSelf, command.GetDefaultDecoders(), self.indexTracker, self.env.GetEventDispatcher()) + self.Fsm = NewFsm(raftConfig.DataDir, raftConfig.RestartSelf, self.decoders, self.indexTracker, self.env.GetEventDispatcher()) if err = self.Fsm.Init(); err != nil { return fmt.Errorf("failed to init FSM (%w)", err) diff --git a/controller/sync_strats/public_key_test.go b/controller/sync_strats/public_key_test.go new file mode 100644 index 000000000..f06d901a1 --- /dev/null +++ b/controller/sync_strats/public_key_test.go @@ -0,0 +1,50 @@ +package sync_strats + +import ( + "crypto/sha1" + "fmt" + "testing" + + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" + "github.com/stretchr/testify/require" +) + +func Test_newPublicKey(t *testing.T) { + data := []byte("anchor-cert-der") + intermediate1 := []byte("intermediate-1-der") + intermediate2 := []byte("intermediate-2-der") + + t.Run("sets kid from data fingerprint and carries usages and intermediates", func(t *testing.T) { + req := require.New(t) + + publicKey := newPublicKey(data, edge_ctrl_pb.DataState_PublicKey_X509CertDer, firstPartyCaUsages, intermediate1, intermediate2) + + req.Equal(data, publicKey.Data) + req.Equal(fmt.Sprintf("%x", sha1.Sum(data)), publicKey.Kid) + req.Equal(firstPartyCaUsages, publicKey.Usages) + req.Equal(edge_ctrl_pb.DataState_PublicKey_X509CertDer, publicKey.Format) + req.Equal([][]byte{intermediate1, intermediate2}, publicKey.Intermediates) + }) + + t.Run("no intermediates yields empty intermediates", func(t *testing.T) { + req := require.New(t) + + publicKey := newPublicKey(data, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages) + + req.Empty(publicKey.Intermediates) + }) + + t.Run("usage sets pair the deprecated usage with the party-specific usage", func(t *testing.T) { + req := require.New(t) + + req.Equal([]edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation}, controllerCertUsages) + + req.Contains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) + req.Contains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) + req.NotContains(firstPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) + + req.Contains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) + req.Contains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) + req.NotContains(thirdPartyCaUsages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) + }) +} diff --git a/controller/sync_strats/sync_instant.go b/controller/sync_strats/sync_instant.go index c6c1b0ff0..fd1b179fe 100644 --- a/controller/sync_strats/sync_instant.go +++ b/controller/sync_strats/sync_instant.go @@ -19,7 +19,7 @@ package sync_strats import ( "context" "crypto/sha1" - "crypto/tls" + "crypto/x509" "encoding/binary" "encoding/json" "fmt" @@ -123,18 +123,6 @@ func (strategy *InstantStrategy) NextIndex(ctx boltz.MutateContext) (uint64, err return strategy.indexProvider.NextIndex(ctx) } -func (strategy *InstantStrategy) AddPublicKey(cert *tls.Certificate) { - publicKey := newPublicKey(cert.Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation}) - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} - newEvent := &edge_ctrl_pb.DataState_Event{ - Action: edge_ctrl_pb.DataState_Create, - Model: newModel, - IsSynthetic: true, - } - - strategy.HandlePublicKeyEvent(newEvent, newModel) -} - // Initialize implements RouterDataModelCache func (strategy *InstantStrategy) Initialize(logSize uint64, bufferSize uint) error { strategy.RouterDataModelSender = common.NewRouterDataModelSender(strategy.ae, logSize, bufferSize) @@ -779,10 +767,12 @@ func (strategy *InstantStrategy) synchronize(rtx *RouterSender) { } for _, pk := range pks { + // Send the stored key as-is: rebuilding it field-by-field silently drops fields + // (this synthetic set bypasses router index checks and overwrites the full-sync copy). peerEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: &edge_ctrl_pb.DataState_Event_PublicKey{ - PublicKey: newPublicKey(pk.Data, pk.Format, pk.Usages), + PublicKey: pk, }, IsSynthetic: true, } @@ -904,7 +894,11 @@ func (strategy *InstantStrategy) BuildServicePolicies(tx *bbolt.Tx, rdm *common. func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.RouterDataModelSender) error { serverTls := strategy.ae.HostController.Identity().ServerCert() - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(serverTls[0].Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})} + // Controller certs are published leaf-only: routers use them solely for JWT validation, which + // needs no chain, and a controller cert's kid is emitted by several paths (identity TLS chain + // here, controller store records below and on create/update events). Identical content keeps + // the sender and router models convergent under last-writer-wins by kid. + newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(serverTls[0].Certificate[0], edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: newModel, @@ -923,7 +917,7 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route } certs := nfPem.PemStringToCertificates(storeModel.CertPem) - newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_JWTValidation, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})} + newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, Model: newModel, @@ -935,9 +929,18 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route caPEMs := strategy.ae.GetConfig().Edge.CaPems() caCerts := nfPem.PemBytesToCertificates(caPEMs) + // Non-root CA certs in the bundle are intermediates; publish them on every root from + // the same bundle. Verification treats intermediates as a pool, so extras are harmless. + var caIntermediates [][]byte + for _, caCert := range caCerts { + if caCert.IsCA && !identity.IsRootCa(caCert) { + caIntermediates = append(caIntermediates, caCert.Raw) + } + } + for _, caCert := range caCerts { if identity.IsRootCa(caCert) { - publicKey := newPublicKey(caCert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation}) + publicKey := newPublicKey(caCert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, firstPartyCaUsages, caIntermediates...) newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} newEvent := &edge_ctrl_pb.DataState_Event{ Action: edge_ctrl_pb.DataState_Create, @@ -965,7 +968,7 @@ func (strategy *InstantStrategy) BuildPublicKeys(tx *bbolt.Tx, rdm *common.Route continue } - publicKey := newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation}) + publicKey := newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...) newModel := &edge_ctrl_pb.DataState_Event_PublicKey{PublicKey: publicKey} newEvent := &edge_ctrl_pb.DataState_Event{ @@ -1605,13 +1608,44 @@ func newService(storeModel *db.EdgeService) *edge_ctrl_pb.DataState_Service { } } -func newPublicKey(data []byte, format edge_ctrl_pb.DataState_PublicKey_Format, usages []edge_ctrl_pb.DataState_PublicKey_Usage) *edge_ctrl_pb.DataState_PublicKey { - return &edge_ctrl_pb.DataState_PublicKey{ - Data: data, - Kid: fmt.Sprintf("%x", sha1.Sum(data)), - Usages: usages, - Format: format, +// Usage sets for published public keys. ClientX509CertValidation is deprecated but still +// emitted on CA anchors so routers predating the first/third-party usages keep validating +// client certs. Controller certs carry only JWTValidation: a controller identity is never a +// CA, so its certs anchor no client cert chains — trust anchors come from the CA bundles. +var ( + controllerCertUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_JWTValidation, } + firstPartyCaUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + } + thirdPartyCaUsages = []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + } +) + +// newPublicKey builds a DataState_PublicKey for data, deriving the kid from its SHA-1 +// fingerprint. Any intermediates are CA certs, in the same format, chaining data's issued +// certs to data. +func newPublicKey(data []byte, format edge_ctrl_pb.DataState_PublicKey_Format, usages []edge_ctrl_pb.DataState_PublicKey_Usage, intermediates ...[]byte) *edge_ctrl_pb.DataState_PublicKey { + return &edge_ctrl_pb.DataState_PublicKey{ + Data: data, + Kid: fmt.Sprintf("%x", sha1.Sum(data)), + Usages: usages, + Format: format, + Intermediates: intermediates, + } +} + +// certsToRaw returns the raw DER bytes of each certificate. +func certsToRaw(certs []*x509.Certificate) [][]byte { + var result [][]byte + for _, cert := range certs { + result = append(result, cert.Raw) + } + return result } func newPostureCheckById(tx *bbolt.Tx, ae *env.AppEnv, id string) (*edge_ctrl_pb.DataState_PostureCheck, error) { @@ -1847,21 +1881,19 @@ func (strategy *InstantStrategy) PostureCheckDelete(index uint64, postureCheck * func (strategy *InstantStrategy) ControllerCreate(index uint64, controller *db.Controller) { certs := nfPem.PemStringToCertificates(controller.CertPem) - cert := certs[0] - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(cert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)) } func (strategy *InstantStrategy) ControllerUpdate(index uint64, controller *db.Controller) { certs := nfPem.PemStringToCertificates(controller.CertPem) - cert := certs[0] - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(cert.Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, edge_ctrl_pb.DataState_PublicKey_JWTValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, controllerCertUsages)) } func (strategy *InstantStrategy) CaCreate(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Create, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...)) } } @@ -1869,7 +1901,7 @@ func (strategy *InstantStrategy) CaUpdate(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Update, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Update, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages, certsToRaw(certs[1:])...)) } } @@ -1877,7 +1909,7 @@ func (strategy *InstantStrategy) CaDelete(index uint64, ca *db.Ca) { certs := nfPem.PemBytesToCertificates([]byte(ca.CertPem)) if len(certs) > 0 { - strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Delete, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, []edge_ctrl_pb.DataState_PublicKey_Usage{edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation})) + strategy.handlePublicKey(index, edge_ctrl_pb.DataState_Delete, newPublicKey(certs[0].Raw, edge_ctrl_pb.DataState_PublicKey_X509CertDer, thirdPartyCaUsages)) } } diff --git a/router/env/ctrls.go b/router/env/ctrls.go index c10d25679..5edaaaafd 100644 --- a/router/env/ctrls.go +++ b/router/env/ctrls.go @@ -122,6 +122,7 @@ type networkControllers struct { leaderId concurrenz.AtomicValue[string] ctrlChangeListeners concurrenz.CopyOnWriteSlice[CtrlEventListener] controllerDetails concurrenz.AtomicValue[map[string]*ctrl_pb.CtrlDetail] + closed atomic.Bool // everConnected records that a control channel was established at some point. It is never cleared, so it // answers whether the router has ever reached a controller rather than whether it is reachable now. everConnected atomic.Bool @@ -237,6 +238,11 @@ func (self *networkControllers) getControllerDetail(controllerId string) *ctrl_p func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.CtrlDetail) { log := pfxlog.Logger().WithField("ctrlId", detail.Id).WithField("detail", detail) + if self.closed.Load() { + log.Info("network controllers closed, not dialing controller") + return + } + if len(detail.Endpoints) == 0 { log.Error("controller has no endpoints, unable to connect") return @@ -254,6 +260,10 @@ func (self *networkControllers) connectToControllerWithBackoff(detail *ctrl_pb.C idx := 0 operation := func() error { + if self.closed.Load() { + return backoff.Permanent(errors.New("network controllers closed")) + } + if detail.Id != "" && !self.idsBeingDialed.Has(detail.Id) { return backoff.Permanent(errors.New("controller removed before connection established")) } @@ -758,6 +768,7 @@ func (self *networkControllers) ForEach(f func(controllerId string, ch channel.C } func (self *networkControllers) Close() error { + self.closed.Store(true) self.idsBeingDialed.Clear() var errList []error self.ForEach(func(_ string, ch channel.Channel) { diff --git a/router/state/cert_origin.go b/router/state/cert_origin.go index 52f64044c..f852c4b94 100644 --- a/router/state/cert_origin.go +++ b/router/state/cert_origin.go @@ -23,6 +23,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" ) // CertOrigin indicates whether a client certificate was issued by the internal (first-party) CA @@ -35,23 +36,60 @@ const ( CertOriginThirdParty ) -// controllerRootCache caches the root CA extracted from a controller's cert chain. +// controllerRootCache caches the root CAs extracted from a controller's cert chain. type controllerRootCache struct { - mu sync.RWMutex - rootPool *x509.CertPool - inited bool + mu sync.RWMutex + roots []*x509.Certificate + inited bool } -// IsFirstPartyCert reports whether the leaf of peerCerts chains to a controller-trusted -// root CA. peerCerts is a TLS peer chain with the leaf at index 0; remaining entries are -// used as intermediates. Time validity is not checked; callers enforce expiry. +// IsFirstPartyCert reports whether the leaf of peerCerts chains to a first-party trust +// anchor: a router data model public key with the FirstPartyX509CertValidation usage, or +// a root CA from the ctrl channel certificate chain (covers controllers that predate the +// first-party usage). peerCerts is a TLS peer chain with the leaf at index 0; remaining +// entries are used as intermediates, along with intermediates published on the data model +// keys. Time validity is not checked; callers enforce expiry. func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { if len(peerCerts) == 0 { return false } - pool := self.getControllerRootPool() - if pool == nil { + roots := x509.NewCertPool() + intermediates := x509.NewCertPool() + rootCount := 0 + + if rdm := self.routerDataModel.Load(); rdm != nil { + for keysTuple := range rdm.PublicKeys.IterBuffered() { + publicKey := keysTuple.Val + if !contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) { + continue + } + + anchor, err := self.getX509FromData(publicKey.Kid, publicKey.GetData()) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse x509 certificate data for first party public key") + continue + } + roots.AddCert(anchor) + rootCount++ + + for _, intermediateDer := range publicKey.Intermediates { + intermediate, err := x509.ParseCertificate(intermediateDer) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse intermediate certificate data for first party public key") + continue + } + intermediates.AddCert(intermediate) + } + } + } + + for _, root := range self.getControllerRoots() { + roots.AddCert(root) + rootCount++ + } + + if rootCount == 0 { return false } @@ -61,13 +99,12 @@ func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { certCopy.NotBefore = time.Now().Add(-1 * time.Hour) certCopy.NotAfter = time.Now().Add(1 * time.Hour) - intermediates := x509.NewCertPool() for _, c := range peerCerts[1:] { intermediates.AddCert(c) } opts := x509.VerifyOptions{ - Roots: pool, + Roots: roots, Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, } @@ -79,12 +116,12 @@ func (self *ManagerImpl) IsFirstPartyCert(peerCerts []*x509.Certificate) bool { return false } -func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { +func (self *ManagerImpl) getControllerRoots() []*x509.Certificate { self.ctrlRootCache.mu.RLock() if self.ctrlRootCache.inited { - pool := self.ctrlRootCache.rootPool + roots := self.ctrlRootCache.roots self.ctrlRootCache.mu.RUnlock() - return pool + return roots } self.ctrlRootCache.mu.RUnlock() @@ -93,7 +130,7 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { // double-check after acquiring write lock if self.ctrlRootCache.inited { - return self.ctrlRootCache.rootPool + return self.ctrlRootCache.roots } ctrls := self.env.GetNetworkControllers() @@ -110,10 +147,10 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { } // Walk the cert chain to find the root (self-signed) CA. - rootPool := x509.NewCertPool() + var roots []*x509.Certificate for _, cert := range certs { if identity.IsRootCa(cert) { - rootPool.AddCert(cert) + roots = append(roots, cert) } } @@ -128,14 +165,13 @@ func (self *ManagerImpl) getControllerRootPool() *x509.CertPool { } if chains, err := certs[0].Verify(opts); err == nil { for _, chain := range chains { - root := chain[len(chain)-1] - rootPool.AddCert(root) + roots = append(roots, chain[len(chain)-1]) } } } } self.ctrlRootCache.inited = true - self.ctrlRootCache.rootPool = rootPool - return rootPool + self.ctrlRootCache.roots = roots + return roots } diff --git a/router/state/cert_origin_test.go b/router/state/cert_origin_test.go new file mode 100644 index 000000000..f5a2f8ae1 --- /dev/null +++ b/router/state/cert_origin_test.go @@ -0,0 +1,381 @@ +package state + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "math/big" + "testing" + "time" + + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/stretchr/testify/require" + + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" +) + +// testCa holds a CA certificate and its signing key for issuing test certificates. +type testCa struct { + cert *x509.Certificate + key *ecdsa.PrivateKey +} + +// newTestRootCa creates a self-signed root CA. +func newTestRootCa(name string) (*testCa, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return &testCa{cert: cert, key: key}, nil +} + +// newIntermediateCa creates an intermediate CA issued by this CA. +func (ca *testCa) newIntermediateCa(name string) (*testCa, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + } + + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return &testCa{cert: cert, key: key}, nil +} + +// issueClientCert issues a client leaf certificate from this CA. +func (ca *testCa) issueClientCert(name string) (*x509.Certificate, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, big.NewInt(int64(1)<<62)) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: name}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(der) +} + +// newCertOriginTestManager builds a ManagerImpl with just enough state for +// IsFirstPartyCert: a pre-seeded ctrl-channel root pool and an empty router data model. +func newCertOriginTestManager(ctrlRoot *x509.Certificate) *ManagerImpl { + mgr := &ManagerImpl{ + certCache: cmap.New[*x509.Certificate](), + } + + if ctrlRoot != nil { + mgr.ctrlRootCache.roots = []*x509.Certificate{ctrlRoot} + } + mgr.ctrlRootCache.inited = true + + mgr.routerDataModel.Store(common.NewBareRouterDataModel()) + + return mgr +} + +// publishPublicKey adds a public key to the manager's router data model with the given +// usages and optional published intermediates. +func publishPublicKey(mgr *ManagerImpl, kid string, cert *x509.Certificate, usages []edge_ctrl_pb.DataState_PublicKey_Usage, intermediates ...*x509.Certificate) { + publicKey := &edge_ctrl_pb.DataState_PublicKey{ + Data: cert.Raw, + Kid: kid, + Usages: usages, + Format: edge_ctrl_pb.DataState_PublicKey_X509CertDer, + } + + for _, intermediate := range intermediates { + publicKey.Intermediates = append(publicKey.Intermediates, intermediate.Raw) + } + + mgr.routerDataModel.Load().PublicKeys.Set(kid, publicKey) +} + +func Test_IsFirstPartyCert(t *testing.T) { + ctrlCa, err := newTestRootCa("ctrl-root") + require.NoError(t, err) + + signingCa, err := newTestRootCa("edge-signing-root") + require.NoError(t, err) + + thirdPartyCa, err := newTestRootCa("third-party-root") + require.NoError(t, err) + + t.Run("cert issued by the ctrl channel root is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + clientCert, err := ctrlCa.issueClientCert("client-ctrl") + req.NoError(err) + + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a distinct edge signing CA published as first party is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }) + + clientCert, err := signingCa.issueClientCert("client-signing") + req.NoError(err) + + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a published intermediate of the edge signing CA is first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + intermediateCa, err := signingCa.newIntermediateCa("edge-signing-intermediate") + req.NoError(err) + + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + }, intermediateCa.cert) + + clientCert, err := intermediateCa.issueClientCert("client-intermediate") + req.NoError(err) + + // leaf only; the intermediate must come from the published key, not the peer chain + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by a third party CA is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "third-party-root", thirdPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + }) + + clientCert, err := thirdPartyCa.issueClientCert("client-third-party") + req.NoError(err) + + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("cert issued by an unpublished CA is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + clientCert, err := signingCa.issueClientCert("client-unpublished") + req.NoError(err) + + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{clientCert})) + }) + + t.Run("old controller publishing only deprecated usage retains prior behavior", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + publishPublicKey(mgr, "signing-root", signingCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + + ctrlClientCert, err := ctrlCa.issueClientCert("client-ctrl-old") + req.NoError(err) + req.True(mgr.IsFirstPartyCert([]*x509.Certificate{ctrlClientCert})) + + signingClientCert, err := signingCa.issueClientCert("client-signing-old") + req.NoError(err) + req.False(mgr.IsFirstPartyCert([]*x509.Certificate{signingClientCert})) + }) + + t.Run("no peer certs is not first party", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(ctrlCa.cert) + + req.False(mgr.IsFirstPartyCert(nil)) + }) +} + +// parseCertDirect is a parseCert callback for buildClientCertRoots without caching. +func parseCertDirect(_ string, data []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(data) +} + +// verifyAgainstRdmPools verifies cert against the root and intermediate pools built by +// buildClientCertRoots from rdm. A nil result means the cert chained to an anchor. +func verifyAgainstRdmPools(rdm *common.RouterDataModel, cert *x509.Certificate) error { + roots, published, count := buildClientCertRoots(rdm, parseCertDirect) + if count == 0 { + return errors.New("no anchors selected") + } + intermediates := x509.NewCertPool() + for _, intermediate := range published { + intermediates.AddCert(intermediate) + } + _, err := cert.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + return err +} + +func Test_buildClientCertRoots(t *testing.T) { + firstPartyCa, err := newTestRootCa("first-party-root") + require.NoError(t, err) + + thirdPartyCa, err := newTestRootCa("third-party-root") + require.NoError(t, err) + + t.Run("first and third party keys are both anchors", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest()) + publishPublicKey(mgr, "third", thirdPartyCa.cert, thirdPartyCaUsagesForTest()) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, firstPartyClient)) + + thirdPartyClient, err := thirdPartyCa.issueClientCert("client-third") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, thirdPartyClient)) + }) + + t.Run("deprecated-only key is ignored when new usages are present", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest()) + publishPublicKey(mgr, "deprecated", thirdPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + rdm := mgr.routerDataModel.Load() + + thirdPartyClient, err := thirdPartyCa.issueClientCert("client-third") + req.NoError(err) + req.Error(verifyAgainstRdmPools(rdm, thirdPartyClient)) + }) + + t.Run("deprecated keys are anchors when no key has the new usages", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "deprecated", firstPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + }) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, firstPartyClient)) + }) + + t.Run("published intermediates chain leaf certs to the anchor", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + + intermediateCa, err := firstPartyCa.newIntermediateCa("first-party-intermediate") + req.NoError(err) + + publishPublicKey(mgr, "first", firstPartyCa.cert, firstPartyCaUsagesForTest(), intermediateCa.cert) + rdm := mgr.routerDataModel.Load() + + leaf, err := intermediateCa.issueClientCert("client-intermediate") + req.NoError(err) + req.NoError(verifyAgainstRdmPools(rdm, leaf)) + }) + + t.Run("jwt-only key is not an anchor", func(t *testing.T) { + req := require.New(t) + mgr := newCertOriginTestManager(nil) + publishPublicKey(mgr, "jwt", firstPartyCa.cert, []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_JWTValidation, + }) + rdm := mgr.routerDataModel.Load() + + firstPartyClient, err := firstPartyCa.issueClientCert("client-first") + req.NoError(err) + req.Error(verifyAgainstRdmPools(rdm, firstPartyClient)) + }) +} + +// firstPartyCaUsagesForTest returns the usage set a current controller publishes for +// first-party CA roots. +func firstPartyCaUsagesForTest() []edge_ctrl_pb.DataState_PublicKey_Usage { + return []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation, + } +} + +// thirdPartyCaUsagesForTest returns the usage set a current controller publishes for +// third-party CAs. +func thirdPartyCaUsagesForTest() []edge_ctrl_pb.DataState_PublicKey_Usage { + return []edge_ctrl_pb.DataState_PublicKey_Usage{ + edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation, + edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation, + } +} diff --git a/router/state/cert_validating_identity.go b/router/state/cert_validating_identity.go index fea3a9663..77aac7677 100644 --- a/router/state/cert_validating_identity.go +++ b/router/state/cert_validating_identity.go @@ -25,13 +25,14 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" + "github.com/openziti/ziti/v2/common" "github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb" ) // certValidatingIdentity wraps an identity.Identity to add client certificate chain verification // at the TLS level via a VerifyConnection callback. It dynamically builds a CA pool from the -// router data model's PublicKeys with ClientX509CertValidation usage, ensuring the latest -// trust anchors are always used. +// router data model's client cert validation PublicKeys, ensuring the latest trust anchors +// are always used. type certValidatingIdentity struct { identity.Identity stateManager Manager @@ -57,6 +58,54 @@ func (self *certValidatingIdentity) ServerTLSConfig() *tls.Config { return cfg } +// buildClientCertRoots builds the trust anchor pool for client certificate verification +// from the router data model's public keys. Keys with the FirstPartyX509CertValidation or +// ThirdPartyX509CertValidation usage are trust anchors; when no key carries either usage +// (controller predates the first/third-party split), keys with the deprecated +// ClientX509CertValidation usage are used instead. Intermediates published on the selected +// keys are returned separately so callers can add them to their intermediate pool. +// parseCert converts a key's anchor data to a certificate, allowing callers to supply caching. +func buildClientCertRoots(rdm *common.RouterDataModel, parseCert func(kid string, data []byte) (*x509.Certificate, error)) (roots *x509.CertPool, intermediates []*x509.Certificate, rootCount int) { + roots = x509.NewCertPool() + + var anchors []*edge_ctrl_pb.DataState_PublicKey + var fallback []*edge_ctrl_pb.DataState_PublicKey + for keysTuple := range rdm.PublicKeys.IterBuffered() { + publicKey := keysTuple.Val + if contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_FirstPartyX509CertValidation) || + contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_ThirdPartyX509CertValidation) { + anchors = append(anchors, publicKey) + } else if contains(publicKey.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { + fallback = append(fallback, publicKey) + } + } + + if len(anchors) == 0 { + anchors = fallback + } + + for _, publicKey := range anchors { + anchor, err := parseCert(publicKey.Kid, publicKey.GetData()) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse x509 certificate data for client cert verification") + continue + } + roots.AddCert(anchor) + rootCount++ + + for _, intermediateDer := range publicKey.Intermediates { + intermediate, err := x509.ParseCertificate(intermediateDer) + if err != nil { + pfxlog.Logger().WithField("kid", publicKey.Kid).WithError(err).Error("could not parse intermediate certificate data for client cert verification") + continue + } + intermediates = append(intermediates, intermediate) + } + } + + return roots, intermediates, rootCount +} + // verifyConnection is a TLS VerifyConnection callback that verifies client certificates against // the CA pool built from RDM PublicKeys. func (self *certValidatingIdentity) verifyConnection(state tls.ConnectionState) error { @@ -71,32 +120,26 @@ func (self *certValidatingIdentity) verifyConnection(state tls.ConnectionState) return errors.New("router data model not yet available, cannot verify client certificate") } - rootPool := x509.NewCertPool() - intermediatePool := x509.NewCertPool() - certCount := 0 - for keysTuple := range rdm.PublicKeys.IterBuffered() { - if contains(keysTuple.Val.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { - parsed, err := x509.ParseCertificate(keysTuple.Val.GetData()) - if err != nil { - pfxlog.Logger().WithField("kid", keysTuple.Val.Kid).WithError(err).Error("could not parse x509 certificate data for TLS client verification") - continue - } - rootPool.AddCert(parsed) - certCount++ - } - } + rootPool, publishedIntermediates, certCount := buildClientCertRoots(rdm, func(_ string, data []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(data) + }) if certCount == 0 { return errors.New("no trusted CA certificates available in router data model") } - // Add the router identity's CA bundle as intermediates. The RDM PublicKeys typically - // contain only root CAs, but client certs may be signed by an intermediate CA that - // is part of the router's trust bundle. + // Start the intermediate pool from the router identity's CA bundle. The RDM anchors are + // typically root CAs, but client certs may be signed by an intermediate CA that is part + // of the router's trust bundle rather than published on an anchor. + intermediatePool := x509.NewCertPool() if idCa := self.Identity.CA(); idCa != nil { intermediatePool = idCa.Clone() } + for _, intermediate := range publishedIntermediates { + intermediatePool.AddCert(intermediate) + } + // Also add any additional certs from the TLS peer chain as intermediates. for _, cert := range state.PeerCertificates[1:] { intermediatePool.AddCert(cert) diff --git a/router/state/manager.go b/router/state/manager.go index 166a0b6bb..f45637356 100644 --- a/router/state/manager.go +++ b/router/state/manager.go @@ -857,27 +857,18 @@ func (self *ManagerImpl) getX509FromData(kid string, data []byte) (*x509.Certifi // certificate authorities, ensuring only properly signed certificates can // establish authenticated connections. func (self *ManagerImpl) VerifyClientCert(cert *x509.Certificate) error { - - rootPool := x509.NewCertPool() - rdm := self.routerDataModel.Load() - for keysTuple := range rdm.PublicKeys.IterBuffered() { - if contains(keysTuple.Val.Usages, edge_ctrl_pb.DataState_PublicKey_ClientX509CertValidation) { - cert, err := self.getX509FromData(keysTuple.Val.Kid, keysTuple.Val.GetData()) + rootPool, publishedIntermediates, _ := buildClientCertRoots(rdm, self.getX509FromData) - if err != nil { - pfxlog.Logger().WithField("kid", keysTuple.Val.Kid).WithError(err).Error("could not parse x509 certificate data") - continue - } - - rootPool.AddCert(cert) - } + intermediatePool := x509.NewCertPool() + for _, intermediate := range publishedIntermediates { + intermediatePool.AddCert(intermediate) } opts := x509.VerifyOptions{ Roots: rootPool, - Intermediates: x509.NewCertPool(), + Intermediates: intermediatePool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, CurrentTime: cert.NotBefore, } diff --git a/tests/api_session_totp_token_test.go b/tests/api_session_totp_token_test.go index 5167bb449..fa4c44e78 100644 --- a/tests/api_session_totp_token_test.go +++ b/tests/api_session_totp_token_test.go @@ -160,7 +160,9 @@ func Test_API_Session_TOTP_Tokens(t *testing.T) { code := adminTotpProvider.Code() + beforeTotpCreate := time.Now() totpToken, err := adminManagementClient.GetTotpToken(code) + afterTotpCreate := time.Now() ctx.Req.NoError(err) ctx.Req.NotNil(totpToken) ctx.Req.NotNil(totpToken.Token) @@ -191,8 +193,8 @@ func Test_API_Session_TOTP_Tokens(t *testing.T) { ctx.NoError(err) ctx.NotNil(issuedAt) - delta := issuedAt.Time.UTC().Sub(accessClaims.IssuedAt.AsTime().UTC()).Abs() - ctx.True(delta < 2*time.Millisecond) + // The serialized iat truncates to whole seconds, so the lower bound must too. + ctx.Req.WithinRange(issuedAt.Time, beforeTotpCreate.Truncate(time.Second), afterTotpCreate) ctx.Equal(accessClaims.ApiSessionId, totpClaims.ApiSessionId) diff --git a/tests/cli_tests/cli_test.go b/tests/cli_tests/cli_test.go index 5406d99ee..8d85c9354 100644 --- a/tests/cli_tests/cli_test.go +++ b/tests/cli_tests/cli_test.go @@ -269,6 +269,14 @@ func (s *cliTestState) runCLIWithContext(ctx context.Context, cmdToRun string) ( r, w, _ := os.Pipe() os.Stdout = w + // Drain the pipe while the command runs: pipe buffers are small (4KB on Windows), so a + // command producing more output than that deadlocks if reading is deferred until it exits. + outC := make(chan string, 1) + go func() { + outBytes, _ := io.ReadAll(r) + outC <- string(outBytes) + }() + v2 := cmd.NewRootCommand(os.Stdin, w, w) v2.SetArgs(strings.Split(cmdToRun, " ")) v2.SetContext(ctx) @@ -278,8 +286,7 @@ func (s *cliTestState) runCLIWithContext(ctx context.Context, cmdToRun string) ( _ = w.Close() os.Stdout = orig - outBytes, _ := io.ReadAll(r) - return string(outBytes), err + return <-outC, err } func test(t *testing.T, name string, fn func(*testing.T)) { diff --git a/tests/configsets.go b/tests/configsets.go index 8185563d7..517ccd526 100644 --- a/tests/configsets.go +++ b/tests/configsets.go @@ -3,12 +3,13 @@ package tests // ConfigSet describes a named collection of config files for a single test scenario. // All file paths are relative to the tests/ working directory. type ConfigSet struct { - Name string // display name matching the testdata/configs subdirectory - CtrlConfig string // controller config file; empty if the set does not define one - EdgeRouter string // edge router config; empty if the set does not define one - TunnelerRouter string // tunneler-enabled edge router config; empty if not defined - TransitRouter string // transit router config; empty if not defined - FabricRouters []string // fabric-only router configs, ordered by 1-based index + Name string // display name matching the testdata/configs subdirectory + CtrlConfig string // controller config file; empty if the set does not define one + PeerCtrlConfigs []string // additional cluster-member controller configs, joined to the primary by StartHaCluster + EdgeRouter string // edge router config; empty if the set does not define one + TunnelerRouter string // tunneler-enabled edge router config; empty if not defined + TransitRouter string // transit router config; empty if not defined + FabricRouters []string // fabric-only router configs, ordered by 1-based index } // DefaultATS is the standard full-stack config set used by the majority of the @@ -44,6 +45,25 @@ var DisabledOidcAutoBinding = ConfigSet{ CtrlConfig: "testdata/configs/disabled-oidc-auto-binding/ctrl.yml", } +// Ha3DataDir is the parent raft data directory used by the Ha3 config set. It is cleaned by +// StartHaCluster before each run and must contain the cluster.dataDir of every ha-3 controller. +const Ha3DataDir = "testdata/ha-3-data" + +// Ha3 is a three-controller raft cluster config set whose edge signing CA root is distinct from +// the ctrl-channel root CA. Each controller signs identity certs with its own intermediate under +// the shared signing root. Used to exercise first-party client cert validation when the signing +// CA and ctrl-channel CA differ, including certs issued by a controller other than the one a +// router is subscribed to. +var Ha3 = ConfigSet{ + Name: "ha-3", + CtrlConfig: "testdata/configs/ha-3/ctrl1.yml", + PeerCtrlConfigs: []string{ + "testdata/configs/ha-3/ctrl2.yml", + "testdata/configs/ha-3/ctrl3.yml", + }, + EdgeRouter: "testdata/configs/ha-3/edge-router.yml", +} + // DualOidcServers is a controller-only config set with two web server entries, each // on a different port and each hosting the edge-oidc API. Used to verify that the OIDC // discovery document returns issuer-specific endpoint URLs that reflect the port the diff --git a/tests/context.go b/tests/context.go index cdd5adf26..9d2a5fc1f 100644 --- a/tests/context.go +++ b/tests/context.go @@ -123,6 +123,7 @@ type TestContext struct { edgeRouterEntity *edgeRouter transitRouterEntity *transitRouter routers []*router.Router + peerControllers []*peerController testing *testing.T LogLevel string ControllerConfig *config.Config @@ -438,14 +439,14 @@ func (ctx *TestContext) StartServer() *ControllerHelper { // single test override settings (e.g., OIDC token durations) without affecting // other tests that call StartServer with the shared default config. func (ctx *TestContext) StartServerWithConfigModifier(modifier func(*config.Config)) *ControllerHelper { - return ctx.startServerWith("testdata/default.db", true, modifier) + return ctx.startServerWith("testdata/default.db", true, modifier, false) } func (ctx *TestContext) StartServerFor(testDb string, clean bool) *ControllerHelper { - return ctx.startServerWith(testDb, clean, nil) + return ctx.startServerWith(testDb, clean, nil, false) } -func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func(*config.Config)) *ControllerHelper { +func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func(*config.Config), initAdminAfterRun bool) *ControllerHelper { if ctx.LogLevel != "" { if level, err := logrus.ParseLevel(ctx.LogLevel); err == nil { logrus.StandardLogger().SetLevel(level) @@ -491,14 +492,14 @@ func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func ctx.EdgeController.Initialize() - err = ctx.EdgeController.AppEnv.Managers.Identity.InitializeDefaultAdmin(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password, eid.New()) - if err != nil { - log.WithError(err).Warn("error during initialize admin") + // In raft/cluster mode the default admin is created after Run, mirroring `ziti agent cluster + // init` against a live controller: InitializeDefaultAdmin bootstraps raft and triggers self + // registration, which captures API addresses from the running xweb, so it must run once xweb is + // fully up. Non-raft controllers initialize the admin before Run as before. + if !initAdminAfterRun { + ctx.initializeDefaultAdmin() } - logrus.Infof("default admin - username: %v", ctx.AdminAuthenticator.Username) - logrus.Infof("default admin - password: %v", ctx.AdminAuthenticator.Password) - ctx.EdgeController.Run() go func() { err = ctx.fabricController.Run() @@ -507,9 +508,25 @@ func (ctx *TestContext) startServerWith(testDb string, clean bool, modifier func err = ctx.waitForRestAPIPort(time.Minute * 5) ctx.Req.NoError(err) + if initAdminAfterRun { + ctx.initializeDefaultAdmin() + } + return &ControllerHelper{Controller: ctx.EdgeController} } +// initializeDefaultAdmin creates the default admin identity. In raft mode this also bootstraps the +// cluster (via Dispatcher.Bootstrap) and waits for leadership before the admin is created. +func (ctx *TestContext) initializeDefaultAdmin() { + err := ctx.EdgeController.AppEnv.Managers.Identity.InitializeDefaultAdmin(ctx.AdminAuthenticator.Username, ctx.AdminAuthenticator.Password, eid.New()) + if err != nil { + pfxlog.Logger().WithError(err).Warn("error during initialize admin") + } + + logrus.Infof("default admin - username: %v", ctx.AdminAuthenticator.Username) + logrus.Infof("default admin - password: %v", ctx.AdminAuthenticator.Password) +} + func (ctx *TestContext) createAndEnrollEdgeRouter(tunneler bool, roleAttributes ...string) *edgeRouter { ctx.requireCreateEdgeRouter(tunneler, roleAttributes...) ctx.requireEnrollEdgeRouter(tunneler, ctx.edgeRouterEntity.id) @@ -695,6 +712,7 @@ func (ctx *TestContext) RequireAdminClientApiLogin() { func (ctx *TestContext) Teardown() { pfxlog.Logger().Info("tearing down test context") ctx.shutdownRouters() + ctx.shutdownPeerControllers() if ctx.EdgeController != nil { ctx.EdgeController.Shutdown() ctx.EdgeController = nil @@ -789,6 +807,12 @@ func (ctx *TestContext) completeCaAutoEnrollmentWithName(certAuth *certAuthentic } func (ctx *TestContext) completeOttEnrollment(identityId string) *certAuthenticator { + return ctx.completeOttEnrollmentAtApiHost(identityId, ctx.ApiHost) +} + +// completeOttEnrollmentAtApiHost completes an identity's OTT enrollment against the client API +// at the given host, so cluster tests can enroll via a specific controller. +func (ctx *TestContext) completeOttEnrollmentAtApiHost(identityId string, apiHost string) *certAuthenticator { result := ctx.AdminManagementSession.requireQuery(fmt.Sprintf("identities/%v", identityId)) tokenValue := result.Path("data.enrollment.ott.token") @@ -810,11 +834,11 @@ func (ctx *TestContext) completeOttEnrollment(identityId string) *certAuthentica csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}) - resp, err := ctx.newAnonymousClientApiRequest(). - SetBody(csrPem). + resp, err := ctx.NewRestClientWithDefaults().R(). SetHeader("content-type", "application/x-pem-file"). SetHeader("accept", "application/json"). - Post("enroll?token=" + token) + SetBody(csrPem). + Post("https://" + apiHost + EdgeClientApiPath + "/enroll?token=" + token) ctx.Req.NoError(err) ctx.logJson(resp.Body()) ctx.Req.Equal(http.StatusOK, resp.StatusCode()) diff --git a/tests/ha_cluster.go b/tests/ha_cluster.go new file mode 100644 index 000000000..c202f573a --- /dev/null +++ b/tests/ha_cluster.go @@ -0,0 +1,225 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "fmt" + "net/url" + "os" + "strings" + "time" + + hashiraft "github.com/hashicorp/raft" + "github.com/michaelquigley/pfxlog" + edgeApis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/ziti/v2/common/pb/cmd_pb" + "github.com/openziti/ziti/v2/controller" + "github.com/openziti/ziti/v2/controller/config" + "github.com/openziti/ziti/v2/controller/raft" + "github.com/openziti/ziti/v2/controller/server" +) + +// peerController is a secondary cluster-member controller started by StartHaCluster. The +// primary controller remains on the TestContext fields (EdgeController, ApiHost, ...) so all +// existing helpers keep working against it; peers are addressed via their own ApiHost. +type peerController struct { + fabricController *controller.Controller + EdgeController *server.Controller + ApiHost string + config *config.Config +} + +// PeerControllerApiHosts returns the API host:port of each peer controller, in +// PeerCtrlConfigs order. +func (ctx *TestContext) PeerControllerApiHosts() []string { + var result []string + for _, peer := range ctx.peerControllers { + result = append(result, peer.ApiHost) + } + return result +} + +// NewEdgeClientApiForHost returns a ClientHelperClient targeting the given API host instead of +// the primary controller's. The CA pool is shared cluster-wide. +func (ctx *TestContext) NewEdgeClientApiForHost(apiHost string, totpProvider func(chan string)) *ClientHelperClient { + if totpProvider == nil { + totpProvider = func(chan string) {} + } + apiUrl, err := url.Parse("https://" + apiHost + EdgeClientApiPath) + ctx.Req.NoError(err) + + client := edgeApis.NewClientApiClient([]*url.URL{apiUrl}, ctx.ControllerCaPool(), totpProvider) + + return &ClientHelperClient{ + ClientApiClient: client, + testCtx: ctx, + } +} + +// StartHaCluster starts the config set's primary controller plus every PeerCtrlConfigs member, +// joins them into a single raft cluster, and waits until every member is a voter and is +// registered in the Controller store. dataDir is the parent raft data directory shared by the +// set's controllers (e.g. Ha3DataDir); it is removed first so each run bootstraps fresh. +func (ctx *TestContext) StartHaCluster(dataDir string) *ControllerHelper { + err := os.RemoveAll(dataDir) + ctx.Req.NoError(err) + + helper := ctx.startServerWith("testdata/ha-unused.db", true, nil, true) + + raftCtrl, ok := ctx.fabricController.GetCommandDispatcher().(*raft.Controller) + ctx.Req.True(ok, "primary controller is not running in cluster mode") + + for _, peerConfigFile := range ctx.configSet.PeerCtrlConfigs { + ctx.startPeerController(peerConfigFile) + } + + for _, peer := range ctx.peerControllers { + ctx.Req.NotNil(peer.config.Ctrl.Options.AdvertiseAddress, "peer controller config has no ctrl advertise address") + peerCtrlAddr := (*peer.config.Ctrl.Options.AdvertiseAddress).String() + ctx.Req.NoError(ctx.waitForPort(strings.TrimPrefix(peerCtrlAddr, "tls:"), time.Minute)) + + req := &cmd_pb.AddPeerRequest{ + Addr: peerCtrlAddr, + IsVoter: true, + } + pfxlog.Logger().WithField("addr", req.Addr).Info("joining peer controller to test cluster") + + // Adding a voter can bounce leadership while replication to the new member settles, + // so joins are retried until the cluster has a stable leader again. + var joinErr error + for attempt := 0; attempt < 30; attempt++ { + if joinErr = raftCtrl.HandleAddPeer(req); joinErr == nil { + break + } + pfxlog.Logger().WithError(joinErr).WithField("addr", req.Addr).Warn("join attempt failed, retrying") + time.Sleep(time.Second) + } + ctx.Req.NoError(joinErr, "could not join peer controller to test cluster") + } + + ctx.waitForClusterReady(raftCtrl, 1+len(ctx.peerControllers), time.Minute) + + return helper +} + +// startPeerController starts one secondary cluster-member controller. Unlike the primary, no +// default admin is initialized: peers get all replicated state through raft once joined. +func (ctx *TestContext) startPeerController(configFile string) { + log := pfxlog.Logger().WithField("config", configFile) + log.Info("starting peer controller") + + cfg, err := config.LoadConfig(configFile) + ctx.Req.NoError(err) + + fabricController, err := controller.NewController(cfg, NewVersionProviderTest()) + ctx.Req.NoError(err) + + edgeController, err := server.NewController(fabricController) + ctx.Req.NoError(err) + + edgeController.Initialize() + edgeController.Run() + go func() { + ctx.Req.NoError(fabricController.Run()) + }() + + peer := &peerController{ + fabricController: fabricController, + EdgeController: edgeController, + ApiHost: cfg.Edge.Api.Address, + config: cfg, + } + ctx.peerControllers = append(ctx.peerControllers, peer) + + ctx.Req.NoError(ctx.waitForPort(peer.ApiHost, time.Minute)) + log.WithField("apiHost", peer.ApiHost).Info("peer controller started") +} + +// waitForClusterReady polls until the raft configuration lists memberCount voters and the +// Controller store holds memberCount records, so tests observe a fully-registered cluster. +func (ctx *TestContext) waitForClusterReady(raftCtrl *raft.Controller, memberCount int, timeout time.Duration) { + deadline := time.Now().Add(timeout) + var lastState string + + for { + voters := 0 + configFuture := raftCtrl.GetRaft().GetConfiguration() + if err := configFuture.Error(); err == nil { + for _, srv := range configFuture.Configuration().Servers { + if srv.Suffrage == hashiraft.Voter { + voters++ + } + } + } + + registered := 0 + var recordDescs []string + if result, err := ctx.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none"); err == nil { + registered = len(result.Entities) + for _, entity := range result.Entities { + recordDescs = append(recordDescs, fmt.Sprintf("%s(%s)", entity.Id, entity.Name)) + } + } + + var peerCounts []int + for _, peer := range ctx.peerControllers { + count := -1 + if result, err := peer.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none"); err == nil { + count = len(result.Entities) + } + peerCounts = append(peerCounts, count) + } + + lastState = fmt.Sprintf("voters: %v/%v, controller store records: %v/%v %v, peer store counts: %v", voters, memberCount, registered, memberCount, recordDescs, peerCounts) + if voters >= memberCount && registered >= memberCount { + pfxlog.Logger().Info("test cluster ready: " + lastState) + return + } + + if time.Now().After(deadline) { + ctx.Req.Fail("timed out waiting for cluster to become ready", lastState) + return + } + time.Sleep(100 * time.Millisecond) + } +} + +// waitForIdentityOnPeer polls until the peer controller at peerIndex can read the given +// identity, covering raft replication lag after creating state via another cluster member. +func (ctx *TestContext) waitForIdentityOnPeer(peerIndex int, identityId string, timeout time.Duration) error { + peer := ctx.peerControllers[peerIndex] + deadline := time.Now().Add(timeout) + for { + _, err := peer.EdgeController.AppEnv.Managers.Identity.Read(identityId) + if err == nil { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("identity %s did not replicate to peer controller %d within %s: %w", identityId, peerIndex, timeout, err) + } + time.Sleep(25 * time.Millisecond) + } +} + +// shutdownPeerControllers stops every peer controller started by StartHaCluster. +func (ctx *TestContext) shutdownPeerControllers() { + for _, peer := range ctx.peerControllers { + peer.EdgeController.Shutdown() + peer.fabricController.Shutdown() + } + ctx.peerControllers = nil +} diff --git a/tests/ha_cluster_formation_test.go b/tests/ha_cluster_formation_test.go new file mode 100644 index 000000000..241bd0cfa --- /dev/null +++ b/tests/ha_cluster_formation_test.go @@ -0,0 +1,15 @@ +//go:build apitests + +package tests + +import ( + "testing" +) + +// Test_HaClusterFormation verifies the multi-controller test harness forms a full raft +// cluster: every member is a voter and every member is registered in the Controller store. +func Test_HaClusterFormation(t *testing.T) { + ctx := NewTestContextWithConfigSet(t, Ha3) + defer ctx.Teardown() + ctx.StartHaCluster(Ha3DataDir) +} diff --git a/tests/ha_first_party_cert_test.go b/tests/ha_first_party_cert_test.go new file mode 100644 index 000000000..56a1d8b68 --- /dev/null +++ b/tests/ha_first_party_cert_test.go @@ -0,0 +1,245 @@ +//go:build apitests + +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tests + +import ( + "crypto" + "errors" + "slices" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + nfpem "github.com/openziti/foundation/v2/pem" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/ziti/v2/common" + "github.com/openziti/ziti/v2/common/eid" + "github.com/openziti/ziti/v2/controller/oidc_auth" +) + +// extJwtTestSigner holds the signing material and claims values of a registered test +// ext-jwt-signer, so tests can mint bearer tokens the controller will accept. +type extJwtTestSigner struct { + audience string + issuer string + kid string + key crypto.PrivateKey +} + +// newJwtForIdentity mints a bearer token for the given identity, signed by the test signer. +func (s *extJwtTestSigner) newJwtForIdentity(identityId string) (string, error) { + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.RegisteredClaims{ + Audience: []string{s.audience}, + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(2 * time.Hour)}, + ID: uuid.NewString(), + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + Issuer: s.issuer, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + Subject: identityId, + } + jwtToken.Header["kid"] = s.kid + return jwtToken.SignedString(s.key) +} + +// registerExtJwtSignerAllowedByDefaultPolicy creates an enabled ext-jwt-signer and patches the +// default auth policy to allow it as a primary auth method. +func registerExtJwtSignerAllowedByDefaultPolicy(managementHelper *ManagementHelperClient) (*extJwtTestSigner, error) { + signerCert, signerKey := newSelfSignedCert("Test Ext-Jwt Signer") + testSigner := &extJwtTestSigner{ + audience: uuid.NewString(), + issuer: uuid.NewString(), + kid: uuid.NewString(), + key: signerKey, + } + + createSigner := external_jwt_signer.NewCreateExternalJWTSignerParams() + createSigner.ExternalJWTSigner = &rest_model.ExternalJWTSignerCreate{ + CertPem: ToPtr(nfpem.EncodeToString(signerCert)), + Enabled: ToPtr(true), + Name: ToPtr("Test Ext-Jwt Signer"), + Kid: ToPtr(testSigner.kid), + Issuer: ToPtr(testSigner.issuer), + Audience: ToPtr(testSigner.audience), + } + signerCreateResp, err := managementHelper.API.ExternalJWTSigner.CreateExternalJWTSigner(createSigner, nil) + if err != nil { + return nil, rest_util.WrapErr(err) + } + + patchPolicy := auth_policy.NewPatchAuthPolicyParams() + patchPolicy.ID = "default" + patchPolicy.AuthPolicy = &rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{signerCreateResp.Payload.Data.ID}, + }, + }, + } + if _, err = managementHelper.API.AuthPolicy.PatchAuthPolicy(patchPolicy, nil); err != nil { + return nil, rest_util.WrapErr(err) + } + + return testSigner, nil +} + +// dialServiceWithFirstPartyCert authenticates via ext-jwt while presenting the given +// first-party cert at the TLS layer, verifies the session carries no cert fingerprints (so +// router-side acceptance must come from the first-party origin check, not a fingerprint +// match), and dials the service, requiring an edge router connection. +func dialServiceWithFirstPartyCert(ctx *TestContext, testSigner *extJwtTestSigner, identityId string, certAuth *certAuthenticator, serviceName string) error { + jwtStr, err := testSigner.newJwtForIdentity(identityId) + if err != nil { + return err + } + + // An enrolled client trusts the controller's full CA bundle, which includes the edge + // signing CA chain that issues router server certs, not just the ctrl-channel root. + caPool := ctx.ControllerCaPool().Clone() + caPool.AppendCertsFromPEM(ctx.ControllerConfig.Edge.CaPems()) + + baseCreds := edge_apis.NewJwtCredentials(jwtStr) + baseCreds.CaPool = caPool + creds := &extJwtCredsWithFirstPartyCert{ + JwtCredentials: baseCreds, + certs: certAuth.certs, + key: certAuth.key, + } + + clientContext, err := ziti.NewContext(&ziti.Config{ + ZtAPI: "https://" + ctx.ApiHost + EdgeClientApiPath, + Credentials: creds, + }) + if err != nil { + return err + } + defer clientContext.Close() + + if err = clientContext.Authenticate(); err != nil { + return err + } + + ctxImpl, ok := clientContext.(*ziti.ContextImpl) + if !ok { + return errors.New("expected *ziti.ContextImpl from ziti.NewContext") + } + oidcSession, ok := ctxImpl.CtrlClt.GetCurrentApiSession().(*edge_apis.ApiSessionOidc) + if !ok { + return errors.New("expected OIDC api session; SDK fell back to legacy, this test requires OIDC mode") + } + claims := &common.AccessClaims{} + if _, _, err = jwt.NewParser().ParseUnverified(oidcSession.OidcTokens.AccessToken, claims); err != nil { + return err + } + if !slices.Contains(claims.AuthenticationMethodsReferences, oidc_auth.AuthMethodExtJwt) { + return errors.New("expected ext-jwt authentication method reference on the session") + } + if len(claims.CertFingerprints) > 0 { + return errors.New("ext-jwt auth must not bind the first-party cert into the session fingerprints") + } + + connectChan := make(chan struct{}, 1) + clientContext.Events().AddRouterConnectedListener(func(ziti.Context, string, string) { + select { + case connectChan <- struct{}{}: + default: + } + }) + conn, _ := clientContext.Dial(serviceName) + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + select { + case <-connectChan: + return nil + case <-time.After(5 * time.Second): + return errors.New("router connection did not occur within 5 seconds") + } +} + +// Test_FirstPartyCert_SeparateSigningCa runs a three-controller cluster whose edge signing CA +// root is distinct from the ctrl-channel root CA. An identity authenticates via ext-jwt (so no +// cert fingerprints bind to the session) and attaches to an edge router presenting its +// first-party enrollment cert. The router must recognize the cert as first-party via the +// signing CA published in the router data model, for certs issued by the controller the router +// is subscribed to as well as by peer controllers. +func Test_FirstPartyCert_SeparateSigningCa(t *testing.T) { + ctx := NewTestContextWithConfigSet(t, Ha3) + defer ctx.Teardown() + ctx.StartHaCluster(Ha3DataDir) + + managementHelper := ctx.NewEdgeManagementApi(nil) + adminCreds := ctx.NewAdminCredentials() + _, err := managementHelper.Authenticate(adminCreds, nil) + ctx.Req.NoError(rest_util.WrapErr(err)) + ctx.RequireAdminManagementApiLogin() + + testService := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll("smartrouting") + ctx.CreateEnrollAndStartEdgeRouter() + + testSigner, err := registerExtJwtSignerAllowedByDefaultPolicy(managementHelper) + ctx.Req.NoError(err) + + t.Run("cert enrolled via the primary controller is accepted by the edge router", func(t *testing.T) { + ctx.NextTest(t) + + identityId, certAuth := ctx.AdminManagementSession.requireCreateIdentityOttEnrollment(eid.New(), false) + + // The enrollment cert must chain to the signing root, not the ctrl-channel root. + ctx.Req.Equal("Controller One Edge Signing Cert", certAuth.certs[0].Issuer.CommonName) + + ctx.Req.NoError(dialServiceWithFirstPartyCert(ctx, testSigner, identityId, certAuth, testService.Name)) + }) + + t.Run("cert enrolled via a peer controller is accepted by the edge router", func(t *testing.T) { + ctx.NextTest(t) + + identityId := ctx.AdminManagementSession.requireCreateIdentityOttEnrollmentUnfinished(eid.New(), false) + ctx.Req.NoError(ctx.waitForIdentityOnPeer(0, identityId, 5*time.Second)) + certAuth := ctx.completeOttEnrollmentAtApiHost(identityId, ctx.PeerControllerApiHosts()[0]) + + // The peer controller signs with its own intermediate under the shared signing root. + ctx.Req.Equal("Controller Two Edge Signing Cert", certAuth.certs[0].Issuer.CommonName) + + ctx.Req.NoError(dialServiceWithFirstPartyCert(ctx, testSigner, identityId, certAuth, testService.Name)) + }) + + t.Run("controller store records hold full cert chains", func(t *testing.T) { + ctx.NextTest(t) + + result, err := ctx.EdgeController.AppEnv.Managers.Controller.BaseList("true limit none") + ctx.Req.NoError(err) + ctx.Req.Len(result.Entities, 3) + + for _, controllerEntity := range result.Entities { + certs := nfpem.PemStringToCertificates(controllerEntity.CertPem) + ctx.Req.GreaterOrEqual(len(certs), 2, "controller %s certPem should hold the full chain", controllerEntity.Id) + } + }) +} diff --git a/tests/testdata/configs/README.md b/tests/testdata/configs/README.md index b1803d63e..2333aeec4 100644 --- a/tests/testdata/configs/README.md +++ b/tests/testdata/configs/README.md @@ -15,6 +15,38 @@ All paths inside config files are relative to the `tests/` working directory (th standard Go test working directory for this package), so cert and key paths such as `testdata/ca/...` resolve correctly regardless of which config set is active. +## Test PKI + +The raft/HA config sets use a SPIFFE-capable PKI under `testdata/pki/`, generated by +`tests/testdata/create-pki.sh` (and the equivalent `create-pki.ps1`) with `ziti pki`. +The other config sets continue to use the older `testdata/ca` material. + +- A single root CA, `Ziti Test Root CA`, under `pki/root/`. +- Three controller intermediates (`ctrl1`, `ctrl2`, `ctrl3`), each signing its own + server and client certs with a `spiffe://ziti.test/controller/` URI SAN. +- Fabric routers `001` and `002`, signed by the `ctrl1` intermediate, each with a + shared key backing its server and client certs and a `spiffe://ziti.test/router/` + URI SAN. +- A `ctrl1` wildcard alt server cert (`ctrl1-wildcard.chain.pem` / `ctrl1-wildcard.key`) + whose only SAN is `*.wildcard.test`. +- A separate edge signing PKI: a second self-signed root, `Ziti Test Edge Signing Root CA`, + under `pki/signing-root/`, with per-controller signing intermediates (`signing1`, `signing2`, + `signing3`) and a shared bundle (`pki/signing-root/certs/signing-bundle.pem`, root + all three + intermediates). Used by the `ha-3` config set to model networks whose + `edge.enrollment.signingCert` root differs from the ctrl-channel root CA. + +In the identity blocks of configs using this PKI: + +- `cert` / `server_cert` point at the full chain files (`*.chain.pem`, leaf → intermediate + → root) so peers can build the chain from what is presented on the wire. +- `ca` is the root cert only (`testdata/pki/root/certs/root.cert`); the trust anchor is the + root, and intermediates arrive in the presented chains. +- the controller's client and server certs use separate keys, so `key` and `server_key` + are both set. + +To regenerate the PKI, run `create-pki.sh` (or `create-pki.ps1`) from anywhere; it anchors +itself to `tests/testdata/` and writes to `pki/`. + ## Directory Layout ``` @@ -43,6 +75,16 @@ testdata/configs/ the `edge:` section. Used to verify that the auto-binding behaviour is suppressed when the operator opts out, leaving OIDC absent from the running controller. +- **`ha-3`** (`Ha3`) — Three-controller raft cluster whose edge signing CA root + (`pki/signing-root`) is distinct from the ctrl-channel root CA (`pki/root`). Each controller + signs identity certs with its own intermediate under the shared signing root + (`signing1/2/3`), and all three point `edge.enrollment.signingCert.ca` at the shared signing + bundle. Controllers listen on 1281/1282/1283 (web), 6262/6363/6464 (ctrl), 10000/10001/10002 + (mgmt); raft data lives under `testdata/ha-3-data/ctrl{1,2,3}`. Includes an edge router config + listing all three ctrl endpoints. Started with `StartHaCluster`. Used to exercise first-party + client cert validation when the signing CA and ctrl-channel CA differ, including certs issued + by a controller other than the one a router is subscribed to. + ## Adding a New Config Set 1. Create a subdirectory with a short, descriptive name (kebab-case). diff --git a/tests/testdata/configs/ha-3/ctrl1.yml b/tests/testdata/configs/ha-3/ctrl1.yml new file mode 100644 index 000000000..72bc7b173 --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl1.yml @@ -0,0 +1,60 @@ +v: 3 + +# Controller 1 of the ha-3 config set: a three-controller raft cluster whose edge signing CA +# root (pki/signing-root) is distinct from the ctrl-channel root CA (pki/root). Each controller +# signs identity certs with its own intermediate under the shared signing root. The dataDir is +# cleaned by StartHaCluster before each run. +cluster: + dataDir: testdata/ha-3-data/ctrl1 + +identity: + cert: testdata/pki/ctrl1/certs/client.chain.pem + server_cert: testdata/pki/ctrl1/certs/server.chain.pem + key: testdata/pki/ctrl1/keys/client.key + server_key: testdata/pki/ctrl1/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6262 + options: + advertiseAddress: tls:127.0.0.1:6262 + +mgmt: + listener: tls:127.0.0.1:10000 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1281 + enrollment: + signingCert: + cert: testdata/pki/signing1/certs/signing1.chain.pem + key: testdata/pki/signing1/keys/signing1.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1281 + address: 127.0.0.1:1281 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/ctrl2.yml b/tests/testdata/configs/ha-3/ctrl2.yml new file mode 100644 index 000000000..ea4bd03fd --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl2.yml @@ -0,0 +1,57 @@ +v: 3 + +# Controller 2 of the ha-3 config set. See ctrl1.yml for the set's description. +cluster: + dataDir: testdata/ha-3-data/ctrl2 + +identity: + cert: testdata/pki/ctrl2/certs/client.chain.pem + server_cert: testdata/pki/ctrl2/certs/server.chain.pem + key: testdata/pki/ctrl2/keys/client.key + server_key: testdata/pki/ctrl2/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6363 + options: + advertiseAddress: tls:127.0.0.1:6363 + +mgmt: + listener: tls:127.0.0.1:10001 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1282 + enrollment: + signingCert: + cert: testdata/pki/signing2/certs/signing2.chain.pem + key: testdata/pki/signing2/keys/signing2.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1282 + address: 127.0.0.1:1282 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/ctrl3.yml b/tests/testdata/configs/ha-3/ctrl3.yml new file mode 100644 index 000000000..81a5c4f88 --- /dev/null +++ b/tests/testdata/configs/ha-3/ctrl3.yml @@ -0,0 +1,57 @@ +v: 3 + +# Controller 3 of the ha-3 config set. See ctrl1.yml for the set's description. +cluster: + dataDir: testdata/ha-3-data/ctrl3 + +identity: + cert: testdata/pki/ctrl3/certs/client.chain.pem + server_cert: testdata/pki/ctrl3/certs/server.chain.pem + key: testdata/pki/ctrl3/keys/client.key + server_key: testdata/pki/ctrl3/keys/server.key + ca: testdata/pki/root/certs/root.cert + +trustDomain: ziti.test + +ctrl: + listener: tls:127.0.0.1:6464 + options: + advertiseAddress: tls:127.0.0.1:6464 + +mgmt: + listener: tls:127.0.0.1:10002 + +terminator: + validators: + edge: edge + +edge: + api: + sessionTimeout: 30m + address: 127.0.0.1:1283 + enrollment: + signingCert: + cert: testdata/pki/signing3/certs/signing3.chain.pem + key: testdata/pki/signing3/keys/signing3.key + ca: testdata/pki/signing-root/certs/signing-bundle.pem + edgeIdentity: + duration: 20m + edgeRouter: + duration: 60m + +web: + - name: client-management-localhost + bindPoints: + - interface: 127.0.0.1:1283 + address: 127.0.0.1:1283 + options: {} + apis: + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc + options: + redirectURIs: + - "http://localhost:*/auth/callback" + - "http://127.0.0.1:*/auth/callback" diff --git a/tests/testdata/configs/ha-3/edge-router.yml b/tests/testdata/configs/ha-3/edge-router.yml new file mode 100644 index 000000000..4d7f30782 --- /dev/null +++ b/tests/testdata/configs/ha-3/edge-router.yml @@ -0,0 +1,40 @@ +v: 3 + +# Edge router for the ha-3 config set. The identity files are written by enrollment at test +# runtime; the names are distinct from the default-ats router so the sets don't clobber each +# other. All three controller ctrl endpoints are listed. +identity: + cert: testdata/edge-router/ha-3-edge-router-client.cert.pem + server_cert: testdata/edge-router/ha-3-edge-router-server.cert.pem + key: testdata/edge-router/ha-3-edge-router.key.pem + ca: testdata/edge-router/ha-3-edge-router-ca-chain.cert.pem + +ctrl: + endpoints: + - tls:127.0.0.1:6262 + - tls:127.0.0.1:6363 + - tls:127.0.0.1:6464 + +edge: + heartbeatIntervalSeconds: 10 + csr: + country: US + province: NC + locality: Charlotte + organization: NetFoundry + organizationalUnit: Ziti + sans: + dns: + - "localhost" + ip: + - "127.0.0.1" + +dialers: + - binding: udp + - binding: transport + +listeners: + - binding: edge + address: tls:0.0.0.0:3022 + options: + advertise: 127.0.0.1:3022 diff --git a/tests/testdata/create-pki.ps1 b/tests/testdata/create-pki.ps1 new file mode 100644 index 000000000..c587ca0be --- /dev/null +++ b/tests/testdata/create-pki.ps1 @@ -0,0 +1,94 @@ +# Builds a fresh, HA/SPIFFE-capable test PKI under testdata/pki, modeled on doc/ha/create-pki.sh. +# Separate from the existing testdata/ca PKI, which is left untouched. Can be run from any +# directory; it anchors itself to the script's own location. + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +# Anchor to the script's own directory (tests/testdata) so paths resolve regardless of cwd. +Set-Location -LiteralPath $PSScriptRoot + +$PkiRoot = 'pki' +$TrustDomain = 'ziti.test' +$RootNotBefore = (Get-Date).ToUniversalTime().AddHours(-24).ToString('yyyy-MM-ddTHH:mm:ss') + 'Z' +$IntermediateNotBefore = (Get-Date).ToUniversalTime().AddHours(-12).ToString('yyyy-MM-ddTHH:mm:ss') + 'Z' + +# Self-signed root CA. --trust-domain makes every cert issued under it SPIFFE-capable. +ziti pki create ca --pki-root $PkiRoot --trust-domain $TrustDomain --ca-file root --ca-name 'Ziti Test Root CA' --not-before $RootNotBefore + +# Controller 1: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl1 --intermediate-name 'Controller One Signing Cert' --not-before $IntermediateNotBefore + +# Controller 1: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --dns localhost --ip 127.0.0.1 --server-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --client-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: wildcard alt server cert (only SAN is *.wildcard.test). Used by the +# wildcard-oidc-server config set / Test_OidcDiscoveryEndpoints_WildcardIssuer to exercise +# OIDC issuer derivation from a wildcard server-cert SAN. +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --server-file ctrl1-wildcard --server-name ctrl1-wildcard --dns '*.wildcard.test' + +# Controller 2: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl2 --intermediate-name 'Controller Two Signing Cert' --not-before $IntermediateNotBefore + +# Controller 2: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl2 --dns localhost --ip 127.0.0.1 --server-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 2: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl2 --client-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 3: intermediate (signing) cert +ziti pki create intermediate --pki-root $PkiRoot --ca-name root --intermediate-file ctrl3 --intermediate-name 'Controller Three Signing Cert' --not-before $IntermediateNotBefore + +# Controller 3: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl3 --dns localhost --ip 127.0.0.1 --server-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Controller 3: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl3 --client-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Routers are signed by controller 1's intermediate. Each router uses a single key +# shared by its server cert (link/edge listeners) and client cert (ctrl channel). + +# Router 001: key +ziti pki create key --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 + +# Router 001: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 --server-file 001-server --server-name 001 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/001' + +# Router 001: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --key-file 001 --client-file 001-client --client-name 001 --spiffe-id 'router/001' + +# Router 002: key +ziti pki create key --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 + +# Router 002: server cert +ziti pki create server --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 --server-file 002-server --server-name 002 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/002' + +# Router 002: client cert +ziti pki create client --pki-root $PkiRoot --ca-name ctrl1 --key-file 002 --client-file 002-client --client-name 002 --spiffe-id 'router/002' + +# Separate edge signing PKI: a self-signed root distinct from the ctrl-channel root, with +# per-controller signing intermediates. Used by the ha-3 config set to exercise networks whose +# edge.enrollment.signingCert root differs from the ctrl-channel root CA. +ziti pki create ca --pki-root $PkiRoot --trust-domain $TrustDomain --ca-file signing-root --ca-name 'Ziti Test Edge Signing Root CA' --not-before $RootNotBefore + +# Controller 1: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing1 --intermediate-name 'Controller One Edge Signing Cert' --not-before $IntermediateNotBefore + +# Controller 2: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing2 --intermediate-name 'Controller Two Edge Signing Cert' --not-before $IntermediateNotBefore + +# Controller 3: edge signing intermediate +ziti pki create intermediate --pki-root $PkiRoot --ca-name signing-root --intermediate-file signing3 --intermediate-name 'Controller Three Edge Signing Cert' --not-before $IntermediateNotBefore + +# Shared edge signing CA bundle: the signing root plus every per-controller signing +# intermediate. Each controller's edge.enrollment.signingCert.ca points here, so the +# controllers publish the signing root as a trust anchor with the intermediates attached. +Get-Content ` + "$PkiRoot/signing-root/certs/signing-root.cert", ` + "$PkiRoot/signing1/certs/signing1.cert", ` + "$PkiRoot/signing2/certs/signing2.cert", ` + "$PkiRoot/signing3/certs/signing3.cert" | + Set-Content "$PkiRoot/signing-root/certs/signing-bundle.pem" diff --git a/tests/testdata/create-pki.sh b/tests/testdata/create-pki.sh new file mode 100644 index 000000000..a50f98236 --- /dev/null +++ b/tests/testdata/create-pki.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# Builds a fresh, HA/SPIFFE-capable test PKI under testdata/pki, modeled on doc/ha/create-pki.sh. +# Separate from the existing testdata/ca PKI, which is left untouched. Can be run from any +# directory; it anchors itself to the script's own location. + +set -euo pipefail + +# Anchor to the script's own directory (tests/testdata) so paths resolve regardless of cwd. +cd "$(dirname "${BASH_SOURCE[0]}")" + +PKI_ROOT="pki" +TRUST_DOMAIN="ziti.test" +# Epoch math (-d @SECONDS) so this works with both GNU date and BusyBox date. +NOW="$(date -u +%s)" +ROOT_NOT_BEFORE="$(date -u -d "@$((NOW - 86400))" +%Y-%m-%dT%H:%M:%SZ)" # 24h before now +INTERMEDIATE_NOT_BEFORE="$(date -u -d "@$((NOW - 43200))" +%Y-%m-%dT%H:%M:%SZ)" # 12h before now + +# Self-signed root CA. --trust-domain makes every cert issued under it SPIFFE-capable. +ziti pki create ca \ + --pki-root "$PKI_ROOT" \ + --trust-domain "$TRUST_DOMAIN" \ + --ca-file root \ + --ca-name 'Ziti Test Root CA' \ + --not-before "$ROOT_NOT_BEFORE" + +# Controller 1: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl1 --intermediate-name 'Controller One Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 1: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --dns localhost --ip 127.0.0.1 --server-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --client-name ctrl1 --spiffe-id 'controller/ctrl1' + +# Controller 1: wildcard alt server cert (only SAN is *.wildcard.test). Used by the +# wildcard-oidc-server config set / Test_OidcDiscoveryEndpoints_WildcardIssuer to exercise +# OIDC issuer derivation from a wildcard server-cert SAN. +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --server-file ctrl1-wildcard --server-name ctrl1-wildcard --dns '*.wildcard.test' + +# Controller 2: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl2 --intermediate-name 'Controller Two Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 2: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl2 --dns localhost --ip 127.0.0.1 --server-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 2: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl2 --client-name ctrl2 --spiffe-id 'controller/ctrl2' + +# Controller 3: intermediate (signing) cert +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name root --intermediate-file ctrl3 --intermediate-name 'Controller Three Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 3: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl3 --dns localhost --ip 127.0.0.1 --server-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Controller 3: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl3 --client-name ctrl3 --spiffe-id 'controller/ctrl3' + +# Routers are signed by controller 1's intermediate. Each router uses a single key +# shared by its server cert (link/edge listeners) and client cert (ctrl channel). + +# Router 001: key +ziti pki create key --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 + +# Router 001: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 --server-file 001-server --server-name 001 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/001' + +# Router 001: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 001 --client-file 001-client --client-name 001 --spiffe-id 'router/001' + +# Router 002: key +ziti pki create key --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 + +# Router 002: server cert +ziti pki create server --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 --server-file 002-server --server-name 002 --dns localhost --ip 127.0.0.1 --spiffe-id 'router/002' + +# Router 002: client cert +ziti pki create client --pki-root "$PKI_ROOT" --ca-name ctrl1 --key-file 002 --client-file 002-client --client-name 002 --spiffe-id 'router/002' + +# Separate edge signing PKI: a self-signed root distinct from the ctrl-channel root, with +# per-controller signing intermediates. Used by the ha-3 config set to exercise networks whose +# edge.enrollment.signingCert root differs from the ctrl-channel root CA. +ziti pki create ca \ + --pki-root "$PKI_ROOT" \ + --trust-domain "$TRUST_DOMAIN" \ + --ca-file signing-root \ + --ca-name 'Ziti Test Edge Signing Root CA' \ + --not-before "$ROOT_NOT_BEFORE" + +# Controller 1: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing1 --intermediate-name 'Controller One Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 2: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing2 --intermediate-name 'Controller Two Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Controller 3: edge signing intermediate +ziti pki create intermediate --pki-root "$PKI_ROOT" --ca-name signing-root --intermediate-file signing3 --intermediate-name 'Controller Three Edge Signing Cert' --not-before "$INTERMEDIATE_NOT_BEFORE" + +# Shared edge signing CA bundle: the signing root plus every per-controller signing +# intermediate. Each controller's edge.enrollment.signingCert.ca points here, so the +# controllers publish the signing root as a trust anchor with the intermediates attached. +cat "$PKI_ROOT/signing-root/certs/signing-root.cert" \ + "$PKI_ROOT/signing1/certs/signing1.cert" \ + "$PKI_ROOT/signing2/certs/signing2.cert" \ + "$PKI_ROOT/signing3/certs/signing3.cert" \ + > "$PKI_ROOT/signing-root/certs/signing-bundle.pem" diff --git a/tests/testdata/pki/ctrl1/certs/001-client.cert b/tests/testdata/pki/ctrl1/certs/001-client.cert new file mode 100644 index 000000000..8d9a12d70 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ZI+UH00lzkWUe9rGcWXA +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-client.chain.pem b/tests/testdata/pki/ctrl1/certs/001-client.chain.pem new file mode 100644 index 000000000..67920b991 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +ZI+UH00lzkWUe9rGcWXA +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-server.cert b/tests/testdata/pki/ctrl1/certs/001-server.cert new file mode 100644 index 000000000..9031ed53c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/001-server.chain.pem b/tests/testdata/pki/ctrl1/certs/001-server.chain.pem new file mode 100644 index 000000000..469d87713 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/001-server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-client.cert b/tests/testdata/pki/ctrl1/certs/002-client.cert new file mode 100644 index 000000000..58a737326 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +lCamByWqrRsC9gOONRg= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-client.chain.pem b/tests/testdata/pki/ctrl1/certs/002-client.chain.pem new file mode 100644 index 000000000..c6af2c6f6 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIQIy0Ji9MAGj1Be+t+PHuF6jANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIE9u +ZSBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjA0WhcNMjcwNjA4MTk0NzA0WjBW +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQwwCgYDVQQDEwMwMDIwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDI492ZM6/QBOJ+sWIrCl7ws9UvsK2cN59H +VUWfXqp8/mr7y8fW0CqOdYcJHuKhr5W0ZVquZV8HkOLqL7rBjlW//3pwqOmjgCX0 +H0DLWyuBr/Y6IIP0D8qa4Hj5RtPvWa9G8U5+d2z19qZNgRvZL79fHiKFlMW0ParI +480mCeR8u+QNx+vNwLkWPjtN2TucaNdArlrpSLlpmW+E6SvBaql1W9g7F+g3mHC1 +TUsPmyrGkE0TTWNyslsNb2VwJTOOkE65YW5ltTZygcajh1j8ZfFqn/hMy+B90WNe +FAN3xzLSxQRiOnD4MptcbQKNTvWgxBS5OagaKIWWi7FWsGsmMSztTIQ4U8K6oSAn +mhNgEcDcvSPvj4Zq8o/Wx2q2qqTo8W83FEazoEuohg5fubQZrhDPkrOr3fEmm54J +GjE670UdVMUSY7bnWtYGf6Mn8B5zdDDSWWvH/PmnT/KPKHdo24obRujZfb0kYEzc +D7GBhJPeut3NEZpIHGrdXuueFtiYOYb77V/ODC+jB7gFObEj6YyQDiBsihqeMmOC +vg0IGeS3T7E5O+cqvnPei9rfGsHEw64XOyLvhjgE3AbEvZkAAX0sqnmtcNXo81yD +AidnhHE3QGiUdgTi4+W8+Ns3iWXzjulllc/RFefL39OUR5oeuhxjp0fP0QPhILJX +iMgvaopFIwIDAQABo4GLMIGIMA4GA1UdDwEB/wQEAwIF4DAMBgNVHRMBAf8EAjAA +MB0GA1UdDgQWBBRGYVW8EyxowoKbjDVCZcg0C0Zd9DAfBgNVHSMEGDAWgBSwB8V3 +vlY1dTSfKgSjFTSrWH69SDAoBgNVHREEITAfhh1zcGlmZmU6Ly96aXRpLnRlc3Qv +cm91dGVyLzAwMjANBgkqhkiG9w0BAQsFAAOCAgEAE4/sHy4seUgwB8XmAPHTPB4m +tXRs1/+X26Gza1hswUvm/onGEg4NNsMt/V1n54LYSDN45YnT7K672MDPfi03j3wp +A6FtX5lp6ZOfrM49eA8omwQxGSHeDeXO5UDu14XMoEwicV2Lz8cARi9jq6RbJzmN +n5gIdzfGXpyQbg1Rv6bgV9qZ2pA8nE9urHXzeDvQYFukmFdwqWtd3KR66eJ+qB+p +a+BTB6m6LxyktX2dfS8jLt8y9u2ElDy2yAJ36AV9gNkPsrd6TGCLq3iSMkRIkY/j +nxf8vvZxJj8QmYFiAhPPciO7eh7t8u8jdf1KxCPo2XNQyua9oPM3c3xmdhjrcDsg +VHDDT6zWJcl1VRO8sI/CkQfJtGDQzjJsuTYT//ttBtUPdWPIZf6+jwhCCOVJI3os +XuqnP6sa+VuNHnaoc6/boyG04quPaRLs5143//a2GwOyX0A9f02ZjOXJCBk8x3bU +VqskPjfzKC73GloyoJFKuzSawDvHLc9oOp7TT8CzpEgOd9i33+4tgSTAQ0nyumIX +Lsu4LgjrF4NcWEVq1aJEBsGYdmFIiR5xvLucjAZxA0oPFojEWEK4tfjZD0OkriMS +ASoFlZVRKxkUt9+jCVZHEK7gZHttUltrNbv4ehTdZbxjWByi2WHyxQ63HNN7SoA+ +lCamByWqrRsC9gOONRg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-server.cert b/tests/testdata/pki/ctrl1/certs/002-server.cert new file mode 100644 index 000000000..3c6d47f1c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/002-server.chain.pem b/tests/testdata/pki/ctrl1/certs/002-server.chain.pem new file mode 100644 index 000000000..f8c8d2d49 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/002-server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/client.cert b/tests/testdata/pki/ctrl1/certs/client.cert new file mode 100644 index 000000000..d5e9e5a8e --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +6WGKTA7mi2TXOGLv7velocFTYMO8fQ== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/client.chain.pem b/tests/testdata/pki/ctrl1/certs/client.chain.pem new file mode 100644 index 000000000..7c6b1abaa --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +6WGKTA7mi2TXOGLv7velocFTYMO8fQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert new file mode 100644 index 000000000..abc5e70e2 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF1TCCA72gAwIBAgIQDMbvu6na8/uUMbRL+PYXajANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIE9u +ZSBTaWduaW5nIENlcnQwHhcNMjYwNjE1MTczNDM4WhcNMjcwNjE1MTczNTM4WjBh +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRcwFQYDVQQDEw5jdHJsMS13aWxkY2Fy +ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALdz2IJ9kZV42nBIIyKN +xTNnVYSNj3Y6NgP4dKiw9MvpeomAJ46Z5SBMcjP65huKxUS7+i6BGROdzaFcVzt7 +ZHVhLCs5K7/XCDPA18klbciIU8zfZKbS2FD5J5gA6xZWey8apButysnFpXlqquhX +dZPCNRex9Dllnc/hYBMvbLW5j1OTaP9HESvSAiWCx7eEnrAFz8TzCmBboa1UyhaA +1jeas22Ua6MFxsYUKXlxtR1bVo1SEBnHFMpo+ERqwIBRoZHdF0mKf0U4YzGRBGpo +ZcaUbAVhnToRvyj8r5129Tm01ALFRQqlXp6N1Eh1r/1APOjHkPi4m+iaQoH8yhp5 +DHD+hhJXkG/+kBiWT38G8AOXU9V1Ckak4iFeqInxrCULQqkjbW9squXdnFTA/ycK +md/kR6bTqdehSbBc0O45KFzBxijRNmhlO2NaoQPzXEhpM0z+/Gop9RDQfdNits76 +GNMk1GnI+k5Ra3gxvIAbmPZEal4kdWicFDU9+Jgpc0EgbqeWHbbwjKMuVloTQ0BK +4oByn5iy+wChEJhclEk0dSVRnMQKuyHJAIn4EsL5q/lhr85Kgd9dmcgGLoNHvSBW +spPQXwE87/Uji+60bYlxHV0RLpkZAx8aSoku3xutRv6NPfTSS37AvhxpTlp2SjmZ +KDlCcCZ2PSX8u10I/Xor+287AgMBAAGjfDB6MA4GA1UdDwEB/wQEAwIF4DAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBQ4DxuHc8kkS8lQcVPj/o/HkmcaBTAfBgNVHSME +GDAWgBSwB8V3vlY1dTSfKgSjFTSrWH69SDAaBgNVHREEEzARgg8qLndpbGRjYXJk +LnRlc3QwDQYJKoZIhvcNAQELBQADggIBAIGZ5BFULiJDd/GTZ0LtLCvyip2zk4pm +wXMKcXcNm73SZTJg1pYKTf1jePwpPKHXyZXNRlWgcy9SAhpvjTMKnJgg8GQAsc4i +5MfPft8ec+r2IguZHmp43qtZDdo/vZxSEaS/ANWlr/KlsCV83dIeBRrKOqx/gkpD +b4vo92QgZJz+A1v3RwsV4FJnvs0nKIBZuDtY9Tkhg3PbZKDiHqK8VgFXvXas6+82 +LDRbAePPi4sd/0uTAkwkuFe01YUqz/fTUWXW/9w8D8ncqkGFX1gx7j8IayrEVlTq +itBBSivzeymZxWSB6k+CyTT4YTIS0zyFC+IYFC6b+1O/io6iJEo4EELsUIho0BEo +QZaMQuYFEeGVastirGrJWdxP0QxhPe8A+Bjf5pgIsbDhn2VjvHGls/mrm847vH/Y +dtT6amek4rEjXfymHTTyPFDnLz+LWYwLOe2cS+cTco61GtlCMLiXS+l8AvbU3rsl +0Sqea+Zla+tHdzEmMHkeU9ghjtuHfOzSlB4FMRMHMDqaoQ8yOkDJ4ESXkbBBNhEx +PkbZrTAB+kh1LTsrsiDIX2r1IOgRUz151NCMKnh7XojgGrewt7Dj61337bD4itqf +8E4Cp6D3dd0zCMYudm1yCpaQH8q9bHNBtc62H6Z8UsUJp0QARuZRQZAfj5qq7d0P +4PEK1Z+dLLpB +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem new file mode 100644 index 000000000..3c9f22c5b --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1-wildcard.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF1TCCA72gAwIBAgIQDMbvu6na8/uUMbRL+PYXajANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIE9u +ZSBTaWduaW5nIENlcnQwHhcNMjYwNjE1MTczNDM4WhcNMjcwNjE1MTczNTM4WjBh +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRcwFQYDVQQDEw5jdHJsMS13aWxkY2Fy +ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALdz2IJ9kZV42nBIIyKN +xTNnVYSNj3Y6NgP4dKiw9MvpeomAJ46Z5SBMcjP65huKxUS7+i6BGROdzaFcVzt7 +ZHVhLCs5K7/XCDPA18klbciIU8zfZKbS2FD5J5gA6xZWey8apButysnFpXlqquhX +dZPCNRex9Dllnc/hYBMvbLW5j1OTaP9HESvSAiWCx7eEnrAFz8TzCmBboa1UyhaA +1jeas22Ua6MFxsYUKXlxtR1bVo1SEBnHFMpo+ERqwIBRoZHdF0mKf0U4YzGRBGpo +ZcaUbAVhnToRvyj8r5129Tm01ALFRQqlXp6N1Eh1r/1APOjHkPi4m+iaQoH8yhp5 +DHD+hhJXkG/+kBiWT38G8AOXU9V1Ckak4iFeqInxrCULQqkjbW9squXdnFTA/ycK +md/kR6bTqdehSbBc0O45KFzBxijRNmhlO2NaoQPzXEhpM0z+/Gop9RDQfdNits76 +GNMk1GnI+k5Ra3gxvIAbmPZEal4kdWicFDU9+Jgpc0EgbqeWHbbwjKMuVloTQ0BK +4oByn5iy+wChEJhclEk0dSVRnMQKuyHJAIn4EsL5q/lhr85Kgd9dmcgGLoNHvSBW +spPQXwE87/Uji+60bYlxHV0RLpkZAx8aSoku3xutRv6NPfTSS37AvhxpTlp2SjmZ +KDlCcCZ2PSX8u10I/Xor+287AgMBAAGjfDB6MA4GA1UdDwEB/wQEAwIF4DAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBQ4DxuHc8kkS8lQcVPj/o/HkmcaBTAfBgNVHSME +GDAWgBSwB8V3vlY1dTSfKgSjFTSrWH69SDAaBgNVHREEEzARgg8qLndpbGRjYXJk +LnRlc3QwDQYJKoZIhvcNAQELBQADggIBAIGZ5BFULiJDd/GTZ0LtLCvyip2zk4pm +wXMKcXcNm73SZTJg1pYKTf1jePwpPKHXyZXNRlWgcy9SAhpvjTMKnJgg8GQAsc4i +5MfPft8ec+r2IguZHmp43qtZDdo/vZxSEaS/ANWlr/KlsCV83dIeBRrKOqx/gkpD +b4vo92QgZJz+A1v3RwsV4FJnvs0nKIBZuDtY9Tkhg3PbZKDiHqK8VgFXvXas6+82 +LDRbAePPi4sd/0uTAkwkuFe01YUqz/fTUWXW/9w8D8ncqkGFX1gx7j8IayrEVlTq +itBBSivzeymZxWSB6k+CyTT4YTIS0zyFC+IYFC6b+1O/io6iJEo4EELsUIho0BEo +QZaMQuYFEeGVastirGrJWdxP0QxhPe8A+Bjf5pgIsbDhn2VjvHGls/mrm847vH/Y +dtT6amek4rEjXfymHTTyPFDnLz+LWYwLOe2cS+cTco61GtlCMLiXS+l8AvbU3rsl +0Sqea+Zla+tHdzEmMHkeU9ghjtuHfOzSlB4FMRMHMDqaoQ8yOkDJ4ESXkbBBNhEx +PkbZrTAB+kh1LTsrsiDIX2r1IOgRUz151NCMKnh7XojgGrewt7Dj61337bD4itqf +8E4Cp6D3dd0zCMYudm1yCpaQH8q9bHNBtc62H6Z8UsUJp0QARuZRQZAfj5qq7d0P +4PEK1Z+dLLpB +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1.cert b/tests/testdata/pki/ctrl1/certs/ctrl1.cert new file mode 100644 index 000000000..123517c0c --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem b/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem new file mode 100644 index 000000000..84cbb1219 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/ctrl1.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/server.cert b/tests/testdata/pki/ctrl1/certs/server.cert new file mode 100644 index 000000000..329042233 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/certs/server.chain.pem b/tests/testdata/pki/ctrl1/certs/server.chain.pem new file mode 100644 index 000000000..e7dcffdb4 --- /dev/null +++ b/tests/testdata/pki/ctrl1/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl1/crlnumber b/tests/testdata/pki/ctrl1/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl1/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl1/index.txt b/tests/testdata/pki/ctrl1/index.txt new file mode 100644 index 000000000..96a030828 --- /dev/null +++ b/tests/testdata/pki/ctrl1/index.txt @@ -0,0 +1,7 @@ +V 270608194659Z 42BAB1AD44A3E68A09FAFA96D5053C55 server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1 +V 270608194700Z 10F986EB4390F63470E06103115D027E client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1 +V 270608194703Z A0489D9392A153CF99FDFE979C440300 001-server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=001 +V 270608194703Z F4C38A7DC40F7CA063E8898C02D1EFBD 001-client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=001 +V 270608194704Z C4B86735AA971B44B232966933AC0082 002-server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=002 +V 270608194704Z 232D098BD3001A3D417BEB7E3C7B85EA 002-client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=002 +V 270615173538Z 0CC6EFBBA9DAF3FB9431B44BF8F6176A ctrl1-wildcard.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl1-wildcard diff --git a/tests/testdata/pki/ctrl1/index.txt.attr b/tests/testdata/pki/ctrl1/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl1/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl1/keys/001-client.key b/tests/testdata/pki/ctrl1/keys/001-client.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/001-server.key b/tests/testdata/pki/ctrl1/keys/001-server.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001-server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/001.key b/tests/testdata/pki/ctrl1/keys/001.key new file mode 100644 index 000000000..edd50e251 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/001.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDGAnn10g6qJbjd +v7WTMLe9PBUopw0O0IKd/s8KuB7lJXinYS4/OKWa5vuMYHCvdUTs43ITMYT1SAAA +3NHhxbG7sohtTBdilKB6KTiBJyal1WT8r0SkVtQDupL8f9qneeGBFWSxhOZ8Gh7V +iu4P6ZvMUazUriG0oIt/wdNvrR8vwjqYuHSYtSH+dJl+G6AWq2oDlLXujp9rYmBe +vstyEm51Xaqbs9I3g38XWtmycb1ppWpvTT/Q0YZrADm1j8KPdZj177nbieVyGixW +bD1mpFNRdobSq8CF5zL42EAj0HuF5UyI362kJavp1Q3cNCcZxK2WuvpM+sqLDTRj +wqb0wvlC0PXXJatHi7DTXycREIKfW3JZrS9e0xeliZxZ3N9eN6K1SRKNdFs5uIPT +lB2aYUUGper+14x6Zs3sP28iCf/soV+1ciuc5c2eKFvJtoOs13H0C5QEHF3eBQFG +xhfvyYJ9gWKHQd8ZqzcMZKGlx8AJ5f22BJTDUYx9bsdC7okEHGvxiBZZI00S/DhJ +0uZ/sq2MgLs+Nep9u95XLu2IENzJGHf3TPC1YLeZ6Rm6Dz7C9ODMk0i2DubXlxTT +nnxqizqPSYx6wsY5mhBkyvbKwJeeECFycXHg5udSDRMPAjD3N38HLMQC5HpPd+Py +EqLwDZHnvA7TfEJNnS0r6Nr+9oA8KwIDAQABAoICABcTU847gB0BD2W8QTdZjpgd +h+O4cBOhZg9ukgdQRL10m/Z5o0+X1OjT8U2/MXKpKnKm4PTuqO2CyglPsp/qj5dt +q2t9Wh5RbtA9vImEnALodv/hlxDF06hpllaZucwZrpeB/D2Z3NhqnNxK+ApfsZni +jM8uSdxId2lYl8DkfoVsz7JbkK75MPB7+yVQp9vsdwraykb2VQrJKKzx9DijgD7q +PwctMhrpFqcMneEnUykcL2awLHcz68PNf2wyyDw6uqwyl0jH7bbZ6P/bVl5/WPxd +4UFs5Z9VPcH1RahG1BZbDgvHcW93Om0iOJ7IbyQYIRi3SpMtCOyiic/0b8uHa0/T +FLkclI4zQdb7sSMs+URHbF1tI1rsJLjQweELGcVgTxRUZ6oeX3gKSZbsXpT9WYWL +eTGjOVRCaBaJR7kdL6URHLp+YGqjHhyK0GCczJQFTWRw4vSUVmrfSw6x5ArM5fcn +QqYrVKDcKqBWMS99EDr97Vk4pLEXcZBkiSb7JCkvTaOxwAzMDfcjRUbNj21olT+q +hPlr40/AXff3tlBeZEF2y8VbNclXYhWNbCep6qf25fwO+5nQwmbi7JBkNUEnGSPh +TTtEpLYTZM+YlomtGkzm+M7p85o5YWQyTGpx47oi5UYuSyM9I1HQgA71JgHKUXkP +aekzF9LW8FGniKKKeBytAoIBAQDaMr7xH3UmjvR4wRtoux8w7qZsrzQLRZQ+UO/a +AgBHGIzKM37H28D5ywQ5OZEIfgHVDTLXdMTqIfdPnnS2wS3smISgA/2pVQSWrjdL +yQ7J6ZEKlloQj5HLHCdH8noi+yGVV8hgN0D5aieSDER8Jklpx/gqVIU2w8yJpfqf +qUaJHz8A+yAtjSB9o/9nUy7qG+ANH98Zu5UzM/avLjjwI4ttFHvBoy3DghZ0FEj2 +JMSG1eiwtbMmghMGTAxtGd5CeMUCNo7bhmyAkF/twOYNIYbLLtUNQBOI/6NKDJE4 +IrhBN/Rd+DEiLXg2sapjZVayefix8ghoLqmf+ShUu7JxsYuNAoIBAQDoUFppWXeZ +TZI5gRBiB0/gNxTKp+KH4OrzBCjmzIZG3m/GHaLb+XMWp3lDaSCx51FW6rs+Y3H9 +K9U/yEi8/QGNA85ShKLjfK8On0MJ1iApAR0GI7WRF/LGLHOTktvNFU8+jiAAwErE +FdjyPcXGwhcGh9uVgbsCCrY4n7cCzFsQlQOp7JiizHXHnqD4pgDbhMb9UHV9D8PB ++bAiOGjuMJbpX7Ca5qzufqMQrPuUI27zYdKbcSW4xdmnnL2UpWOhfP5V2HVNGMtv +2S3tlHmAzGKLIQrOC+L0GVaVjaXZzcE+3ZqbZrEnFtxBSNKlicGAH3rEOgjex/DJ +5bX8yrYu8ZyXAoIBAQDHsmHmtG+3lmZQjgvT0k49rm1KUx9Coa+HSPP7hpFSgDl6 +ZZ7Gs2zuMMvww6rlJs3ue2HopsoBrox1pgQTrsGlJAFdoclqTJEihte82IpcaROB +qRFMztkUFvQseNMnaMfXsruEs40Xt/v3Qpr7NN9DnOyXcTtqX9C2ud2ien4/yQUU +lMFibyMpWgrKOetZ/6ES4fSBzJhEgG5o7djWjvFwJ6sMEZg0sr5yBo6wF/51mRiZ +gfIwcCORQ9CrmV09YOKNn/knGOP81iW1pzHlGqYPFxGOSvKWE8ESaGYQBlaz+c4A +osH1EcBP7lKPHZQ3TEI8OdNP5kUAUDyFMDOksmWRAoIBAC95pFBdMTzKBnTr9D4k +QmWOvBgCISAljb4l/nnUGCjJckwpaEvN+YAKr5RgGodgv1Wk2KkSR4w2dfj35C2D +AtsfiP2CD4uvwlKZp2iOMrpOePD1QTiqjTmEggYJgBO0JCKjhtTAd0cFM/WlPBK3 +PV9fSjeOcHolWEHQGeVff8iuuzXxnOvAHt2xhDHsMsappsTDuY7aNuYgMad8oazz +jxOYM44kT/jZNlfFEhCfASCJwDF1+QB+plFDjdafyfGJiqxedPJRe9map3Ei44W3 +vvexw/SBp1q9Bt2/OcJ98tz3Co5xeQiE+nP4/ttHgJy/UiZhuchVUdbgs8U6Y+rK +A3cCggEAbWj5NNDv8JuXOpQVtvyAS5v6UJYgZpX7+Ts11hXANK5NwsxVoKM7PlQ3 +2PKEcT5eWpiygj76e2/jxcSlW7smlxh05Q8Pi/zHaYOVtBRygfA7znZHIlWv+OFh +UJUJrXhe/Tg/MoaQy/tagcwVY9Nt7lh4XPhH6ENkO9OLy8W9XcPhMytpOWE6cWnr +bghpz6yxp6/ZbG9tPW5oFmKkDQJp4oF79fl5uGn4UqKZvh9jBdrCKgFHORrVam+f +iJtK+D/PgeTs5XGUOC2otWc5rZMPCDiT4wrgNkxnxlkIqQ3OlszKRjs0/gfvZL6H +xNxIR9fQijm7VhVumF+XPU5LW3GLYg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002-client.key b/tests/testdata/pki/ctrl1/keys/002-client.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002-server.key b/tests/testdata/pki/ctrl1/keys/002-server.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002-server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/002.key b/tests/testdata/pki/ctrl1/keys/002.key new file mode 100644 index 000000000..f747b52df --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/002.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +JEwDoz4jrGsQIvYnrDygS1DUgU8BXw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/client.key b/tests/testdata/pki/ctrl1/keys/client.key new file mode 100644 index 000000000..b09506b65 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +GS310cbh3YMphOQfnvPdUmpYosUE+mA= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key b/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key new file mode 100644 index 000000000..0e1becded --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/ctrl1-wildcard.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +nA2XaI04MOuQS6f+KbxCKjPm4wAqTg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/ctrl1.key b/tests/testdata/pki/ctrl1/keys/ctrl1.key new file mode 100644 index 000000000..00fde317e --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/ctrl1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +0m5V3q28vSoG+oMh8tb52ou7ehAx +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/keys/server.key b/tests/testdata/pki/ctrl1/keys/server.key new file mode 100644 index 000000000..a6a2c2928 --- /dev/null +++ b/tests/testdata/pki/ctrl1/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +iZ8vcOG7Ef5P8ozXCTY2gC0LgAwtGE0= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl1/serial b/tests/testdata/pki/ctrl1/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl1/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl2/certs/client.cert b/tests/testdata/pki/ctrl2/certs/client.cert new file mode 100644 index 000000000..79dbe6884 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +Ux+nTcEJiFavZWYRwExpVLHPoNt5h6Q= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/client.chain.pem b/tests/testdata/pki/ctrl2/certs/client.chain.pem new file mode 100644 index 000000000..5f619e09d --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +Ux+nTcEJiFavZWYRwExpVLHPoNt5h6Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/ctrl2.cert b/tests/testdata/pki/ctrl2/certs/ctrl2.cert new file mode 100644 index 000000000..d16b1d146 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/ctrl2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem b/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem new file mode 100644 index 000000000..bc0b38f90 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/ctrl2.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/server.cert b/tests/testdata/pki/ctrl2/certs/server.cert new file mode 100644 index 000000000..0be56eafe --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIQSVxp5FK1FgizYm7h4uH0/zANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIFR3 +byBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAxWhcNMjcwNjA4MTk0NzAxWjBY +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALeAzoNPjeodnKM6757o1p2HeUKZ9Sjk +3jwf0YKJZ88ZyotseFXRheCWVcIaprvnEiSU2xxU9DcQNKCL1sdGoyHvA17VVxOz +uRAmVOvcleL4P+3zsfwnv2SG+9tsRSDBNRqjiBnb312cWpLPZFvhA2neYX7rKbGf +eMWswPuo9txNyKW6w/0gwqdg4VYJlNMau0iyR4R0tqMj2ujcyHOualkQ9UHof9vA +9SO/3XbbgVKerS1NRRifkAlkBZmWRN33FRckJhs/TBmgeylKBKph74LIda5qvOCV +D+aEj8rZ088yAJvebpwf84d1J+6oJLM2Jg7pZrRF17qjcEX8Ws2UWnZ1h9tfsLpc +o35f3AzxCAy0qPNIj3AK2ea4d+xOceq8z4K9C6pYsmlDykyXYb6ajpRXNtW7PUl+ +CaSSf9PcYpz6uos0pqqe1dm1CtBaMY7EAYPJKdjDCoKOmdnGpmBeAeN2+XiBiuJn +sm+LfR5JI3NTne76/oTcFAsSL7MvmuuSfZFGi0ITEZJSnW40Si6yNuaFqIL1w7jo +24Cao1qPUb4Oq7b2d6BE6kS0CQrsXqo4SHmZlCqPWszMF4I7XlcPFYrNAwLFvHh8 +ELhXFi5edhG1b9Ev4S8wn45xr8kg8SoX0tkMG09eGwItKv2d8YYmvNCZVjsXPUY8 +9QlM1GReJMadAgMBAAGjgaIwgZ8wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFJONrbM0VVrFZGIz4dJublQXLBDdMB8GA1UdIwQYMBaAFH7P +j+tHTk24/Hyq1ptc2HJ6+r4kMD8GA1UdEQQ4MDaCCWxvY2FsaG9zdIcEfwAAAYYj +c3BpZmZlOi8veml0aS50ZXN0L2NvbnRyb2xsZXIvY3RybDIwDQYJKoZIhvcNAQEL +BQADggIBADVe4P4lrgXQLf7lYPPtH23EujMw77eL7vKgGhnrUNqwB3N1Bl2rNaEd +wTCQVTR5QMNAbHCP/FMrlDot76wKpIg6b7bkfKmIV+89YtTIZ4Q7PORtj9Dch7wF +FO+a01YELZM6rNbdACvnBDOm44ofN5rbGZ+EkJKtZjjqcXo0VEV25qiX9wrzEkar +Dsm8cru/7oJ5nyiYl0XCVoOWg7s1ENKWAjUWAo5vDUCaLpILac0SH+9EW94jZSVq +b93/FyMLQ4TMHc8OTH2x6QdcICqV1RswMD9X7ZAqVl+uGnwO9bZSO3364Vh0Rtx4 +eU9aBZqWrkmWCy4ufZ+7OgBfm3CCIyQjD8Ro/dZyrqSYORG8TCmQAXtCkorjar0H +w10Z+K9YVcIrVMo48n3TafI8WOxTVe+IStEw9yPOpVEasLKpwZEhz5DQgky9KvOR +n3+Xz3aiBz0V4XtkAycDjX0cXPNH92McsAcU4xa1RJZZo42aB+jjJKrtja3zB9fb +dtpHcBoOWzlfPAP3ln89HYH1OlzMmics30sMVwOt0JnARGd00wziPVBeIkRTygcD +X11QRhnpbgj6pnogvy22aUkvv+Evc6sZ1MJrwtaq/NWgdotrLPfM6nHat49Mv8x4 +GrBCYbe/Emui8/Mom/nCw1MuBgRUXgJxJvH9HmvU4e8pDNGNxwZX +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/certs/server.chain.pem b/tests/testdata/pki/ctrl2/certs/server.chain.pem new file mode 100644 index 000000000..4f2979d32 --- /dev/null +++ b/tests/testdata/pki/ctrl2/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIQSVxp5FK1FgizYm7h4uH0/zANBgkqhkiG9w0BAQsFADBu +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSQwIgYDVQQDExtDb250cm9sbGVyIFR3 +byBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAxWhcNMjcwNjA4MTk0NzAxWjBY +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALeAzoNPjeodnKM6757o1p2HeUKZ9Sjk +3jwf0YKJZ88ZyotseFXRheCWVcIaprvnEiSU2xxU9DcQNKCL1sdGoyHvA17VVxOz +uRAmVOvcleL4P+3zsfwnv2SG+9tsRSDBNRqjiBnb312cWpLPZFvhA2neYX7rKbGf +eMWswPuo9txNyKW6w/0gwqdg4VYJlNMau0iyR4R0tqMj2ujcyHOualkQ9UHof9vA +9SO/3XbbgVKerS1NRRifkAlkBZmWRN33FRckJhs/TBmgeylKBKph74LIda5qvOCV +D+aEj8rZ088yAJvebpwf84d1J+6oJLM2Jg7pZrRF17qjcEX8Ws2UWnZ1h9tfsLpc +o35f3AzxCAy0qPNIj3AK2ea4d+xOceq8z4K9C6pYsmlDykyXYb6ajpRXNtW7PUl+ +CaSSf9PcYpz6uos0pqqe1dm1CtBaMY7EAYPJKdjDCoKOmdnGpmBeAeN2+XiBiuJn +sm+LfR5JI3NTne76/oTcFAsSL7MvmuuSfZFGi0ITEZJSnW40Si6yNuaFqIL1w7jo +24Cao1qPUb4Oq7b2d6BE6kS0CQrsXqo4SHmZlCqPWszMF4I7XlcPFYrNAwLFvHh8 +ELhXFi5edhG1b9Ev4S8wn45xr8kg8SoX0tkMG09eGwItKv2d8YYmvNCZVjsXPUY8 +9QlM1GReJMadAgMBAAGjgaIwgZ8wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFJONrbM0VVrFZGIz4dJublQXLBDdMB8GA1UdIwQYMBaAFH7P +j+tHTk24/Hyq1ptc2HJ6+r4kMD8GA1UdEQQ4MDaCCWxvY2FsaG9zdIcEfwAAAYYj +c3BpZmZlOi8veml0aS50ZXN0L2NvbnRyb2xsZXIvY3RybDIwDQYJKoZIhvcNAQEL +BQADggIBADVe4P4lrgXQLf7lYPPtH23EujMw77eL7vKgGhnrUNqwB3N1Bl2rNaEd +wTCQVTR5QMNAbHCP/FMrlDot76wKpIg6b7bkfKmIV+89YtTIZ4Q7PORtj9Dch7wF +FO+a01YELZM6rNbdACvnBDOm44ofN5rbGZ+EkJKtZjjqcXo0VEV25qiX9wrzEkar +Dsm8cru/7oJ5nyiYl0XCVoOWg7s1ENKWAjUWAo5vDUCaLpILac0SH+9EW94jZSVq +b93/FyMLQ4TMHc8OTH2x6QdcICqV1RswMD9X7ZAqVl+uGnwO9bZSO3364Vh0Rtx4 +eU9aBZqWrkmWCy4ufZ+7OgBfm3CCIyQjD8Ro/dZyrqSYORG8TCmQAXtCkorjar0H +w10Z+K9YVcIrVMo48n3TafI8WOxTVe+IStEw9yPOpVEasLKpwZEhz5DQgky9KvOR +n3+Xz3aiBz0V4XtkAycDjX0cXPNH92McsAcU4xa1RJZZo42aB+jjJKrtja3zB9fb +dtpHcBoOWzlfPAP3ln89HYH1OlzMmics30sMVwOt0JnARGd00wziPVBeIkRTygcD +X11QRhnpbgj6pnogvy22aUkvv+Evc6sZ1MJrwtaq/NWgdotrLPfM6nHat49Mv8x4 +GrBCYbe/Emui8/Mom/nCw1MuBgRUXgJxJvH9HmvU4e8pDNGNxwZX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF1jCCA76gAwIBAgIQbznIHhu+aeWoj8Lm/IcLPTANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMRowGAYDVQQDExFaaXRpIFRlc3QgUm9v +dCBDQTAeFw0yNjA2MDcxOTQ2NThaFw0zNjA2MDUxOTQ2NThaMGQxCzAJBgNVBAYT +AlVTMRIwEAYDVQQHEwlDaGFybG90dGUxEzARBgNVBAoTCk5ldEZvdW5kcnkxEDAO +BgNVBAsTB0FEVi1ERVYxGjAYBgNVBAMTEVppdGkgVGVzdCBSb290IENBMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5QfuKeMRpUNFlS555kKJi2Hx62/O +yb/+cRrxzY8xIhTle1f+5Df8ag4dN3NdtrGecd2Y+M65nSXoDAvT+yjKv1REiTke +TZMkyi1ZFmKzJU6K8hClS6sQrw9NEDMdNuLd+o0LXeT4aCFpVKfQoe6wC1ZqCjaR +2pDsl7BpHwD3Xxp+wuY/L5OGY0hPZfCVxsr7AkR2jNJV3xMzzianQoX0cCBW8BqA +z4u3adur5iduRwBzKVbIF9VGBJ1SeOphYeMFyt4POijQV8xo8tZ2fXrCkIcPMDDe +efBzE5FtxUKrVrr63GtKKRdZErZAe6VOKt175sn14+ZIgtlvbkEkV1YXEglQt+qI +BNF9FSvrwvoivWKhfn2JZg+EC2KAxwuUHzdU8HH3jh8kfgtl2IA0QcyG0jpRMdrZ +hxnJ+XGGLnlbC0Jla/KBJuC6CVBVsR6qnlCM84BSZByPMrqwD0ZizA7a+e7Z5fAX +KMcJjx+DwXt5EKhCIjRShdcJODbPnvF+Z/nVcYVaoVeEHpWPUFWpKrEgcam6Z6af +V58wokI/T8lBf8A0lz1KfpJkGIy0PFDHyGRYwrBK1Vjhsla2Q8N+s7eHBFxxInjy +jA/LS91Xs5VNfJ7Abo96w6nPcVHb+k/j8JaL2GC8G4vJ4OfYbOsjYztCy9gUxkAU +n0oGd9ChDcLWHZUCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUutSJwRtAe1zG6uPjmlbxGC/ye3owHwYDVR0jBBgw +FoAUutSJwRtAe1zG6uPjmlbxGC/ye3owHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0 +aS50ZXN0MA0GCSqGSIb3DQEBCwUAA4ICAQBCB5ZVUx6uYscDxmhHkdcgsbtaWAkN +PeSoRzGjeWrXyMpp24zVuIL51CJ/9l1hf1VdCZnnNEfdOXnwBul6wWOjrIgB02zP +ikPQp95e1GdHc1naWp7Y6H8fNxUoOohb9G+PfO7UqjxZH9/vgC0wd858JHCjmJZ5 +Jf1mB4DCpicBDNUfwJSIfuP7gS4I1gM8vsHyKGJWSvYIUD8ArasFm4+wXi7hKLpa +aNkVgBYxW2wJjj6LeKcwWX9BiaCKJJY5ukvWARPufPkUiS4VL/mw4n9u5Yt9Vo5Q +e4hdXr3q7xfrfm2hbOOPkuAfI+dhJ5iLzcmEqm89L+1Il1TjptvDsOoBVFgrPDfh +c6v3LWImrvbT72Jy2LyhVdEy8pIfhzblbb2iJjkaqHCPLl3Zthv0k01FTDlty0Yk +a6nopVgruzvANz1/b23w/d2uMRcnd/fDU4SK6CjgA+IZI/07AVWfWKyQlerEp4cF +dovVTYzV0nxej1pfCCRFp0r4hqYPNHggPRox01+xjyLM/toLY/LX0TRsvz7BaN6c +skWZT+MpHljjHmFH5HNcaoRBQmagMKoEQLHAf3is7cSACFKLM8WJhCQXBce5KoUY +tKg6hv165NIPpB03x0eWjxMpq1SEano2CTA6mSVFhHOxSr9YQGgJ/jEb0+HrLcCP +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl2/crlnumber b/tests/testdata/pki/ctrl2/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl2/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl2/index.txt b/tests/testdata/pki/ctrl2/index.txt new file mode 100644 index 000000000..602efa132 --- /dev/null +++ b/tests/testdata/pki/ctrl2/index.txt @@ -0,0 +1,2 @@ +V 270608194701Z 495C69E452B51608B3626EE1E2E1F4FF server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl2 +V 270608194701Z 99E7ACDD30109FCF741C1B50D5734475 client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl2 diff --git a/tests/testdata/pki/ctrl2/index.txt.attr b/tests/testdata/pki/ctrl2/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl2/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl2/keys/client.key b/tests/testdata/pki/ctrl2/keys/client.key new file mode 100644 index 000000000..d26af536a --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +GMCP9cRBczHq3SwCtS05fdJZS1YP +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/keys/ctrl2.key b/tests/testdata/pki/ctrl2/keys/ctrl2.key new file mode 100644 index 000000000..d24cbbcc3 --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/ctrl2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +eyhEiBE1bGvtRz2pOW2AwJ/4o2oX3w== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/keys/server.key b/tests/testdata/pki/ctrl2/keys/server.key new file mode 100644 index 000000000..e0d66d328 --- /dev/null +++ b/tests/testdata/pki/ctrl2/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +tmOMH+mLTElAKmn0PX+kD0XMbYcB390= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl2/serial b/tests/testdata/pki/ctrl2/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl2/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl3/certs/client.cert b/tests/testdata/pki/ctrl3/certs/client.cert new file mode 100644 index 000000000..614a63132 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/client.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCA82gAwIBAgIRAJR+Z3r5ca2rcP81499c/i4wDQYJKoZIhvcNAQELBQAw +cDELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEmMCQGA1UEAxMdQ29udHJvbGxlciBU +aHJlZSBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAyWhcNMjcwNjA4MTk0NzAy +WjBYMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJk6/PetUwUgp9/NGNa9XHCsZrVV +zKdvdY72nHUn25xrssWYUmhn1cbC4Zk1LkIOC2ZCjN4E2ghELtMrQC+ehTq5bw46 +XpVR2Oe/5vwp7bKEggI87Zm71axN+qQNImpcivzYzbN2XXaS9aglDxADay5QmkJK +fJN5fTdWgSO9DPrysbiiZB/Rq+Hy7ZIb7WqSJNcB+Ikk8zhIENyJl6IfT0mBSmKx +B1imeqJFAQI1E7nDC571yg0CiRal2/aBLSXSL2xhp9imtsWz4AUj2SM/PwSF+YeO +MPucBQqEio8LiJv5yRf4y9++bekhWdEPgzl8c7Ok7ipVuEu2jGL3DdYmIT40c25L +jS9CgH8Nn0A5KOeS0LMQ7zak7LRepJUkMCP9nMzITVZos5xrmhEfwEGgA4mdsTwy +1+yC+dOyzhx3QHD0wcOlJbbHgZ+dxFA6LbQ2SYj6ACT/SDYCG6BeBLTFHmPgDXRN +hOJVHOYqTuOjDFGCkTwpWe6ZA5DZzuhA0fSingM2GAFv4s1liIsm5FW3sFZm/8uf +peukW+qUZm9s7ItxKxFyO5D4GNyvhDsoDVYhOnnjviHvBlH4BH6njMIkdKIZPcrs ++St/n3L0IwHAH6Z+z+Ss3/jy6cYg5OBrEQBi+icsTaQfv/xb1yg6Vazs+iOST/HY +bN8NL1AcGtdpgFJFAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB +/wQCMAAwHQYDVR0OBBYEFIwk5BkFpTrvwTixqKqXyauIZ8bEMB8GA1UdIwQYMBaA +FEXOuRtwkd7kHv3/yzcUKCUgZiEwMC4GA1UdEQQnMCWGI3NwaWZmZTovL3ppdGku +dGVzdC9jb250cm9sbGVyL2N0cmwzMA0GCSqGSIb3DQEBCwUAA4ICAQCNwLVbetaH +P84InOu1AcwVMUdkN9OD8Tl/16eQ3V8p1+3awRPqKS5nQDUciw0YTU85QeQobdY2 +mgPlmcBlD7l/EcVXQRTCnC0MLFbEkhqjd0UxCcAt8TT8OWCGr88eFECrXBP7YUIh +oIprnqlrJyCFcS4rwgSDP3PqB0JULxjfTsJUWAKcsx9g48m32yXYo5o2h8x0WnwJ +X1/HKyqDytshATF/zEaA8dx+CvyvGrK957jv8Q3T/Ggz7+D8Cj6o9cGd3B8XcQOM +lfzOQBvJoUQClLC2kit4TQPvpLedH/ggfFQI7Lu+WBONlodco6M+5jmUg1bFFZnf +JVQ9BqYJ80WvXLgti5lwD2ETV+U6CBv869mFO/LPyUsmSXn6u7qtzugx/AqvASjp +cG7JE+Nchse5souPSRUerkUI2yJTEwUz1XPKtpy0bfqYY9M+82hU3MizdVp8Th7N +S2LzARGfwrIEf7prjTQGV7mfrMB6FP1hoGxVBLJTn0D6uWUkbhzq+f+2MIXkLel4 +sIWHzgcfbh8myHBHHh/K3vk+XNigNqXGft4tD0INVOJd6nfIMFXVtSiPFAKhH1Rt +nOFj9oYlfmDLgEDcJwE+eHYi3ciYtdC1QuCsehz4ZJ/z72w/cU+5Lusu91zlUYnW +xPulh0qLGQYUplsS+oZLne3DESbfB7tvTQ== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/client.chain.pem b/tests/testdata/pki/ctrl3/certs/client.chain.pem new file mode 100644 index 000000000..8bd3e413a --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/client.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCA82gAwIBAgIRAJR+Z3r5ca2rcP81499c/i4wDQYJKoZIhvcNAQELBQAw +cDELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEmMCQGA1UEAxMdQ29udHJvbGxlciBU +aHJlZSBTaWduaW5nIENlcnQwHhcNMjYwNjA4MTk0NjAyWhcNMjcwNjA4MTk0NzAy +WjBYMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMQ4wDAYDVQQDEwVjdHJsMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJk6/PetUwUgp9/NGNa9XHCsZrVV +zKdvdY72nHUn25xrssWYUmhn1cbC4Zk1LkIOC2ZCjN4E2ghELtMrQC+ehTq5bw46 +XpVR2Oe/5vwp7bKEggI87Zm71axN+qQNImpcivzYzbN2XXaS9aglDxADay5QmkJK +fJN5fTdWgSO9DPrysbiiZB/Rq+Hy7ZIb7WqSJNcB+Ikk8zhIENyJl6IfT0mBSmKx +B1imeqJFAQI1E7nDC571yg0CiRal2/aBLSXSL2xhp9imtsWz4AUj2SM/PwSF+YeO +MPucBQqEio8LiJv5yRf4y9++bekhWdEPgzl8c7Ok7ipVuEu2jGL3DdYmIT40c25L +jS9CgH8Nn0A5KOeS0LMQ7zak7LRepJUkMCP9nMzITVZos5xrmhEfwEGgA4mdsTwy +1+yC+dOyzhx3QHD0wcOlJbbHgZ+dxFA6LbQ2SYj6ACT/SDYCG6BeBLTFHmPgDXRN +hOJVHOYqTuOjDFGCkTwpWe6ZA5DZzuhA0fSingM2GAFv4s1liIsm5FW3sFZm/8uf +peukW+qUZm9s7ItxKxFyO5D4GNyvhDsoDVYhOnnjviHvBlH4BH6njMIkdKIZPcrs ++St/n3L0IwHAH6Z+z+Ss3/jy6cYg5OBrEQBi+icsTaQfv/xb1yg6Vazs+iOST/HY +bN8NL1AcGtdpgFJFAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgXgMAwGA1UdEwEB +/wQCMAAwHQYDVR0OBBYEFIwk5BkFpTrvwTixqKqXyauIZ8bEMB8GA1UdIwQYMBaA +FEXOuRtwkd7kHv3/yzcUKCUgZiEwMC4GA1UdEQQnMCWGI3NwaWZmZTovL3ppdGku +dGVzdC9jb250cm9sbGVyL2N0cmwzMA0GCSqGSIb3DQEBCwUAA4ICAQCNwLVbetaH +P84InOu1AcwVMUdkN9OD8Tl/16eQ3V8p1+3awRPqKS5nQDUciw0YTU85QeQobdY2 +mgPlmcBlD7l/EcVXQRTCnC0MLFbEkhqjd0UxCcAt8TT8OWCGr88eFECrXBP7YUIh +oIprnqlrJyCFcS4rwgSDP3PqB0JULxjfTsJUWAKcsx9g48m32yXYo5o2h8x0WnwJ +X1/HKyqDytshATF/zEaA8dx+CvyvGrK957jv8Q3T/Ggz7+D8Cj6o9cGd3B8XcQOM +lfzOQBvJoUQClLC2kit4TQPvpLedH/ggfFQI7Lu+WBONlodco6M+5jmUg1bFFZnf +JVQ9BqYJ80WvXLgti5lwD2ETV+U6CBv869mFO/LPyUsmSXn6u7qtzugx/AqvASjp +cG7JE+Nchse5souPSRUerkUI2yJTEwUz1XPKtpy0bfqYY9M+82hU3MizdVp8Th7N +S2LzARGfwrIEf7prjTQGV7mfrMB6FP1hoGxVBLJTn0D6uWUkbhzq+f+2MIXkLel4 +sIWHzgcfbh8myHBHHh/K3vk+XNigNqXGft4tD0INVOJd6nfIMFXVtSiPFAKhH1Rt +nOFj9oYlfmDLgEDcJwE+eHYi3ciYtdC1QuCsehz4ZJ/z72w/cU+5Lusu91zlUYnW +xPulh0qLGQYUplsS+oZLne3DESbfB7tvTQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/ctrl3.cert b/tests/testdata/pki/ctrl3/certs/ctrl3.cert new file mode 100644 index 000000000..d0c74a596 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/ctrl3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem b/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem new file mode 100644 index 000000000..5e275e3a9 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/ctrl3.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/server.cert b/tests/testdata/pki/ctrl3/certs/server.cert new file mode 100644 index 000000000..baf93bec7 --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/server.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/certs/server.chain.pem b/tests/testdata/pki/ctrl3/certs/server.chain.pem new file mode 100644 index 000000000..b511da8ed --- /dev/null +++ b/tests/testdata/pki/ctrl3/certs/server.chain.pem @@ -0,0 +1,102 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/ctrl3/crlnumber b/tests/testdata/pki/ctrl3/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl3/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/ctrl3/index.txt b/tests/testdata/pki/ctrl3/index.txt new file mode 100644 index 000000000..1608c060d --- /dev/null +++ b/tests/testdata/pki/ctrl3/index.txt @@ -0,0 +1,2 @@ +V 270608194702Z 50E4CACA9B71F50108FC0D4656297B72 server.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl3 +V 270608194702Z 947E677AF971ADAB70FF35E3DF5CFE2E client.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=ctrl3 diff --git a/tests/testdata/pki/ctrl3/index.txt.attr b/tests/testdata/pki/ctrl3/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/ctrl3/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/ctrl3/keys/client.key b/tests/testdata/pki/ctrl3/keys/client.key new file mode 100644 index 000000000..c40543d3c --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +UmJ3wX2J8nbWkv0jDPNpZS1bAdvsSg== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/keys/ctrl3.key b/tests/testdata/pki/ctrl3/keys/ctrl3.key new file mode 100644 index 000000000..d627f9bbb --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/ctrl3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +OuO7d1g9Ek/alqalOhx/fpRu9ZO4Sw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/keys/server.key b/tests/testdata/pki/ctrl3/keys/server.key new file mode 100644 index 000000000..d33be67cf --- /dev/null +++ b/tests/testdata/pki/ctrl3/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +fxDKHr5e6RElhWIDnvjxEn2vGdhwfQ== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/ctrl3/serial b/tests/testdata/pki/ctrl3/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/ctrl3/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/root/certs/ctrl1.cert b/tests/testdata/pki/root/certs/ctrl1.cert new file mode 100644 index 000000000..123517c0c --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +i8Cvn86n75R7dYulC0pnR8nNGCpxLx8= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/ctrl2.cert b/tests/testdata/pki/root/certs/ctrl2.cert new file mode 100644 index 000000000..d16b1d146 --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ZZaD6pW9+nFJAHwO6JwH4Wj4nn7GAp0= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/ctrl3.cert b/tests/testdata/pki/root/certs/ctrl3.cert new file mode 100644 index 000000000..d0c74a596 --- /dev/null +++ b/tests/testdata/pki/root/certs/ctrl3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +AC0jCYIehvS8+UpzdwVe6K9AuxoiUUUjZFc= +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/certs/root.cert b/tests/testdata/pki/root/certs/root.cert new file mode 100644 index 000000000..93c5eade7 --- /dev/null +++ b/tests/testdata/pki/root/certs/root.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +uDrWCGilEcCtPw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/root/crlnumber b/tests/testdata/pki/root/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/root/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/root/index.txt b/tests/testdata/pki/root/index.txt new file mode 100644 index 000000000..ff5f48994 --- /dev/null +++ b/tests/testdata/pki/root/index.txt @@ -0,0 +1,4 @@ +V 360605194658Z 6F39C81E1BBE69E5A88FC2E6FC870B3D root.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Ziti Test Root CA +V 360605194658Z 1BD94A4C2C917927AC9231CAE00C6AC4 ctrl1.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller One Signing Cert +V 360605194700Z 227D1377F2694DE34C97389078818305 ctrl2.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Two Signing Cert +V 360605194702Z E694727C36315D5E094BC539D0FE4339 ctrl3.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Three Signing Cert diff --git a/tests/testdata/pki/root/index.txt.attr b/tests/testdata/pki/root/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/root/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/root/keys/ctrl1.key b/tests/testdata/pki/root/keys/ctrl1.key new file mode 100644 index 000000000..00fde317e --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCj9dl0nTsVXPHa +jDTvFw+TimPLC/543QX/lmnMLU3YWDqMpSj0g2PYATWjAMG6nH1dg7ZmDNXZnIud +zjB//ul13Oqqn7bCTYBMzWdinMTIfrUhBgrfLVJolwmiw3utnuU+hBnepPZhiy0t +UKx8VHLK3qE9g/BnkMgBFmwFf6DHPg0x+PSCBGCvQsl4TC3CKnt+IF1f7Estn2sp +A5SHZH1LO0XM8kt6Phuxz05IgK0YSEjvH4kFuSE2lbGPQglnqJZj14AVCxF6MRUj +XDJaSEsYiZ4uPlexwD+h1e0K3xLAqDvyxF7C/C2BXZTrCGBpb3TVGOsNu6zf1Cqo +Sgpf/JZPNlLzeS+7ek/G/Cwz7Ap5xsK+DOZf6SSwAH3H2uwRG5YKsdFELGkxH3Ew +QcG3b6TJVlo294sdNYt1UB7uvlxYw3kfWXYcxoaqJSCLr0gn7mFmIavhFnAlqHHh +uNZ+qva3un2cB52tN4AQj5b8xvlA2bgzii1rl0eI88eMwgYuhpcAuhlyzqOQphRb +u2WBOLtRyB5I/ttG1J4pJUglQUoXQE/nsl4VksiW1zJ8XQ+Hb0RwhnkJAJsiNNFK +qcKmCkjMglc9RnTfzOP9mO4dj7DwPrMKwNnE5qgwtROIn6vdkHAyI+NVTdw8YDDN +V0NZWePVyrWzHNrqBy6oIZcVN90dZwIDAQABAoICAC7dbGEhOgyiqvyxe8XlXQ3q +jhixHnUUlAzYxPfX8TrICUA/SyQM1EKfIeIsKrO43DqZFc84lv2i+eNK1uEXD0sh +sK/BhB8owOXzBjyRG8xFL2e3ju74yOfdWCM+ZgEb/GGwp6ZUl5oNCoY723mUN9WV +6henuVUY9Joe+xRdRSr+KQ5iHx10u+AMooKwn5myw+aqwJXU+C4btakc/Vzv08Jn +uE1a6kkQLKFX5IPjx9Y7fyFeba+FmaE9C2or0X1gGlCCfflF1yKKmgSn6zqUFGb4 +mw6TwkQr8+RBvgYP+g+4Zp4/E+j+5NDn21OM6uXoNkhc3X7o6IJ35hOBSlLiY2He +rU8nezykfpCg+qjeve6FKzMTmDJRMbZIYAB+T4JqjkvGwbPpihdGRnJlBM6+DsTC +QBNnZw4wEKymp4pT7iGffzzLUlJCOZIAHFcOsuG+qNh3bpS0UIvBx6aaM9v+DcHa +8cf/wFLNFdhDQEv0DYpK63R/QxExJ9UwP4Ugf+KTrbNZS/j/sEBpxXdJB91MNWi5 +rX85pZ0x0PzdKiXTHjWE2mI88ZIb/ZdL17Vm8exfyTvAWJdFnVioZkzeqhvCI1JH +JMXjzuRsAXKw6nbA+pJppRcrHs2pdY+qx2JDVsZY/5FU3GVTiCcDup6Z4Jhxy6HW +ez/13LVzTuLHkcEc4d9dAoIBAQDFdB9KUPlN/PrhzYHeL/q7uDv49nfNmyfvQ/vT +ATKZl10fxqGZH6j/CGXdsgd+HFXPfqpJOSSJvyBvuKeVgCqSx6ngey3S/vEQG5Fn +vmeDXRl3rqlB9iCguH4o83qWmjjYxaFsp71AcF3q/jneOZ9NCasp7r2TsEeEV/nC +9lfha5LC3c7p0HTD3xjiFQdJ15pMwilcUlJ1QPlk0PTcgIxIODr/6YEUmdlmVMg+ +L+kp+F7f6w3YPKVcerW5uESa85GQgOppHXIvOQ3NP8E+UfMlz6yDxNCKxt/oxJBV +aFWJiuXS0ZYrQQUx7LljUa0jhEXdmQlpIEfOS/AggHQYBsgTAoIBAQDUk2TzaRW+ +tHQWAaiqty+0CiuBitHf1GbIR3VCurvug6v9x0eJDuEiusOpKT1Cb6SbuJ3thsMh +7gI4NVcGvkV+97ZWfgR2CWy76XHs7skp7IoMXSN0jQq3t6JqFj9X/cp3UcjhfKnt +ePoDRkpiFwAekhn/syuMNBw6b5bukRWeHvj4bKLMYSHh+jWNRHIWe/N2DeuzqSkv +ofjqIGHXP0R6hCCfPlTwG3CHb312AsiXegjJfL8c6JDsjQmXhM1vQ3VqXxRsU3D+ +KK659f01oZsBgyQSpichtRcmXgeC1YRMmAoJgPVsuYF8IGspwGbtQ/fzcCuagYaR +ku2Yr5PoSqfdAoIBAC8sG+GcUMMyAhn6B+G2IrfAPwuujlafj73YxwvVCGqrP8M8 +qBS1/KDZN8TsKGAXkuSchUAzF6iU8cHfIqJT2VfxvYL0yrDS2XKYs3dOhNpcXp46 +KxOoIoljKjjMWmgqdhRLutIDjPIdJkLi855Es+squSqub7od7igPAIt0YPBoy8ok +Ra+UbqDw5rf0gCZDDQjzhgAZZru+hxZv2V/okhsa2/WRqpXqX4bUEHbS6WhufvQN +6uPTMUpTwqCZBkLil88nDVmJgGMJxWNYrOkfmPBamgNs/Ml607l/ZGATKgRPG7Lv +AWpaAUy5Gl1BARUwH6TeT+I+pQkDGV4aciHfVOMCggEALfmK1dIeb9ZbXP8S2Ykw ++gFRE31QktY/PIWn6Ly2NImpwwM8h3n+WyKFeqp+o0W+FifBkEObJFVziXCP19eC +9Eji2KX8lQLIz4NXrmSegUC1QqNKLcTrUnyW1dbl8EPlbBT2Gz55CfEmMVscb0aG +MhZrJRA9FN+YU1MbE5GxWTddpWzpcMZ5K4SP3HO3MQGx0BCGr56gV7ryOMC0KHd4 +ef7lh0tV13A30DLesY08kPZFvD4Mn1X1MhP2xRxlyfCPDmht5FfPkkh+MZ3wG49O +FO+l95qT0Ah4b0Xa3gMLz/z5/sAzVEZyqMPiKW+BU0Nl9vKFm67zybw7QtCGbrDm +yQKCAQA9lChrSe7ti7WYvF4VxtwCt88dAXqxcx1m4zV2g8917FjgXDatIEvrGL4q +JAlYp9fH6a4+4oHMwnlgPYFIjSp2tuPV4lI+t/Od9x98OVhd+7AJwKhQIhA0wEh5 ++t2lhsurIzNQCitG8BtwVS+3I8G76/Ao++ZQ/wF6980TwTjLDPe3ca/ENQ4NTDa6 +jTyjPQso1pbL2CaAejx8Rx3SiF9y0YzBR0LAA1xPh5T0mbnigwqcAPKq6Exu4xqb +WWPhgW2inU6HOq7kjX9BBngxLLUnp/O0ff1x5hcbIF1uPyYBROFEiDAidG0zXcAA +0m5V3q28vSoG+oMh8tb52ou7ehAx +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/ctrl2.key b/tests/testdata/pki/root/keys/ctrl2.key new file mode 100644 index 000000000..d24cbbcc3 --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +eyhEiBE1bGvtRz2pOW2AwJ/4o2oX3w== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/ctrl3.key b/tests/testdata/pki/root/keys/ctrl3.key new file mode 100644 index 000000000..d627f9bbb --- /dev/null +++ b/tests/testdata/pki/root/keys/ctrl3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDy0RJYpujLWqCV +F0IB8OvukufwzNRk7VTwEFlofAe2rN5dSJYhu3ype+ifHrN+T4u6rZJMzr0cUNjx +BEH4F1Wj1xey/NsFsCLcOJjG+v6I6T9132QGWs8W3SQETNSAqJWk58hkWuPwUiNO +RDrVXqM710qlJ3I6MQ1eCfMsuyurUwTrC0dg10C+DWhmcKR5OLdNgx1TLw1aKFRs +qb2LZWuuXpnOhar6wO+C2+mp1DnsXu+tR4pHZlLe/l4FVqlwruWHHp+U+PJJ0glY +1om/i4z5yrokn4ALZO6Ltdu1yZ5g7C83T3qsEmYjE0W1BC3DLP0HiWeR7yV6nw66 +6kD9/o9wVP9aPvvUIcOA9SwoLErKjbQt6Iks+4NG9iHzKU0GUXuzMHfXSiLfW5Sp +NLOb7mq8dGCzi8eXmfA+TKI8R0eerDNUQTxGCdGcVuvhr77LlStcjmv63OaO554f +o9Yg1S/A28WRyIxA28IB9uqoZmZOY+hbHi66apfNrADhBcm9XJAvucyuD4QJrMAd +Yj3T0VcFaABl8+uTqYOn+s/wS52aHry5qc0Eb0V9eG+YyVCwulyk2/NyfwBIYHzD +dCIVTcKuQTTIdjHEcgISY0pfsnw3V2O+/5tJoudsggXJKUPtJXasCHbM1GgBhZE3 +bcOpx8T/qOOdMDRhlMqZ901VLIQVEQIDAQABAoICAAWB7X8aRhR6uUK4dNRTcR6H +sYAaPUUOxwrs7AI9MfWYRTDreRBJzuGPQG7/hMW8KyiwUC2y0MJIKSuKU667ZMNj +GRQDvToLTTceh4SX49caJ0jWqM+mFqVnna4FShqi+EX1xetUzm/AhTF8xbLaQyyT +zQsi8mnUe/+ijSP6GNr5dpaYOmW9bCgDaNdN/cUMHshAzZT5770YRhXy4aw8QC2D +0sxG5uJqJuSadVnXSPsOCjStdzr4XK/XKC3J0e0O4oDmlmsMHH7FJ1YfA5/XG/r8 +eK1k+sQHZYvAs7uTV6bOJKIGCPvHLQ7lnIKnFhyjtBeMK8+5E2oNGontz0yTjhBM +ZZ/3H6pTT2NCo0k79MjXG8muiORWuTl0ZrQZberjOKovgddlUpoeVp6pLieIkzgC +ymddMCVLvntsUgFSX46KDEmj3VIuAC0gxFEyFwgvTpWM/cvG7MDp5hihsukEjwIm +/glJ2sHsPdfE5OHkpQSdG1yjpxUGGfgcI7K+ipPI8/Ke2Vpp18hjlqFRDmlowHBP +FBQYUkzvpwcB4iT+fYkgsbw7mCvc0DTv8L5E7iQYucXuj0vB8EF/UeSB50F2Gvty +ems6DHpeTQ16aDhL09WzWV5eivqrfOsHR1p0HKW/XER2sIdYOjLGfrBGhJ5BJxbQ +OuXAuHXJG/SejQ7wx4UBAoIBAQD8L7EPxTD6Hx/dbxhXNtitmN38PNOBUyof2qGW +zhntSZh4tMS7zK99/vfGNtBuxnfj8lQ2t2BDZxPMrVMSDEuDTp67pb/tmJlnL6Hd +n5n2omYc6NVc065dcZMI2V0C7GLVcdWS5Vb3PshsW2UU58u7W3HLHmmf0mLUHVU4 +XritHd+O6tS/NMr2ho6f2Je6QnaAKFtrElX2lyRMnNnfOV1MzD1rLxLySECNDEV4 +Nr+paVegIVacHE9tBWNKWtp74xdB3bW+4A7+NNSEJTGAluGPE39Rx3NTxjDevkX9 +xZlORwC0Dko61A9kIevVu+nzmMANTFoC79HlT1EaYjiYt9WxAoIBAQD2fRtSPKx1 +coKrqiPalerVPCfTvqMUe69fKd6YQxwSTDYSFcDPm22dkLLuC7Ko7iJKnKwJ+aiW +5Z4XWPHA65qGhd8V/5GB2vWDa6iLY0IX/M2HaQJqiphw20H8HD5F0OSco8R+qfc9 +VKNp5gK7gkExz7BnhPZCuKMNp0pRoAE8XaJvmec6v6mYK3PGfUPPzw7yHDWFE+wp +lmDod/7OdrsD5+MWVbSyy4KkUUrvNi4539BnyGj/vAd9hFJVu/kOlufGGeaiXA5a +q/SvwGYSL3gxsrgS287+3JoQkjbiSLTME16zYxRWUeZ5A1ritFl8SH9x9th34xXq +mLG944kaDC1hAoIBAQCSDvko3heYtdAZys85K/3gxUnEXmJNY6JhIpo2IpZnlRlm +x6Ot9UWq3rIYrgSYNACaF+7oZdquDxQrljMnn9FYcn+CxOPdM2WdmrvQBTEB5Frp +4Xw5sCwr2KzFEkdJeyle3/hHhOaSel1QTLrFmd6oW7UTZEDenNY6bea+qDWjpkql +lqKzP1tR3urZ73MpIHdLkJQp9kutbypJ6QpSvAGqihwEaRY7Fte0GWhe0K6+6tEi +YEyuS8NArD8ugGJMIGGG92bc7x4f4u82vefmxvxKhotWDQNhgMcrKt6UtQ4uhPcG +UcRyQAHOB8t0VcqRGGYbDZ6QVt+lRQP/GOYYpVhxAoIBABEHs8uKxZ+Xuc+Cgdeo +ZAE3lsjacwoHQaahje+XM2lQOqwlNJ0jb/9i7/nidQWW7meZS4mk5jEGzFVwn8Nk +g9inhzJN5g/CwRPDbHG0+ewOW2TvrGsQCFhDzdtNWEAanrDz36+grqReJKw8aBPs +e/SlFNsSJLGXcCyRUmExXOR+06pCR+eXNnB9EBK2tOi2taGksU3wgnCdIzTslX5O +Vb1/WAFDCqkPxobz1umQJMF65TtGbXq90wapDcc+pYaMhpb5UyYEljlNiCpccLDw +9qz4XB9xcGvLchmTAJfBzjwLWo+qWM1d+z6BLNZc/5HGsId/NpWR6wG6aw1jmyVh +kgECggEAabsf/f7tvlbtdZgCG1+IwSsbXKpa/qQJvLSnn2/R2RWYMydTg2bexHqE +NFAaVxwjaxwptq6x+86YTxu3M3c/X4K4i9Qx3IEByTUsPjD8+CCjxLJmSXB9qfJU +Tv4pLunLfS1LH4XuJsPs8ArwI/uPpA3fsqDdu5ojK/HSLk5KRL60DUStm01qHTMp +xs9U8BvVoW+8gg5aPf3g/09GauOS2w5t5BSNjjnXUCwJzLJTAXqaXt5mefA0KC5V +Wix5N9I15ZLWQttTaP27H6K8DioHhPfcsBh8zn0eRZkQ62yQb/tUzSzS+t3gfoF4 +OuO7d1g9Ek/alqalOhx/fpRu9ZO4Sw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/keys/root.key b/tests/testdata/pki/root/keys/root.key new file mode 100644 index 000000000..dbfd9dfe4 --- /dev/null +++ b/tests/testdata/pki/root/keys/root.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +X3LLrLWLVE+ST/a3Y4U7VReDAhLejQ== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/root/serial b/tests/testdata/pki/root/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/root/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing-root/certs/signing-bundle.pem b/tests/testdata/pki/signing-root/certs/signing-bundle.pem new file mode 100644 index 000000000..9cdfc4815 --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing-bundle.pem @@ -0,0 +1,136 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF9TCCA92gAwIBAgIQaXQ4Mg2VCAsv19byNQAAAzANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTI1WhcNMzYwNzE3MjAzOTI1 +WjBzMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSkwJwYDVQQDEyBDb250cm9sbGVy +IFR3byBFZGdlIFNpZ25pbmcgQ2VydDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMym9yd1CNkkhV9gUE5pNMiqp317bFM/AcM6oVjKDPWdIy6dslokaMGg +WYskiHB69RwYCcGyzRJQpZ+ZUU4YX+Wfy0s5Zu6vbogtoM52M3WIg+C8vrIwefTR +fXRcewvtU5ItbV0kNIgm3kNOHJ/pHiTDQ0a48fIXIhlPQtIgzLMB7H/32eknGseD +G5lW281k69XVqo1Ksuj7wheWIXCN3FJZal8EjdJkvCboB6ttb2tokxkMwaaOg7wz +IfLvARAbnSQaa8Gt3XQC38TADiLt5kbrsBw7gifiyOl9Uh2Kc2USabWFc3g8/dSu +m/9tbD5UR3yPniiAOA0K3FkfHmbHNYOsBHL8CAY0ivRFYpLbs/1RZ8QgYog9rp2l +ifKjOv6yae/MrHibYLW2ivrqZWMPPa+5WRb5ycrqjnfOkUoNHHnZGAE83mB4VUbf +4oSaDBJeSpzWgew384CEZTJKZMlmxi2M4lfIWA1hLQRrfp2Q2Njr6BCM6mXnH7P6 +mqJyPQbRkj3t9g9f1UHs6Z+MMI3iEb9De+IqxAhT61RFq7fM1KrCAmkrn2Qw8iqG +wxy9xXQe0swAZXBi1hVgaCMEud2EDleYe+9slb1eT5s/ZArZHYtV2TONeQxLQm4O +JXPEn8MgfBSpGez1txIsrgEFo6UXssDSH/7fNtC238Qo3s95Q7EZAgMBAAGjgYYw +gYMwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYE +FIh+T+V7kS5gdYYlo0Qwbq1BsgYfMB8GA1UdIwQYMBaAFKSxLLYlhadzoiB2UL1a +COLcf2H1MB0GA1UdEQQWMBSGEnNwaWZmZTovL3ppdGkudGVzdDANBgkqhkiG9w0B +AQsFAAOCAgEAHrTBc6XvI0jMbH5x4hrbAY7jancvV44JWjOC0UHX7M0XZlBNUnxq +NvTBvItgS8TAf/LNLFP8Okk5UE/4t+4ju4JkNsvYXvLHdi1+E9TfxBGFhl7fxoga +UwttJVje72oqvxhbZiQY+J/n/foKvA1isTRkL4u+Dlas8mEiygsT97qCInBULtOs +QP6IGkb3OdSO4kNGS4+jHEix2Hc2oosk2zthdv07JsD/hpRZ7aewi9KWpCT82AZS +hc1e2A0bJ8uHYbQ4nuoW133/F0CY4a1ThRTLY1Ac/zteBGUguLsRL0tRrEO6mTh7 +YHS2aXzWL5sk4GaArbxYfF4Zp43f8eEe4w74q0c8PJ74aznXutCJIzcqnpq7zJOr +OBoVPNiltfZAWFR9ulayNJ0hRL57hfDJR1LYMlYc9guh1nNyH3lhQ4GCxGGSf9Yh +pZYxaxf03RycUKpJE5QBcR+8uH7SUoid8VWmj2cZ0JCp+oXt5P82IcXOoYDkjnWz +qekw0pjPnTDDbDt3QMfrBJ/Ff+TSLREjjtUUq1ii9pRWUkFhfK+1s3vpAzACKIsM +hVi0bWZ7dlBo8PIT83Y5Ev0aZCDGdy1nUzGqpy92UUdNa0ZLXS/cVpfzSlnTEA6r +k1XJC8XKBHSDP7hCvbgXnmJmoYSuD2BRhHmjig7HSIL25rxr8yqT04k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing-root.cert b/tests/testdata/pki/signing-root/certs/signing-root.cert new file mode 100644 index 000000000..2ab71cddb --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing-root.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing1.cert b/tests/testdata/pki/signing-root/certs/signing1.cert new file mode 100644 index 000000000..64eb58ddd --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing2.cert b/tests/testdata/pki/signing-root/certs/signing2.cert new file mode 100644 index 000000000..61e7d692a --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/certs/signing3.cert b/tests/testdata/pki/signing-root/certs/signing3.cert new file mode 100644 index 000000000..b718a78dd --- /dev/null +++ b/tests/testdata/pki/signing-root/certs/signing3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing-root/crlnumber b/tests/testdata/pki/signing-root/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing-root/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing-root/index.txt b/tests/testdata/pki/signing-root/index.txt new file mode 100644 index 000000000..b44468338 --- /dev/null +++ b/tests/testdata/pki/signing-root/index.txt @@ -0,0 +1,4 @@ +V 360717203902Z 3B87C2F4998E4F147C9B17A09A3928A0 signing-root.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Ziti Test Edge Signing Root CA +V 360717203912Z B7F4FFFF0F1124C93D28FB1FAB1FB1F3 signing1.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller One Edge Signing Cert +V 360717203925Z 697438320D95080B2FD7D6F235000003 signing2.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Two Edge Signing Cert +V 360717203941Z 08252B39FA6BFF5F4A5C9713247987A0 signing3.cert /C=US/O=NetFoundry/OU=ADV-DEV/L=Charlotte/CN=Controller Three Edge Signing Cert diff --git a/tests/testdata/pki/signing-root/index.txt.attr b/tests/testdata/pki/signing-root/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing-root/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing-root/keys/signing-root.key b/tests/testdata/pki/signing-root/keys/signing-root.key new file mode 100644 index 000000000..67a75560e --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing-root.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +VJEDpvAeB2DHyNUEeZtmnx7cHnGc5rA= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing1.key b/tests/testdata/pki/signing-root/keys/signing1.key new file mode 100644 index 000000000..3f76626cf --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 ++zChBm7yqi6IpiHHBzba61+8R4k3OVk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing2.key b/tests/testdata/pki/signing-root/keys/signing2.key new file mode 100644 index 000000000..7b5235ee1 --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xaHRILYXaGEXR2iffVG2IXE3jH8skw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/keys/signing3.key b/tests/testdata/pki/signing-root/keys/signing3.key new file mode 100644 index 000000000..11c20f3e8 --- /dev/null +++ b/tests/testdata/pki/signing-root/keys/signing3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +CYWpy35FrNj2ymT1QJR+XU2Gn4GzJgg= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing-root/serial b/tests/testdata/pki/signing-root/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing-root/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing1/certs/signing1.cert b/tests/testdata/pki/signing1/certs/signing1.cert new file mode 100644 index 000000000..64eb58ddd --- /dev/null +++ b/tests/testdata/pki/signing1/certs/signing1.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing1/certs/signing1.chain.pem b/tests/testdata/pki/signing1/certs/signing1.chain.pem new file mode 100644 index 000000000..295fb57f5 --- /dev/null +++ b/tests/testdata/pki/signing1/certs/signing1.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF9jCCA96gAwIBAgIRALf0//8PESTJPSj7H6sfsfMwDQYJKoZIhvcNAQELBQAw +cTELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMKTmV0 +Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEnMCUGA1UEAxMeWml0aSBUZXN0IEVk +Z2UgU2lnbmluZyBSb290IENBMB4XDTI2MDcyMDA4MzkxMloXDTM2MDcxNzIwMzkx +MlowczELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCUNoYXJsb3R0ZTETMBEGA1UEChMK +TmV0Rm91bmRyeTEQMA4GA1UECxMHQURWLURFVjEpMCcGA1UEAxMgQ29udHJvbGxl +ciBPbmUgRWRnZSBTaWduaW5nIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDIomCPswRMTBlY5CJv/VlL6z98Wg55DtOUpPcKslkmGk7+/ckcaawT +G2qWsBzb1oCrd38S8YjReXpY/lL1QuOMkzM4qQVewyvq1Tbq6COSjRB9dxNkSynH +Dtf2iqwo6tKf2Zd6deCmN5j9mTbV7SGY1h2Ia8nVqHAj5x24kwzIek/vSoQzWWXx +NlJCtGxh6xpPcKr7Xz31bZxEcoQqYgHkq74dOrDAcfj8GD+q+ZRP99JyHcZre8rM +vE/rrgVj8ysSjACUKgTa7SKnIrJaZFi5tu3Gdqu9L2yfraaDp2RcCdt17Uh76L1y +YszR/JjPS23Pg6cfrJ6W+dU7PgmPA437EnK9P9sepBKS743QvcTog0v7wY5PJbSP +0JNQVT/2c4m4AK09UCWFFGTEMpmjFy2bLoGD1N8G5a+JRspKKcqXZTtLSjMqvznj +0SWdFdug5p4t4goenEWEH5vXNXS2fuX7uc7MXDE66bIU3EzGJ5nhTz5MsW9qrCRI +YKePE5lz21ZmId9Q/9BITw8HRZXREKx0iKKbMx2m3EdAzAigbaFFQ3TkDTH6UptK +IcE2Px5xaJe2A9t6NXG4l6uJX0v4tVfj6R/+TbhvprIH8oNRftLdCEqSXbKsT5OA +RDBBZsaoSp0xlTFpQJkZrbsUwFwroIzawLrsdyzWoeTAHd0BrbWrNwIDAQABo4GG +MIGDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtUqowBkHFM9kgrdaU6EsmkMMrLzAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9 +Wgji3H9h9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcN +AQELBQADggIBAFEGTwOBoFYiK/ExL6se/TpErcaQaakrS4orXNE2SUeDdb4ZbjzG +kiVkPU3eRtq+sphvioPjOuZhszVsZzOAgkNsWwp44+3NiUTYgMacj6f7nMyuN8Rq +Mtyxpp5jxUArSQ13200ES76d3Q8Vz8ni0AHD5Apo1YeWOy07Jm5GHRisJfMpPfVu +L8o1MTTbXNcNn+TEICmMpfzrYy0uf9Dl0rFR+M6UmPIhieTOP48k8AGeg0B/vDJ4 +7huCR044DRrQq+zQpMDFS97BoqPCvjLuy8iSjoWywaccZu7tyOjro44AiGDxs92s +lSm3PEKkSEIpYV1mj7e88YuSSz3tJ7Zzm+Jm6JTc+GEx/vqp6/sYumBJd2iqkyXL +7zqcVCzVTc+K3VA9KrQ+DkrZ7+v0iExpYTQbsNpcqdkzXx6nSl57cR9OZDTNBGI1 +W//nzeg7phfRGqDVBmZY+ny5hU46cbkt7oQnJhQfK/gEfMczPxCPyzGxolOR0Uc3 +yFEhSoXu8JwokRsq7rtyWt7Nbf9TynEcErak9Oj5Si8XMiSG2IYtY1h8xn9gL13W +C9otzWExdymMHNDLPddqB+CzQLmWm89xAuTPlCbDaikAOHt59KrZ1Q5L//08L2Sj +LZrRBL1FynfrQCA97wVGS8cxdrik6mRBz4iU+ho3JDeAKBgEWMaosw0W +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8DCCA9igAwIBAgIQO4fC9JmOTxR8mxegmjkooDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzE5MjAzOTAyWhcNMzYwNzE3MjAzOTAy +WjBxMQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3Qg +RWRnZSBTaWduaW5nIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDFo8spPkAj69aH68/Dt5hlgBWAjZwX1DjCfrl8TaZzljPPCvwW1PDyjNkb +hd5CUQaCFs72++EQctNV6YZauEgsROdMyKrSxFLvOv17Gw8IU2F0CX9FX3Wn4h45 +dAL/s18Nr6B0PoQY0fl/ZY55K9nZSOnRJ09NaYT6obwOc7Oy96p0uSDuugM099sO +2Gfgo1z9pAykj3U6aSlMt+v8sACd7Cwn0YKD7t6n57H6kxdMtO6gH0fiLjbDIKfC +WRJa3RSUPAmCr+ThvnAR9pCYj/svOn+U6F+VPKpdtPdNiHj1jeuEduYfZb4fUsSh +ZJmCcHGEee87+n/ZFT+lffp9uWIFPkPeMI8hYEyH3CS+zPHdRRnsn5j/WFQrF9kq +VauRIuGwDu1cj+00sn8UZ2pW7i0D+pdvx8s8jxPj0BWpjkSy26s2FoMlsKoiRyjC +03oKEiTnsObTOuNoVRpHLstRJXv8lwJWF+UzrKSE29i0SIi1PV9OSGrtAb1XfmlI +5OF7jFCKAPOH0pmOAvzvPHRwzTKaTyHKyo4MIeWloDtwn6QYsJ/+JdmGC1rXt6nW +vJidbxqUzzTrmfTNNZ3a2u6U1QqebZZ+6K4xofAiT/M8AEzABIc8HDOESCPDYOhr +wW3j/FbTJ0Tzwv8Qi6q0GPIdf6c2DiDMg06SUex6jXTKzqC1wwIDAQABo4GDMIGA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSksSy2 +JYWnc6IgdlC9Wgji3H9h9TAfBgNVHSMEGDAWgBSksSy2JYWnc6IgdlC9Wgji3H9h +9TAdBgNVHREEFjAUhhJzcGlmZmU6Ly96aXRpLnRlc3QwDQYJKoZIhvcNAQELBQAD +ggIBAKzYlVK92LGyBgKARm6ULC8TBebvCZCW4y2jhJZy6x/3nxN9lBTb2bARuo5q +KTUogZdI+hw/3z2NDGHrrX/cbYL3OQIewwv7p0xe0YqnHhpGz0jTcvLKimSM9OeZ +ZHf5rDpblQuhDOu37id+nIoQP9/eVMIbteQ+N9Ob9UCdLPE/ZCjPY/aAiPeBEedH +RV7UxRruOIzwsmLsVtwifWyDhaqqPl+5kAftJnjpfRz6i/EZHL3u8N0BUtl+Q0p5 +ibnXUTtpkYNr0JaCmUNnD35adXdXAyNseggWSlhtdhGV9f+NoWU4SV7gPhBKGfs3 +gYcRi1ikUZBrPES30aHmsfrMPan7x6NPC0AMi5XTqp/T+0XT6WVNirnUZOVa66op +zxmBTt55Jeg+Y81THw1Vx78qwlm70JsswPkm61z6hvlZTcjCqvHXXqznCbaoBbFa +FxUUocec5puh3ZwvMsnI56IWQC4eChnAfqFnqoQUBKsaPkEQzzbxaIcQk0s/vPaD +jzpcuCRMsXwB3DpFVOvkIdRbl8QhLEBVc5s6DRFTJQ/shrvcUcAugRqqJgDn3ZFk +Cn+AT5PLGgLFs/ZXupUgRxlfZpGPgWooAxA5bfowKfxf/Wc8gqxNDN3X6mMhImly +HBZvQQKYbLSuaJtXszqNhh9JEoHhZJyJVZ/KNHIKNn3PnyDj +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing1/crlnumber b/tests/testdata/pki/signing1/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing1/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing1/index.txt b/tests/testdata/pki/signing1/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing1/index.txt.attr b/tests/testdata/pki/signing1/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing1/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing1/keys/signing1.key b/tests/testdata/pki/signing1/keys/signing1.key new file mode 100644 index 000000000..3f76626cf --- /dev/null +++ b/tests/testdata/pki/signing1/keys/signing1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 ++zChBm7yqi6IpiHHBzba61+8R4k3OVk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing1/serial b/tests/testdata/pki/signing1/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing1/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing2/certs/signing2.cert b/tests/testdata/pki/signing2/certs/signing2.cert new file mode 100644 index 000000000..61e7d692a --- /dev/null +++ b/tests/testdata/pki/signing2/certs/signing2.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing2/certs/signing2.chain.pem b/tests/testdata/pki/signing2/certs/signing2.chain.pem new file mode 100644 index 000000000..e4e5aa192 --- /dev/null +++ b/tests/testdata/pki/signing2/certs/signing2.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing2/crlnumber b/tests/testdata/pki/signing2/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing2/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing2/index.txt b/tests/testdata/pki/signing2/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing2/index.txt.attr b/tests/testdata/pki/signing2/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing2/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing2/keys/signing2.key b/tests/testdata/pki/signing2/keys/signing2.key new file mode 100644 index 000000000..7b5235ee1 --- /dev/null +++ b/tests/testdata/pki/signing2/keys/signing2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +xaHRILYXaGEXR2iffVG2IXE3jH8skw== +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing2/serial b/tests/testdata/pki/signing2/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing2/serial @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing3/certs/signing3.cert b/tests/testdata/pki/signing3/certs/signing3.cert new file mode 100644 index 000000000..b718a78dd --- /dev/null +++ b/tests/testdata/pki/signing3/certs/signing3.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing3/certs/signing3.chain.pem b/tests/testdata/pki/signing3/certs/signing3.chain.pem new file mode 100644 index 000000000..de3cdee71 --- /dev/null +++ b/tests/testdata/pki/signing3/certs/signing3.chain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIIF9zCCA9+gAwIBAgIQCCUrOfpr/19KXJcTJHmHoDANBgkqhkiG9w0BAQsFADBx +MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpOZXRG +b3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMScwJQYDVQQDEx5aaXRpIFRlc3QgRWRn +ZSBTaWduaW5nIFJvb3QgQ0EwHhcNMjYwNzIwMDgzOTQxWhcNMzYwNzE3MjAzOTQx +WjB1MQswCQYDVQQGEwJVUzESMBAGA1UEBxMJQ2hhcmxvdHRlMRMwEQYDVQQKEwpO +ZXRGb3VuZHJ5MRAwDgYDVQQLEwdBRFYtREVWMSswKQYDVQQDEyJDb250cm9sbGVy +IFRocmVlIEVkZ2UgU2lnbmluZyBDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEArUGi2lD/+fA294s8ozg1GXFl+synegd8f2xkO+UuU08JUfwxyKcD +IOQKVHnMEaz9H+g9/AkeLyL/kpXUtEH2L21Epi5z/eY7FolUz5WkLzF0mh5MDWl6 +C9tVDbJ92XDTSUjR3aejK5S/Gx44xVcoeGGvpSuRMHw9NkHPI0LPTKZcV1B/vdsi +SFFi6yWSEGZI4Pc3Xxw+cSL5tQPx56yjAGLV5kiyf5lnwKyxUHxSjO0xwr+Uxr7i +OQSJih/SsQ0AZYVByPsXohe/bSqWSYdImZzW3DSQAE8jElOQBt8eWuSwgOTa/9vC +mvTN5wb27l2M3YyZtxmjcMIT3Y3XeA7eg3qNK2ENPpUuHzs4xyYwSoP0whnoRUiL +JoRYlzV9sKDcXpNziUv24BhW9fDK2AgFoyFm/5yb0oVr/x8mArdLjRv9+bq0iNh/ +HhzVlvRKl538cBBxHfaszWPTld6tbzrZKN0tLyOV6R24GbCflYLpWJCTv2iZzCfg +YeA8nJWifF0KQe/XI/5mmQsD60/l+m0s2okmwuxnNmS+sDbilRQ89O74+gAJhA6W +G//uujxlNKKmvHPMZCQbd+SjnWKJImMl5My8qq4cajQ9d40+/Rk1MXybGu6wto8a +CpHfWWpSIZv3bnJw4G+4VPukvHOa2HH/cic9nypIPfH1ipGpZiqqN50CAwEAAaOB +hjCBgzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E +FgQU9IKTEWT49s1KUmSkohOgW500MnMwHwYDVR0jBBgwFoAUpLEstiWFp3OiIHZQ +vVoI4tx/YfUwHQYDVR0RBBYwFIYSc3BpZmZlOi8veml0aS50ZXN0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAF/qrlAmvyqZ21qZUT7ZuI0CM56OA/YPz5O4U+CtP6PDrdOGcZ +jGySvq8w1Q5Cw1HnDqUqAsbIaa0KSuyxtP9+zwjRpypjM9AB7zy5ENGn92tSSQkR +6e/xRW6kO2DLtEuXnM/ElN57N8q9HWPk3ArtlshFc8wS1IIafoBCzOHaDZ9DR7V+ +b6nM4Dr5B4FsKYJY5fT9ZtQOYEBK3MRN5lSVdG0gJP/gvGG3foOQqbfdFCGy6S36 +h4BPyRmXREDsulCnu5eaugAPIyxnqSYuhi59sNJxEJIvEcygHMKRrVB23oZ+1pPC +bezGkroDkYeEdRIGwPPoeoSBhHPXdPw/0kpnYiE0WF2Mns5KvLhQjtX+57RTqO6L +43QqhoCkHRTRoNZBZW1yalKIq/hl9PrDKhMgAfIJBjitsC5+MHi32VvxYtuXA/5f +hT0UzMYCZL3jZ5NBKr8RrG8ZURC6oqSgh7eS43uWWvXiCR+yxKuMjajl1YFfodxC +Ai3IoDnlZemx2tS4WMTq+uzqxd+AOb8ocFjNtIhRDRZfoAZOmGwscz3l6PHtJpr4 +/BhkR9ijIvPdBFpgL0GP4ILcAPK4PtoErDchMK1vWeQkT2RWscBSsjv/9z5n++Y7 +1tpDFW3p5oyOT0/adgZEzTepxr/VnyQ1KWenWh5x9iHAq8WszlYaCATBAw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/testdata/pki/signing3/crlnumber b/tests/testdata/pki/signing3/crlnumber new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing3/crlnumber @@ -0,0 +1 @@ +01 diff --git a/tests/testdata/pki/signing3/index.txt b/tests/testdata/pki/signing3/index.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testdata/pki/signing3/index.txt.attr b/tests/testdata/pki/signing3/index.txt.attr new file mode 100644 index 000000000..3a7e39e6e --- /dev/null +++ b/tests/testdata/pki/signing3/index.txt.attr @@ -0,0 +1 @@ +unique_subject = no diff --git a/tests/testdata/pki/signing3/keys/signing3.key b/tests/testdata/pki/signing3/keys/signing3.key new file mode 100644 index 000000000..11c20f3e8 --- /dev/null +++ b/tests/testdata/pki/signing3/keys/signing3.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +CYWpy35FrNj2ymT1QJR+XU2Gn4GzJgg= +-----END PRIVATE KEY----- diff --git a/tests/testdata/pki/signing3/serial b/tests/testdata/pki/signing3/serial new file mode 100644 index 000000000..8a0f05e16 --- /dev/null +++ b/tests/testdata/pki/signing3/serial @@ -0,0 +1 @@ +01 diff --git a/ziti/cmd/pki/pki.go b/ziti/cmd/pki/pki.go index 281953fbd..8a19d7f9e 100644 --- a/ziti/cmd/pki/pki.go +++ b/ziti/cmd/pki/pki.go @@ -64,6 +64,7 @@ type PKIFlags struct { SpiffeID string AllowOverwrite bool EcCurve string + NotBefore string } var ( diff --git a/ziti/cmd/pki/pki_create.go b/ziti/cmd/pki/pki_create.go index 2853fa14d..87b43eae8 100644 --- a/ziti/cmd/pki/pki_create.go +++ b/ziti/cmd/pki/pki_create.go @@ -88,6 +88,8 @@ func (options *PKICreateOptions) addPKICreateFlags(cmd *cobra.Command) { err = options.viper.BindPFlag("pki-province", cmd.PersistentFlags().Lookup("pki-province")) options.panicOnErr(err) + cmd.PersistentFlags().StringVarP(&options.Flags.NotBefore, "not-before", "", "", "Certificate notBefore time as RFC3339 (2006-01-02T15:04:05Z) or date (2006-01-02); defaults to now") + // cmd.PersistentFlags().StringVarP(&options.Flags.PKIProvince, "pki-state", "", "NC", "State/Province") // cmd.MarkFlagRequired("pki-state") // options.viper.BindPFlag("pki-state", cmd.PersistentFlags().Lookup("pki-state")) @@ -281,7 +283,7 @@ func (o *PKICreateOptions) ObtainFileName(caFile string, commonName string) stri } // ObtainPKIRequestTemplate returns the 'template' used in the PKI request -func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) *x509.Certificate { +func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) (*x509.Certificate, error) { subject := pkix.Name{CommonName: commonName} if str := o.viper.GetString("pki-organization"); str != "" { @@ -306,7 +308,27 @@ func (o *PKICreateOptions) ObtainPKIRequestTemplate(commonName string) *x509.Cer MaxPathLen: o.Flags.CAMaxPath, } - return template + if o.Flags.NotBefore != "" { + notBefore, err := parseNotBefore(o.Flags.NotBefore) + if err != nil { + return nil, err + } + template.NotBefore = notBefore + } + + return template, nil +} + +// parseNotBefore parses a certificate notBefore value supplied as either an RFC3339 timestamp or a +// date-only string (interpreted as midnight UTC). +func parseNotBefore(value string) (time.Time, error) { + if t, err := time.Parse(time.RFC3339, value); err == nil { + return t, nil + } + if t, err := time.Parse("2006-01-02", value); err == nil { + return t, nil + } + return time.Time{}, fmt.Errorf("invalid not-before [%s], expected RFC3339 (2006-01-02T15:04:05Z) or date (2006-01-02)", value) } // ObtainKeyName returns the private key from the key-file diff --git a/ziti/cmd/pki/pki_create_ca.go b/ziti/cmd/pki/pki_create_ca.go index a1a44cc32..f668875e5 100644 --- a/ziti/cmd/pki/pki_create_ca.go +++ b/ziti/cmd/pki/pki_create_ca.go @@ -104,7 +104,10 @@ func (o *PKICreateCAOptions) Run() error { commonName := o.Flags.CAName filename := o.ObtainFileName(caFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = true diff --git a/ziti/cmd/pki/pki_create_client.go b/ziti/cmd/pki/pki_create_client.go index fd6437ff9..f66bb2b47 100644 --- a/ziti/cmd/pki/pki_create_client.go +++ b/ziti/cmd/pki/pki_create_client.go @@ -108,7 +108,10 @@ func (o *PKICreateClientOptions) Run() error { } filename := o.ObtainFileName(clientCertFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = false template.EmailAddresses = o.Flags.Email diff --git a/ziti/cmd/pki/pki_create_intermediate.go b/ziti/cmd/pki/pki_create_intermediate.go index e8985117f..c70822d7e 100644 --- a/ziti/cmd/pki/pki_create_intermediate.go +++ b/ziti/cmd/pki/pki_create_intermediate.go @@ -102,7 +102,10 @@ func (o *PKICreateIntermediateOptions) Run() error { commonName := o.Flags.IntermediateName filename := o.ObtainFileName(intermediateFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = true diff --git a/ziti/cmd/pki/pki_create_key.go b/ziti/cmd/pki/pki_create_key.go index 0d5f3b88f..594245e1f 100644 --- a/ziti/cmd/pki/pki_create_key.go +++ b/ziti/cmd/pki/pki_create_key.go @@ -96,7 +96,10 @@ func (options *PKICreateKeyOptions) Run() error { return fmt.Errorf("%s", err) } - template := options.ObtainPKIRequestTemplate("") + template, err := options.ObtainPKIRequestTemplate("") + if err != nil { + return err + } var signer *certificate.Bundle signer, err = options.Flags.PKI.GetCA(caName) diff --git a/ziti/cmd/pki/pki_create_server.go b/ziti/cmd/pki/pki_create_server.go index 5248af371..1672e2dd3 100644 --- a/ziti/cmd/pki/pki_create_server.go +++ b/ziti/cmd/pki/pki_create_server.go @@ -114,7 +114,10 @@ func (o *PKICreateServerOptions) Run() error { } filename := o.ObtainFileName(serverCertFile, commonName) - template := o.ObtainPKIRequestTemplate(commonName) + template, err := o.ObtainPKIRequestTemplate(commonName) + if err != nil { + return err + } template.IsCA = false template.IPAddresses = IPs diff --git a/ziti/pki/pki/template.go b/ziti/pki/pki/template.go index 4dcc30846..786a2cbdf 100644 --- a/ziti/pki/pki/template.go +++ b/ziti/pki/pki/template.go @@ -43,7 +43,9 @@ func defaultTemplate(genReq *Request, publicKey crypto.PublicKey) error { } genReq.Template.SerialNumber = sn - genReq.Template.NotBefore = time.Now().Add(-time.Minute) + if genReq.Template.NotBefore.IsZero() { + genReq.Template.NotBefore = time.Now().Add(-time.Minute) + } return nil } From e7cb15d6324d3cd21bccf34ffee6b4208720a854 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 26 Aug 2026 20:59:37 +0100 Subject: [PATCH 72/73] backport openziti/ziti#4222 to release-v2.0.x report isCertExtendable correctly for OIDC cert auth (#4226) - removes a stray assignment that overwrote isCertExtendable with a literal true on every OIDC certificate authentication - reports isCertExtendable as false over OIDC for cert authenticators not issued by the network, matching legacy cert auth - adds coverage asserting the z_ice claim and current-api-session both report a 3rd party CA certificate as not extendable --- controller/oidc_auth/storage.go | 1 - tests/ca_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/controller/oidc_auth/storage.go b/controller/oidc_auth/storage.go index 3d9a4f689..bb4cb54ca 100644 --- a/controller/oidc_auth/storage.go +++ b/controller/oidc_auth/storage.go @@ -455,7 +455,6 @@ func (s *HybridStorage) Authenticate(authCtx model.AuthContext, id string, confi if certAuth != nil { authRequest.IsCertExtendable = certAuth.IsIssuedByNetwork - authRequest.IsCertExtendable = true authRequest.IsCertKeyRollRequested = certAuth.IsKeyRollRequested authRequest.ImproperClientCertChain = result.ImproperClientCertChain() } diff --git a/tests/ca_test.go b/tests/ca_test.go index 46bdeb0dc..05f2af045 100644 --- a/tests/ca_test.go +++ b/tests/ca_test.go @@ -10,6 +10,7 @@ import ( "github.com/Jeffail/gabs" "github.com/openziti/edge-api/rest_model" + edge_apis "github.com/openziti/sdk-golang/edge-apis" "github.com/openziti/ziti/v2/common/eid" "github.com/openziti/ziti/v2/controller/model" ) @@ -267,6 +268,38 @@ func Test_CA(t *testing.T) { ctx.Req.False(*enrolledSession.AuthResponse.IsCertExtendable, "expected isCertExtendable on 3rd party CA certificate authentication to be false") }) + t.Run("oidc auth from CA should not be extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + certCreds := edge_apis.NewCertCredentials(clientAuthenticator.certs, clientAuthenticator.key) + certCreds.CaPool = ctx.ControllerCaPool() + + clientHelper := ctx.NewEdgeClientApi(nil) + + t.Run("access token reports the certificate as not extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + _, accessClaims, err := clientHelper.OidcAccessToken(certCreds) + ctx.Req.NoError(err) + ctx.Req.False(accessClaims.IsCertExtendable, "expected the z_ice claim on 3rd party CA certificate OIDC authentication to be false") + }) + + t.Run("current api session reports the certificate as not extendable", func(t *testing.T) { + ctx.testContextChanged(t) + + clientHelper.SetUseOidc(true) + + apiSession, err := clientHelper.Authenticate(certCreds, nil) + ctx.Req.NoError(err) + ctx.Req.NotNil(apiSession) + + currentSession, err := clientHelper.GetCurrentApiSessionDetail() + ctx.Req.NoError(err) + ctx.Req.NotNil(currentSession.IsCertExtendable) + ctx.Req.False(*currentSession.IsCertExtendable, "expected isCertExtendable on 3rd party CA certificate OIDC authentication to be false") + }) + }) + t.Run("CAs with auth disabled can no longer authenticate", func(t *testing.T) { ctx.testContextChanged(t) ca.isAuthEnabled = false From 08145805d0831048bf78236945884767b11189f4 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Wed, 26 Aug 2026 21:03:00 +0100 Subject: [PATCH 73/73] Backport.v2.0.x.fix.openziti.ziti.4118.ext jwt kid collision (#4192) * backport openziti/ziti#4118 to release-v2.0.x disambiguate overlapping ext-jwt-signer kids by issuer - binds external JWT tokens to signers by exact issuer claim rather than by key ID, so signers drawing from a shared signing-key pool resolve deterministically - binds controller-issued tokens by key ID first, preserving controller token resolution and preventing an external signer configured with a controller's issuer from capturing controller access tokens - removes the external key-ID fallback so a token whose issuer matches no configured signer is not bound to an unrelated signer that happens to share its kid - adds GetControllerIssuerByKid to the TokenIssuerCache interface and implementation - skips disabled external signers in GetIssuerByKid so a disabled signer sharing a kid cannot poison resolution for an enabled one - adds an integration test with two HTTPS JWKS providers sharing a key and kid, covering the enabled-collision and disabled-poison cases - adds the test PKI files the new test reads (pki/root/certs/root.cert, pki/ctrl1/certs/server.chain.pem, pki/ctrl1/keys/server.key), copied byte-identical from main where they are generated by tests/testdata/create-pki.sh * backport openziti/ziti#4118 to release-v2.0.x clarifies ext-jwt token issuer binding comments and godoc - documents that controller issuers are keyed by controller id and that a controller issuer's key ID is the fingerprint of its TLS certificate - documents that an external kid match is ambiguous because signers can share a signing-key pool, and that a definitive binding requires resolving by issuer claim - clarifies the overlapping-kid test comment covering why issuer-claim binding is required when a disabled signer shares a kid --- common/security_tokens.go | 16 +- controller/model/token_provider_cache.go | 19 +- tests/auth_external_jwt_signer_test.go | 239 ++++++++++++++++++++++- 3 files changed, 269 insertions(+), 5 deletions(-) diff --git a/common/security_tokens.go b/common/security_tokens.go index 5bc55e61a..f35f63cbe 100644 --- a/common/security_tokens.go +++ b/common/security_tokens.go @@ -142,6 +142,12 @@ type TokenIssuerCache interface { // GetIssuerByKid returns the TokenIssuer that owns the given key ID GetIssuerByKid(kid string) TokenIssuer + + // GetControllerIssuerByKid returns the controller TokenIssuer that owns the given key ID, + // or nil if no controller issuer claims that kid. A controller issuer's key ID is the + // fingerprint of its TLS certificate, so this resolves controller-issued tokens by kid + // without consulting external signers. + GetControllerIssuerByKid(kid string) TokenIssuer } // SecurityToken is the result of verifying the primary security token presented on a request. @@ -505,12 +511,20 @@ func (s *SecurityTokenCtx) processHeaders() error { continue } + // Bind controller-issued tokens first by kid. A controller issuer's kid is the + // fingerprint of its TLS certificate, so it does not collide with external signers. + // This must precede the issuer-string lookup so an external signer configured with a + // controller's OIDC issuer URL cannot capture controller access tokens. kid := bearerToken.Kid() if kid != "" { - bearerToken.TokenIssuer = s.tokenIssuerCache.GetIssuerByKid(kid) + bearerToken.TokenIssuer = s.tokenIssuerCache.GetControllerIssuerByKid(kid) } + // Otherwise bind external signers by their exact issuer claim. Binding by kid is not + // used as a fallback: external signers can share a kid (shared signing-key pools), so + // kid resolution is ambiguous, and binding a token whose issuer matches no configured + // signer to an unrelated signer that happens to share the kid would be incorrect. if bearerToken.TokenIssuer == nil { issuer := bearerToken.Issuer() if issuer != "" { diff --git a/controller/model/token_provider_cache.go b/controller/model/token_provider_cache.go index edefa4ef0..272878bc1 100644 --- a/controller/model/token_provider_cache.go +++ b/controller/model/token_provider_cache.go @@ -507,15 +507,30 @@ func (a *TokenIssuerCache) IterateControllerIssuers(f func(issuer common.TokenIs }) } -// GetIssuerByKid searches both external JWT signers and controller issuers for the one -// that owns the given key ID. Returns nil if no issuer claims that kid. +// GetIssuerByKid searches enabled external JWT signers and then controller issuers for the one +// that owns the given key ID. Disabled external signers are skipped. Returns nil if no issuer +// claims that kid. +// +// External signers can share a kid when they draw from a common signing-key pool, so an external +// match is ambiguous and does not identify a token's issuer. Callers needing a definitive binding +// must resolve by issuer claim instead. func (a *TokenIssuerCache) GetIssuerByKid(kid string) common.TokenIssuer { for _, issuer := range a.externalIssuers.Items() { + if !issuer.IsEnabled() { + continue + } if pubKey, ok := issuer.PubKeyByKid(kid); ok && pubKey.PubKey != nil { return issuer } } + return a.GetControllerIssuerByKid(kid) +} + +// GetControllerIssuerByKid returns the controller TokenIssuer that owns the given key ID, or nil if +// no controller issuer claims that kid. A controller issuer's key ID is the fingerprint of its TLS +// certificate, so this resolves controller-issued tokens by kid without consulting external signers. +func (a *TokenIssuerCache) GetControllerIssuerByKid(kid string) common.TokenIssuer { for _, controller := range a.controllerIssuers.Items() { if pubKey, ok := controller.PubKeyByKid(kid); ok && pubKey.PubKey != nil { return controller diff --git a/tests/auth_external_jwt_signer_test.go b/tests/auth_external_jwt_signer_test.go index 1c094d2fa..02c2b84db 100644 --- a/tests/auth_external_jwt_signer_test.go +++ b/tests/auth_external_jwt_signer_test.go @@ -19,11 +19,14 @@ package tests import ( + "crypto" + "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "net" "net/http" + "os" "strconv" "sync" "testing" @@ -53,6 +56,7 @@ type jwksServer struct { mutex sync.Mutex requestCount int listener net.Listener + tlsCert *tls.Certificate } func newJwksServer(certificates []*x509.Certificate) *jwksServer { @@ -67,6 +71,16 @@ func newJwksServer(certificates []*x509.Certificate) *jwksServer { return srv } +// newTlsJwksServer returns a jwksServer that serves its JWKS over HTTPS using tlsCert as its +// server certificate. Callers must ensure the controller trusts tlsCert (see the root CA added +// to http.DefaultTransport in the overlapping-kid test) for the controller's JWKS fetch to succeed. +func newTlsJwksServer(tlsCert *tls.Certificate, certificates []*x509.Certificate) *jwksServer { + srv := newJwksServer(certificates) + srv.tlsCert = tlsCert + srv.server.TLSConfig = &tls.Config{Certificates: []tls.Certificate{*tlsCert}} + return srv +} + func (js *jwksServer) AddCertificate(certificate *x509.Certificate) { js.mutex.Lock() defer js.mutex.Unlock() @@ -87,7 +101,11 @@ func (js *jwksServer) RemoveCertificate(certificate *x509.Certificate) { } func (js *jwksServer) GetJwksUrl() string { - return "http://localhost:" + strconv.Itoa(js.port) + "/jwks" + scheme := "http" + if js.tlsCert != nil { + scheme = "https" + } + return scheme + "://localhost:" + strconv.Itoa(js.port) + "/jwks" } func (js *jwksServer) GetRequestCount() int { @@ -107,7 +125,11 @@ func (js *jwksServer) Start() error { js.port = listener.Addr().(*net.TCPAddr).Port js.mutex.Unlock() go func() { - _ = js.server.Serve(listener) + if js.tlsCert != nil { + _ = js.server.ServeTLS(listener, "", "") + } else { + _ = js.server.Serve(listener) + } }() return nil @@ -1036,3 +1058,216 @@ func Test_Authenticate_External_Jwt(t *testing.T) { }) }) } + +// Test_Authenticate_External_Jwt_Overlapping_Kids reproduces intermittent primary ext-jwt +// authentication failures that occur when multiple external JWT signers expose the same key ID +// (kid), as happens when several signers draw from a shared signing-key pool (e.g. multiple +// Entra tenants using Microsoft's keys). Token-to-issuer binding must be disambiguated by the +// token's iss claim so a shared kid resolves deterministically to the correct signer, and a +// disabled signer that happens to share a kid must not poison resolution for an enabled one. +func Test_Authenticate_External_Jwt_Overlapping_Kids(t *testing.T) { + ctx := NewTestContext(t) + defer ctx.Teardown() + ctx.StartServer() + ctx.RequireAdminManagementApiLogin() + + // The controller fetches JWKS over the default HTTP transport. Trust the test PKI root so the + // controller accepts the HTTPS JWKS providers below, which serve using the controller's own + // server certificate. Restore the prior transport config when the test completes. + rootPem, err := os.ReadFile("testdata/pki/root/certs/root.cert") + ctx.Req.NoError(err) + + rootPool, err := x509.SystemCertPool() + if err != nil || rootPool == nil { + rootPool = x509.NewCertPool() + } + ctx.Req.True(rootPool.AppendCertsFromPEM(rootPem), "expected the test root CA to be added to the trust pool") + + httpTransport := http.DefaultTransport.(*http.Transport) + priorTlsConfig := httpTransport.TLSClientConfig + httpTransport.TLSClientConfig = &tls.Config{RootCAs: rootPool} + defer func() { httpTransport.TLSClientConfig = priorTlsConfig }() + + serverTlsCert, err := tls.LoadX509KeyPair("testdata/pki/ctrl1/certs/server.chain.pem", "testdata/pki/ctrl1/keys/server.key") + ctx.Req.NoError(err) + + adminIdentityId := *ctx.AdminManagementSession.AuthResponse.IdentityID + + t.Run("two enabled signers sharing a kid authenticate deterministically by issuer", func(t *testing.T) { + ctx.testContextChanged(t) + + // One signing key and kid, served by two independent HTTPS JWKS providers - the shared + // signing-key pool scenario. + sharedCert, sharedKey := newSelfSignedCert("shared-jwks-pool-" + uuid.NewString()) + sharedKid := sharedCert.Subject.CommonName + + jwksServer1 := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(jwksServer1.Start()) + defer func() { _ = jwksServer1.Stop() }() + + jwksServer2 := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(jwksServer2.Start()) + defer func() { _ = jwksServer2.Stop() }() + + signer1Iss := "iss-shared-kid-1-" + uuid.NewString() + signer1Aud := "aud-shared-kid-1-" + uuid.NewString() + signer1Endpoint := strfmt.URI(jwksServer1.GetJwksUrl()) + signer1Env := &rest_model.CreateEnvelope{} + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &signer1Endpoint, + Enabled: ToPtr(true), + Name: ToPtr("Overlapping Kid Signer 1 - " + uuid.NewString()), + Issuer: ToPtr(signer1Iss), + Audience: ToPtr(signer1Aud), + }).SetResult(signer1Env).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + signer2Iss := "iss-shared-kid-2-" + uuid.NewString() + signer2Aud := "aud-shared-kid-2-" + uuid.NewString() + signer2Endpoint := strfmt.URI(jwksServer2.GetJwksUrl()) + signer2Env := &rest_model.CreateEnvelope{} + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &signer2Endpoint, + Enabled: ToPtr(true), + Name: ToPtr("Overlapping Kid Signer 2 - " + uuid.NewString()), + Issuer: ToPtr(signer2Iss), + Audience: ToPtr(signer2Aud), + }).SetResult(signer2Env).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{signer1Env.Data.ID, signer2Env.Data.ID}, + }, + }, + }).Patch("/auth-policies/default") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode(), string(resp.Body())) + + // External JWT signer creation events are processed asynchronously, so wait until signer 1's + // JWKS has resolved into the issuer cache before asserting deterministic behavior. + ctx.Req.Eventually(func() bool { + code, err := authenticateWithSignedExtJwt(ctx, signer1Iss, signer1Aud, adminIdentityId, sharedKid, sharedKey) + return err == nil && code == http.StatusOK + }, 10*time.Second, 100*time.Millisecond, "signer 1 should become usable for primary authentication") + + // Tokens are issued only by signer 1 (its iss/aud), but the kid is shared with signer 2. + // Kid-first binding over a non-deterministically ordered map would bind some requests to + // signer 2 and reject them on the issuer mismatch. Repeat enough times that a single wrong + // binding is overwhelmingly likely under the pre-fix behavior. + const attempts = 25 + for i := 0; i < attempts; i++ { + code, err := authenticateWithSignedExtJwt(ctx, signer1Iss, signer1Aud, adminIdentityId, sharedKid, sharedKey) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, code, "attempt %d: a token issued by signer 1 must authenticate regardless of a kid shared with signer 2", i) + } + }) + + t.Run("a disabled signer sharing a kid does not poison an enabled signer", func(t *testing.T) { + ctx.testContextChanged(t) + + // One signing key and kid served by two HTTPS JWKS providers - one signer enabled, one + // disabled. A disabled signer that shares a kid must not capture the binding and suppress + // issuer-string resolution for the enabled signer. + sharedCert, sharedKey := newSelfSignedCert("shared-disabled-poison-" + uuid.NewString()) + sharedKid := sharedCert.Subject.CommonName + + enabledJwksServer := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(enabledJwksServer.Start()) + defer func() { _ = enabledJwksServer.Stop() }() + + disabledJwksServer := newTlsJwksServer(&serverTlsCert, []*x509.Certificate{sharedCert}) + ctx.Req.NoError(disabledJwksServer.Start()) + defer func() { _ = disabledJwksServer.Stop() }() + + enabledIss := "iss-poison-enabled-" + uuid.NewString() + enabledAud := "aud-poison-enabled-" + uuid.NewString() + enabledEndpoint := strfmt.URI(enabledJwksServer.GetJwksUrl()) + enabledEnv := &rest_model.CreateEnvelope{} + resp, err := ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &enabledEndpoint, + Enabled: ToPtr(true), + Name: ToPtr("Poison - Enabled - " + uuid.NewString()), + Issuer: ToPtr(enabledIss), + Audience: ToPtr(enabledAud), + }).SetResult(enabledEnv).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + disabledIss := "iss-poison-disabled-" + uuid.NewString() + disabledAud := "aud-poison-disabled-" + uuid.NewString() + disabledEndpoint := strfmt.URI(disabledJwksServer.GetJwksUrl()) + disabledEnv := &rest_model.CreateEnvelope{} + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.ExternalJWTSignerCreate{ + JwksEndpoint: &disabledEndpoint, + Enabled: ToPtr(false), + Name: ToPtr("Poison - Disabled - " + uuid.NewString()), + Issuer: ToPtr(disabledIss), + Audience: ToPtr(disabledAud), + }).SetResult(disabledEnv).Post("/external-jwt-signers") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusCreated, resp.StatusCode(), string(resp.Body())) + + resp, err = ctx.AdminManagementSession.newAuthenticatedRequest().SetBody(&rest_model.AuthPolicyPatch{ + Primary: &rest_model.AuthPolicyPrimaryPatch{ + ExtJWT: &rest_model.AuthPolicyPrimaryExtJWTPatch{ + Allowed: ToPtr(true), + AllowedSigners: []string{enabledEnv.Data.ID}, + }, + }, + }).Patch("/auth-policies/default") + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, resp.StatusCode(), string(resp.Body())) + + // Wait until the enabled signer's JWKS has resolved into the issuer cache (creation events + // are processed asynchronously) before asserting deterministic behavior. + ctx.Req.Eventually(func() bool { + code, err := authenticateWithSignedExtJwt(ctx, enabledIss, enabledAud, adminIdentityId, sharedKid, sharedKey) + return err == nil && code == http.StatusOK + }, 10*time.Second, 100*time.Millisecond, "the enabled signer should become usable for primary authentication") + + // Tokens are issued by the enabled signer. Kid-first binding resolves to the disabled signer, + // which suppresses issuer-string resolution and then fails primary authentication because + // disabled signers are not accepted. Binding by issuer claim avoids that entirely. + const attempts = 25 + for i := 0; i < attempts; i++ { + code, err := authenticateWithSignedExtJwt(ctx, enabledIss, enabledAud, adminIdentityId, sharedKid, sharedKey) + ctx.Req.NoError(err) + ctx.Req.Equal(http.StatusOK, code, "attempt %d: a token issued by the enabled signer must authenticate even though a disabled signer shares its kid", i) + } + }) +} + +// authenticateWithSignedExtJwt signs an ES256 JWT with the given issuer, audience, subject, and kid +// using key, presents it as a primary ext-jwt bearer token, and returns the resulting HTTP status +// code. It does not assert, leaving the caller to decide what a given status means. +func authenticateWithSignedExtJwt(ctx *TestContext, issuer, audience, subject, kid string, key crypto.PrivateKey) (int, error) { + jwtToken := jwt.New(jwt.SigningMethodES256) + jwtToken.Claims = jwt.RegisteredClaims{ + Audience: []string{audience}, + ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(2 * time.Hour)}, + ID: uuid.NewString(), + IssuedAt: &jwt.NumericDate{Time: time.Now()}, + Issuer: issuer, + NotBefore: &jwt.NumericDate{Time: time.Now()}, + Subject: subject, + } + jwtToken.Header["kid"] = kid + + signed, err := jwtToken.SignedString(key) + if err != nil { + return 0, err + } + + result := &rest_model.CurrentAPISessionDetailEnvelope{} + resp, err := ctx.newAnonymousClientApiRequest().SetResult(result).SetHeader("Authorization", "Bearer "+signed).Post("/authenticate?method=ext-jwt") + if err != nil { + return 0, err + } + + return resp.StatusCode(), nil +}