Audience: operators and embedders securing an EventMesh deployment — auth/ACL/quota/audit at the traffic plane, TLS on the wire, and the admin plane's token guard.
SecurityGate (issue #5304) is an opt-in per-request gate installed at every ingress point. When installed, each request flows through one immutable RequestContext and is checked in order:
ingress request
│
▼
SecurityGate.check(RequestContext, EventMeshFrame)
│
├─ 1. FilterChain.invoke(frame) TokenAuthFilter → SignatureVerifierFilter → AclFilter
│ verdict: ALLOW / DENY
├─ 2. if ALLOW → QuotaManager.acquire(ctx, Resource)
│ Resources: CONNECTIONS | SUBSCRIPTIONS | THROUGHPUT | BACKLOG
└─ 3. AuditSink.emit(decision, ctx, frame) LoggingAuditSink (default) or custom
Any rejection short-circuits downstream — the request never touches the storage SPI. Rejections map to HTTP 401 (unauthenticated), 403 (forbidden), 429 (quota exceeded).
RequestContext (builder-built, immutable) carries tenantId, principal, roles, scopes, credential, remoteAddress, source, traceContext, quotaKey, and an Operation enum (PUBLISH / SUBSCRIBE / ACK / CONNECTOR / A2A / ADMIN).FilterChain (TokenAuthFilter, SignatureVerifierFilter, AclFilter) still produces the auth/ACL verdict; new filters drop in without touching the gate.RequestContext.quotaKey() (defaults to tenantId) against a Resource enum — TenantQuotaManager is the in-memory implementation, QuotaManager.unlimited() disables enforcement.AuditSink failure is logged and must not take down the data path.| Ingress | Builder |
|---|---|
Traffic HTTP (/events/*, SSE, WS upgrade) | UniHttpServer#withSecurityGate(gate) |
| A2A gateway | A2AGatewayHttpHandler#withSecurityGate(gate) |
| Connector scheduling | ConnectorScheduler#withSecurityGate(gate) (rejects with ConnectorAccessDeniedException → HTTP 403) |
| Embedder bootstrap | EventMeshApplication#withSecurityGate(gate) — installed into the traffic server at start() |
When no gate is installed the runtime behaves as before (open) — but new endpoints are expected to install one, and embedders can fail closed at boot before serving traffic.
UniIngressService enforces an optional per-topic token bucket (configureTopicRateLimit(topic, capacity, permitsPerSecond)). Exhausted buckets fail publish with RateLimitedException → HTTP 429. Cluster-wide limits are inspectable/adjustable at /admin/ratelimit — see Admin API.
TLS terminates at the traffic HTTP server. Keys (read at boot by EventMeshApplication):
| Key | Default | Purpose |
|---|---|---|
eventmesh.tls.keystore | — | Keystore path; empty = plain HTTP |
eventmesh.tls.keystore.password | — | Keystore password |
eventmesh.tls.truststore | — | Truststore for client certs (mTLS) |
eventmesh.tls.truststore.password | — | Truststore password |
eventmesh.tls.protocol | TLSv1.3 | TLS protocol |
eventmesh.tls.needClientAuth | false | true + truststore → require client certificates |
Embedders can instead call withTls(SSLContext) / withClientAuth(boolean) before start().
Every admin endpoint except /admin/health is wrapped by a bearer-token guard (-Deventmesh.admin.token=<secret>):
Authorization: Bearer <token> → 401.admin_locked — the admin API is fail-closed by default; an operator who never set a token cannot use it accidentally. Only /admin/health (liveness probe) is exempt.MessageDigest.isEqual).| Piece | Location |
|---|---|
| Gate core | eventmesh-runtime/.../security/gate/SecurityGate.java, RequestContext.java, GateDecision.java |
| Quota | security/gate/QuotaManager.java, TenantQuotaManager.java, UnlimitedQuotaManager.java |
| Audit | security/gate/AuditSink.java, LoggingAuditSink.java, DisabledAuditSink.java |
| Auth/ACL filters | eventmesh-runtime/.../security/TokenAuthFilter.java, SignatureVerifierFilter.java, AclFilter.java |
| TLS factory | eventmesh-runtime/.../http/TlsContextFactory.java |
| Admin guard | eventmesh-runtime/.../admin/UniAdminServer.java (guarded() / authorized()) |
| Tests | SecurityGateTest (per-Resource allow/deny, every Operation, short-circuit), TlsIntegrationTest, RateLimitIntegrationTest |