1636 Commits

Author SHA1 Message Date
Paul Lorenz 11897fba7b Merge commit from fork
Fix/peer cert chain validation
2026-07-27 15:12:10 -04:00
Paul Lorenz 3e6f0f2a76 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
2026-07-27 14:06:44 -04:00
Paul Lorenz d9ddd98423 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
2026-07-27 14:06:44 -04:00
Paul Lorenz abf9cf8b1b 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.
2026-07-27 14:06:44 -04:00
Paul Lorenz f347f1a246 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
2026-07-27 14:05:17 -04:00
Paul Lorenz dd54f108b4 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
2026-07-27 14:05:17 -04:00
Paul Lorenz cada8b364a 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
2026-07-27 14:05:17 -04:00
Paul Lorenz 2c60866d3e Add stricter certificate chain validation for peer connections 2026-07-27 14:05:17 -04:00
Paul Lorenz 098bf285aa Copy eventual event data out of bolt memory. For #4108
- copies the eventual event Data out of the bbolt-managed buffer when loading, instead of
  retaining the raw slice
- the data is handed to event listeners asynchronously after the read transaction closes,
  so the retained slice could dangle into reused or remapped mmap memory

(cherry picked from commit 95d6210e22)
2026-07-15 19:14:08 -04:00
Paul Lorenz 1dc6df28ae Copy terminator peer data and instance secret out of bolt memory. Fixes #4108
- copies the terminator PeerData values and InstanceSecret out of the bbolt-managed
  buffer when loading a terminator, instead of retaining the raw slices
- prevents a dangling reference into the mmap: bbolt values are only valid for the life
  of the transaction, and are reused or unmapped once the tx closes or the database file
  is remapped on growth
- fixes a controller SIGSEGV in the create-circuit response encoding, which reads the
  cached terminator peer data and can memmove from a stale address under terminator churn

