Documentation
¶
Overview ¶
Package peerticket verifies short-lived JWTs ("peer tickets") cloudbox issues at `POST /api/v1/ssh/peer-ticket`. The local outpost trades its matrix_elev cookie for one of these tickets, then presents it to a peer outpost on the LAN-direct path. The receiving outpost calls Verify with its stored CloudboxTicketPubkey and, on success, treats the connection as cloudbox-vouched without the X-Periscope-Role header that cloudbox stamps on the matrix-tunnel proxied path.
The cookie itself never leaves the original client ↔ cloudbox channel — only the derived ticket traverses the LAN.
Replay defense is a process-memory LRU keyed by `jti`. 60s TTLs keep the replay window naturally bounded; restart-blast-radius is smaller than the TTL window so persisting jti would be pointless.
Index ¶
Constants ¶
const DefaultJTICap = 4096
DefaultJTICap is the LRU cap. 4096 entries × ~60s TTL bounds memory at ~256KiB worst case — trivial. Lower this if the receiver runs in a heavily memory-constrained environment.
Variables ¶
var ( ErrMalformed = errors.New("peerticket: malformed") ErrBadSignature = errors.New("peerticket: signature invalid") ErrExpired = errors.New("peerticket: expired") ErrNotYetValid = errors.New("peerticket: not yet valid") ErrWrongAudience = errors.New("peerticket: wrong audience") ErrScopeMissing = errors.New("peerticket: required scope missing") ErrReplayed = errors.New("peerticket: jti already consumed") ErrNoPubkey = errors.New("peerticket: no verification key configured") )
Sentinel errors so callers (sshHandler, future capability handlers) can distinguish "this ticket is malformed" from "this ticket is expired" from "this ticket was already consumed."
Functions ¶
func LoadPubkey ¶
LoadPubkey parses a PEM-encoded ed25519 public key (the format cloudbox publishes via /api/register/exchange). Empty input returns (nil, nil) so callers can distinguish "no key configured yet" from "configured but malformed."
Types ¶
type Claims ¶
type Claims struct {
Issuer string // "cloudbox" — informational; not gated on
Audience string // "outpost:<peer agent_name>" — must equal receiver
Subject string // OS user the peer should impersonate
Role string // "user" or "admin" — feeds the X-Periscope-Role-equivalent decision
Scope []string // capability strings the ticket grants ("ssh", "sftp", "backup", …)
ExpiresAt time.Time // hard cutoff (cloudbox issues 60s tickets)
NotBefore time.Time // optional; honored if set
IssuedAt time.Time // informational; not gated on
JTI string // unique-per-issuance; replay-protected by Verifier
}
Claims is the verified peer-ticket payload, projected onto the fields the outpost actually uses. Anything cloudbox adds in the future that we don't read here is silently ignored — fine for additive evolution.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier holds receiver-side state — currently just the replay- protection LRU. One Verifier per process; safe for concurrent use.
func NewVerifier ¶
NewVerifier returns a Verifier ready to use. cap ≤ 0 means use DefaultJTICap.
type VerifyOptions ¶
type VerifyOptions struct {
// Pubkey is the cloudbox ed25519 verification key, loaded from
// FileConfig.CloudboxTicketPubkey via LoadPubkey.
Pubkey ed25519.PublicKey
// ExpectedAudience must match the ticket's `aud` claim exactly.
// The receiver passes its own identity ("outpost:" + AgentName)
// — never a value taken from the request. Prevents an attacker
// who captured a ticket scoped to peer-A from replaying it to
// peer-B.
ExpectedAudience string
// RequiredScope is the capability the receiver is gating right
// now ("ssh" for the WS-SSH handler, "sftp" or "backup" for
// future per-route gates). The ticket's `scope` claim MUST
// contain this value.
RequiredScope string
// ClockSkew widens both the `nbf` and `exp` windows by this
// amount to absorb modest clock drift between cloudbox and the
// receiver. Default 30s when zero.
ClockSkew time.Duration
// Now overrides time.Now() for tests. Zero-valued = real clock.
Now time.Time
}
VerifyOptions threads the receiver-side invariants into Verify. ExpectedAudience and RequiredScope are mandatory — fail-closed on empty values so a misconfigured caller can't accidentally widen trust.