(cherry picked from commit 0cefe735db)
2026-07-15 19:14:08 -04:00
Paul Lorenz 3187db726a Load revocations when rebuilding the router data model. For #4102
Backport of the BuildAll revocation-load fix (main PR #4103) so the
router-data-model validation reflects revocation presence correctly on this
branch. The test accompanying the main-line change is omitted here because its
test helper does not exist on release-v2.0.x.

- adds BuildRevocations and wires it into InstantStrategy.BuildAll so a rebuilt
  data model includes existing revocations instead of dropping them
- skips already-expired revocations at load time
2026-07-14 17:02:45 -04:00
Paul Lorenz 3943a6a55c Defer xweb initialized signal until config is loaded. Fixes #4070
- moves MarkInitialized from the end of initWeb to after config.Configure runs in Run
- prevents GetApiAddresses from caching an empty address set when an early raft mesh hello arrives before the xweb config has been populated
- ensures a controller no longer permanently advertises empty apiAddresses to its peers after losing this startup race

(cherry picked from commit 5c16993ef984f2ee89b6fed47d60e71258dc0680)
2026-07-02 09:55:43 -04:00
Paul Lorenz eba2afbe06 Prevent enrollment-based privilege escalation to admin identities. Fixes #4049
- denies non-admins the ability to create, read, refresh, or delete an
  enrollment belonging to an admin identity, since an enrollment carries
  the one-time-token/JWT used to enroll as its target identity
- filters admin-identity enrollments out of enrollment lists for non-admin
  callers
- denies access when the target identity or enrollment cannot be loaded
  rather than relying on a downstream handler
- adds a permissions test covering each escalation path plus the
  admin-allowed behavior

Backport to release-v2.0.x of #4013.
2026-06-26 13:18:33 -04:00
Paul Lorenz 8a39abad42 Use MaxTokenDuration for identity revocation lifetime. For #3929
- uses Oidc.MaxTokenDuration() instead of RefreshTokenDuration so the
  revocation lifetime always covers the longest-lived token
2026-06-24 15:42:01 -04:00
Paul Lorenz 30256b9918 Enforce api-session and identity revocations on the router. Fixes #3929
Backport to release-v2.0.x of #3927 (main-line fix in PR #3930).

The router's CheckConnections reaper enforced only JWT expiry, so a revoked OIDC
api-session, or a disabled/deleted identity, kept its live circuits and hosted
terminators until the access token expired. The router now enforces the
RouterDataModel revocations directly, tightening access-loss propagation to the
reaper interval.

- adds a Type field to DataState_Revocation and the raft Revocation command
  proto, mirroring rest_model.RevocationTypeEnum (API_SESSION/IDENTITY/JTI) so the
  management API, OIDC producers, sync, and router enforcement share one
  vocabulary; the common.RevocationType* constants are compile-time bound to the
  enum to prevent drift
- adds an IssuedBefore cutoff so an identity revocation invalidates only sessions
  issued before it; a session re-authenticated after the cutoff survives the
  still-lingering revocation. Persists IssuedBefore on the db and model Revocation
  and carries it (plus the Type) through the single and batched raft marshalling
- adds RouterDataModel.IsApiSessionRevoked and IsIdentityRevoked and enforces both
  in CheckConnections, closing a revoked session's connections
- revokes a deleted or disabled identity's live OIDC sessions via an
  IdentityRevocationConstraint in the db package, run as a store pre-commit
  constraint so the revocation is written in the same transaction as the identity
  change and cannot be skipped (self-contained OIDC JWTs aren't otherwise
  reachable). NewIdentityManager installs it with the revocation type and lifetime
- has the OIDC end-session (TerminateSessionFromRequest) revoke the specific
  api-session named by the z_asid claim, with an identity-scoped fallback; sets
  IssuedBefore on the identity fallback and the management revocation API; adds
  RevocationManager.CreateOrReplace, routed through by both the OIDC paths and the
  management revocation API, so a repeat logout/termination/revocation refreshes
  the cutoff rather than colliding on the reused id. Expiry derives from the
  longest configured token duration via a shared common.MaxTokenDuration helper
- adds tests/revocation_enforcement_oidc_test.go covering api-session revocation
  (and a fresh session staying unaffected), identity disable and delete, and the
  identity cutoff (a post-cutoff session surviving the lingering revocation)
- regenerates edge_cmd.pb.go (protoc-gen-go v1.31.0) and edge_ctrl.pb.go
  (v1.36.11) against the release-v2.0.x base, matching each file's existing
  generator version
2026-06-24 15:42:01 -04:00
Andrew Martinez 3082a34453 backport openziti/ziti#3952 to v2.0 reject invalid externalIdClaim and stop enrollment panic (#3955)
* backport openziti/ziti#3952 to v2.0 reject invalid externalIdClaim and stop enrollment panic

- moves the error check before the locator assignment in Ca.GetExternalId
  so an unsupported matcher/parser no longer nil-derefs and returns HTTP 500
- guards the claim index against negative and out-of-range values and errors
  when a matched claim resolves to an empty string, so an empty externalId is
  never silently accepted nor mapped to an identity
- adds validateExternalIdClaim, rejecting unsupported locations, unsupported
  matcher/parser combos, missing matcher/parser criteria, and negative indexes
  with HTTP 400 at CA create and update time
- applies the validation to both CA create and update, which previously had
  no externalIdClaim validation on the update path
- tests GetExternalId across the matcher/parser matrix for no-panic and
  correct error/value behavior
- tests CA create and update reject invalid externalIdClaim configurations
- tests that an externalIdClaim resolving to empty fails enrollment cleanly

* backport openziti/ziti#3952 to v2.0 address pr review
2026-06-12 13:53:47 -04:00
Paul Lorenz 91edc68409 Set fsm.startIndex on FSM init. Fixes #3860
- adds the missing self.startIndex assignment in BoltDbFsm.Init so
  GetStartRaftIndex() returns the persisted raft index instead of 0,
  which was leaving the RDM's RaftIndexProvider seeded at 0 on every
  restart and reporting a stale index until the next command applied
- adds a regression test that opens an FSM, persists a raft index,
  reopens it, and asserts GetStartIndex reflects the persisted value
- notes the fix in CHANGELOG.md under the ziti/v2 issue list
2026-05-11 19:12:16 -04:00
Paul Lorenz c73ea941dd Tidy duplicate permission types. Remove vestigial info message. Update changelog 2026-05-08 11:34:20 -04:00
Paul Lorenz 7c82e2d2c1 Filter current api session certs by current api session id. Fixes #3855 2026-05-07 14:28:43 -04:00
Paul Lorenz 67d34ef829 Fix permissions check on list controllers in management API. Fixes #3838 2026-05-07 14:28:43 -04:00
Paul Lorenz 8a3ad21365 Fix incorrect permissions check on create db snapshot with path. Fixes #3837 2026-05-07 14:28:43 -04:00
Andrew Martinez 949de99ee4 fixes #3809 support CSR submission during OIDC authentication (#3840)
* fixes #3809 support CSR submission during OIDC authentication

- accepts an optional CSR during OIDC login (all auth methods) and
  signs it into a session-bound certificate with a SPIFFE ID derived
  from the identity and API session
- returns the signed certificate PEM as a top-level "session_cert"
  field in the token endpoint JSON response (CodeExchange, RefreshToken,
  TokenExchange)
- adds cert-binding verification on RefreshToken and TokenExchange:
  if z_cfs is present the peer cert fingerprint must match (strict),
  otherwise falls back to SPIFFE ID verification
- supports cert rotation via csr_pem form parameter on refresh and
  token exchange; replaces the session cert fingerprint while
  preserving the authenticating cert fingerprint
- adds AuthCertFingerprints (z_acfs) claim to track permanent auth
  cert fingerprints separately from rotatable session cert fingerprints
- only the leaf certificate fingerprint is added to z_cfs and z_acfs,
  intermediates are never included
- invalid CSR returns 400 Bad Request in OIDC error format
- propagates updated CustomClaims (including CertFingerprints) from
  access token to renewed refresh token so rotated fingerprints are
  enforced on subsequent refreshes
- adds CertGenerated field to ApiSessionEvent
- advertises OIDC_AUTH_WITH_CSR controller capability when OIDC is
  enabled
- adds unit tests for verifyCertBinding (fingerprint, SPIFFE ID,
  edge cases) and CsrPem field parsing
- adds integration tests for initial CSR auth (updb, cert, ext-jwt),
  cert-binding on refresh/exchange, CSR rotation with cert auth,
  SPIFFE fallback and z_cfs transition, and CSR property forging
  resistance

* address pr concerns

* add z_cfs len tests on junk chain certs

* strip csr subject info, replace w/ santized values

* fix csr rotation rejection/paths during token exchange/refresh
2026-05-07 13:56:48 -04:00
Andrew Martinez 767ff11cfc fixes #3846 OIDC tokens from non-cert auth no longer bind incidental TLS certs (#3848)
- gates OIDC `z_cfs` claim on cert-based primary auth so non-cert sessions don't bind TLS-presented certs
- reorders router api session validation: fingerprint match first, then SPIFFE-on-first-party as fallback
- accepts first-party SPIFFE-Identity match as an acceptance signal (previously fell through to fingerprint check)
2026-05-06 21:41:19 -04:00
dovholuknf 7a6da85fdd add generic 'binding: spa', fix quickstart --home, lint cleanup 2026-05-03 11:08:40 -04:00
Paul Lorenz 82ac2e8060 Rework connect event handling to ensure serialized-per-router handling 2026-04-24 13:22:02 -04:00
Paul Lorenz c44350b3a7 Support ziti agent inspect so ziti processes can be inspected via IPC. Fixes #3824. Fixes #2049
- adds `ziti agent inspect <value>...` CLI that sends an InspectRequest directly to a ziti process over its agent IPC channel and pretty-prints JSON values in the response
- accepts app id 0 on controller and router agent channels so a single inspect command works against any process type
- adds Controller.agentOpInspect, backed by a new InspectionsManager.InspectLocal that runs inspect processing on the local controller only, without fanning out to routers or peer controllers
- extracts the router inspect handler into a new router/inspect package and stores a single shared instance on Router, reused by both the control channel and the agent IPC channel
- adds RouterEnv.GetInspectHandler and RouterEnv.GetXgressListeners so the control channel bind pulls the shared handler from env
- removes the now-redundant InspectRouterEnv interface from handler_ctrl/bind.go
- adds tunnel HandleAgentAsyncOp with support for stackdump and sdk inspect keys
2026-04-24 12:12:02 -04:00
Andrew Martinez 8297a817b7 fixes #3734 enforce client certificate proof-of-possession for OIDC sessions (#3805)
* fixes #3734 enforce client certificate proof-of-possession for OIDC sessions

- adds verifyCertProofOfPossession() in resolveOidcSession() to require
  TLS client cert matching a z_cfs fingerprint or SPIFFE ID when the
  OIDC token was issued with cert bindings
- adds z_cae (CertAllowExpired) claim from auth policy, propagated
  through token issuance and refresh
- adds TrustCache.VerifyClientCert() with tiered pool matching
  (first-party roots, trust anchors, third-party) and TTL cache
- adds WrapIdentityWithCertValidation() on the router to verify client
  certs at the TLS level against RDM PublicKeys
- adds IsFirstPartyCert() on the router to gate SPIFFE ID matching by
  checking whether the cert chains to the controller root CA
- adds VerifySpiffeId() in common/spiffehlp with SpiffeMatchApiSession,
  SpiffeMatchIdentity, and SpiffeMatchNone return types
- adds SPIFFE IDs to OTT and token enrollment certs (/identity/<id>)
- enforces cert expiry by match type: API session certs must be valid,
  fingerprint-matched certs respect z_cae, legacy sessions skip checks
- shallow-copies leaf certs before overriding time fields in all cert
  verification paths to avoid races on shared x509.Certificate pointers
- fixes controllerRootCache setting inited=true before the ctrl channel
  is available, which permanently cached the failure
2026-04-23 12:00:46 -04:00
Andrew Martinez 82e9870cd6 fixes #3806 expose OpenZiti endpoints in OIDC discovery document (#3808)
* fixes #3806 expose OpenZiti endpoints in OIDC discovery document

- adds vendor-specific "openziti_endpoints" field to the
  /.well-known/openid-configuration response
- overrides Discovery() on the OIDC server to wrap the standard config
  with OpenZiti login and MFA endpoint URLs
- advertises password, cert, ext-jwt, totp, totp enrollment, and
  auth query endpoints as absolute URLs derived from the issuer
- adds integration test verifying all openziti_endpoints fields
- adds dual-server integration test confirming endpoint URLs reflect
  the correct issuer when edge-oidc is hosted on multiple bind points

* changelog
2026-04-22 10:54:53 -04:00
Paul Lorenz 0042f6a345 Merge pull request #3810 from openziti/db-explorer-import-pr
Import the storage and ziti-db-explorer repos into the ziti repo
2026-04-21 09:54:19 -04:00
Paul Lorenz 9ac9a4f17a Filter service policies by type="Dial"/type="Bind" in queries. Fixes #3818
- registers the service policy type symbol as a string, so queries match the
  API's "Dial" and "Bind" names rather than the internal int32 ids
- adds a symbol mapper that converts the stored int32 to its PolicyType name
  at query eval time
- updates the posture-checks lookup in EdgeServiceManager, the only direct
  caller that went through GetSymbol, to read the mapped string form
- adds a store test covering type = "Dial" and type = "Bind" filtering
- notes the breaking removal of the undocumented type = 1/type = 2 form in
  the 2.0 deprecation cleanup list
2026-04-20 15:25:37 -04:00
Paul Lorenz 040016993e Fix codespell errors 2026-04-16 09:43:49 -04:00
Paul Lorenz 4df6ae564d Update package paths for newly imported storage and ziti-db-explorer packages 2026-04-16 09:18:55 -04:00
Paul Lorenz 9efd930fbd Merge storage project into controller/storage 2026-04-15 16:17:42 -04:00
Paul Lorenz 596e018e25 Merge pull request #3792 from openziti/specific-router-error-types
Add specific circuit failure error codes. Fixes #3717, #3543, #3364, #2888, #2859, #1580
2026-04-14 17:26:31 -04:00
Andrew Martinez 69885639d3 fixes #3788 return proper OIDC error codes and HTTP status codes (#3790)
- migrates from op.NewProvider() to LegacyServer/RegisterLegacyServer,
  routing all OIDC endpoint errors through op.WriteError which maps
  server_error to HTTP 500 and supports custom status codes via
  op.StatusError
- returns oidc.ErrInvalidClient() from AuthorizeClientIDSecret for
  unknown clients and bad secrets (HTTP 400 with invalid_client)
- returns oidc.ErrInvalidGrant() from parseRefreshToken,
  parseAccessToken, createAccessToken, and renewRefreshToken for
  client-supplied token errors (HTTP 400 with invalid_grant)
- fixes copy-paste bug in parseAccessToken that reported "invalid
  refresh_token" for access token errors
- plain Go errors from server-side failures (identity read, JSON
  marshal, token signing, Raft dispatch) now correctly surface as
  HTTP 500 via WriteError's DefaultToServerError handling
- adds integration tests covering error codes for token endpoint,
  login endpoint, userinfo endpoint, and end_session endpoint
2026-04-14 16:44:06 -04:00
Paul Lorenz 90f32affda Address review comments 2026-04-14 16:09:19 -04:00
Andrew Martinez 6515e3615c fixes #3680 add revocation management API, CLI, and enforcement (#3789)
* fixes #3680 add revocation management API, CLI, and enforcement

- adds Management API endpoints for revocations (POST, GET, LIST) with
  type-aware validation (JTI/API_SESSION require UUID, IDENTITY requires
  existing identity)
- adds CLI commands: ziti edge create revocation identity|api-session|jti
- adds revocation checks to resolveOidcSession in security_ctx.go so the
  REST API returns 401 for revoked OIDC tokens. Previously only
  ValidateAccessToken (router ctrl channel path) checked revocations,
  so revoked tokens still received 200 OK from the management and client
  HTTP APIs
- adds api-session revocation check to ValidateAccessToken, which only
  checked JTI and identity revocations
- adds Type field to Revocation model, store, and protobuf message
- adds integration tests covering CRUD, input validation, and token
  enforcement for all three revocation types
- use release edge-api@v0.28.1
2026-04-14 15:53:03 -04:00
Paul Lorenz 538623ceb6 Add specific circuit failure error codes. Fixes #3717, fixes #3543, fixes #3364, fixes #2888, fixes #2859, fixes #1580
- adds ErrorType constants for rejected-by-application, DNS resolution failed,
  port not allowed, invalid link destination, and resources not available
- adds corresponding CircuitFailureCause strings reported in circuit events
- extracts classifyDialError() in route handler to map dial errors to specific
  error codes using typed errors, syscall constants, and string matching
- detects DNS errors via *net.DNSError and string fallback for ER/T hosted
  services where errors are serialized through the SDK message protocol
- detects resource exhaustion via EMFILE, ENFILE, ENOBUFS syscall errors
- introduces InvalidLinkDestinationError typed error in forwarder package
- adds unit tests covering all 16 classification cases
- adds integration tests for rejected-by-application (SDK host),
  DNS resolution failed, connection refused, and port not allowed
  (ER/T host mode) with circuit event verification
- adds CreateEnrollAndStartTunnelerEdgeRouterWithCfgTweaks to test context
2026-04-14 15:52:49 -04:00
Paul Lorenz f951ac2423 Merge pull request #3763 from openziti/add-revocations-to-rdm-full-state
Include revocations in full router data model state. Fixes #3762
2026-04-14 11:22:08 -04:00
Paul Lorenz 6a3292e804 Set empty path when reserving circuit, to avoid panics. Fixes #3777 2026-04-07 09:25:55 -04:00
Paul Lorenz a785ab41fb Fix potential deadlock if panic while lock held 2026-04-06 09:56:43 -04:00
Paul Lorenz 588cb2350c Fix data race on heartbeat lastResponse and peer logger label
- changes lastResponse from plain int64 to atomic.Int64 in both router
  and peer heartbeat callbacks, fixing a data race between the heartbeat
  response handler and the heartbeat check ticker
- fixes peer heartbeat logger channelType from "router" to "peer"
2026-04-03 17:42:18 -04:00
Paul Lorenz 0766e79526 Fix minor config parsing bugs
- uses already-parsed AdvertiseAddress for cert validation instead of
  re-reading from the raw map, which panicked when the key was absent
- replaces errors.Wrapf(err, ...) with fmt.Errorf where err is nil, so
  routerDataModel.listenerBufferSize validation errors are returned
  instead of silently swallowed
- fixes pfxlog.Logger().Warn() call using %w verb, which is not
  interpolated outside fmt.Errorf
2026-04-03 17:42:18 -04:00
Paul Lorenz 1435676f8b Fix mesh peer signing cert from header being overwritten. Fixes #3757
- prefers the signing cert from the hello header when available, falling
  back to the TLS underlay cert
- applies consistently on both dialer and acceptor sides
- removes the dead append on the acceptor side that was immediately
  overwritten
2026-04-03 17:42:18 -04:00
Paul Lorenz afa3121469 Fix TLS rate limiter timeout check reading from wrong scope. Fixes #3756
- checks for the timeout key in the rateLimiter submap instead of the
  parent tls cfgmap, so a user-specified timeout is no longer silently
  overwritten by the default
2026-04-03 15:22:52 -04:00
Paul Lorenz 438c507a63 Fix commandHandler config read from wrong scope. Fixes #3755
- reads commandHandler from the cluster submap instead of the top-level
  cfgmap, matching all surrounding config reads in the cluster block
2026-04-03 15:22:49 -04:00
Paul Lorenz 33e9115182 Fix swapped HasPrefix args in SPIFFE trust domain check. Fixes #3753
- swaps arguments to strings.HasPrefix so it checks whether the trust
  domain starts with "spiffe://" rather than whether "spiffe://" starts
  with the trust domain
- fixes both the trustDomain and additionalTrustDomains code paths
2026-04-03 15:22:41 -04:00
Paul Lorenz 14cc2695c0 Merge pull request #3748 from openziti/fix-connect-handler-goroutine-leak
Fix connect events handler goroutine leak. Fixes #3746
2026-04-03 15:14:56 -04:00
Paul Lorenz 3f14d3e2cb Merge pull request #3751 from openziti/fix-peer-error-marshalling
Update controller peer error marshalling for app code changes. Fixes #3747
2026-04-03 15:14:39 -04:00
Paul Lorenz f2db0e147d Fix connect events handler goroutine leak. Fixes #3746
- replaces the per-handler goroutine in connectEventsHandler with a shared,
  bounded goroutine pool on AppEnv
- the old design spawned a processEvents goroutine per router connection that
  only exited on application shutdown, leaking a goroutine on every reconnect
- adds ConnectEventsConfig to the controller config with pool tuning options
  (queueSize, minWorkers, maxWorkers, idleTime)
- defaults: queue 16, 0-16 workers, 30s idle timeout
2026-04-02 23:44:43 -04:00