Documentation
¶
Overview ¶
Package autoexclude is the adaptive decryption-exclusion cache: a bounded, TTL-bounded, in-memory learned set of hosts that could not be SSL-inspected, so that subsequent CONNECTs to them can fail open (bypass decryption) instead of breaking. It is the ADAPTIVE half of decryption exclusion; the MANUAL half (operator-authored bypass patterns) lives in internal/sslbypass.
This is PAN-OS's "local SSL decryption exclusion cache" modeled for a multi-tenant forward proxy. Four design choices make it safe to auto-disable inspection for a host based on a runtime signal:
SCOPED KEY. Every entry is keyed by (scope, host), where scope is an explicit policy boundary — the matched decryption profile's identity. A host learned under one fail-open profile is consulted ONLY for sessions matched to that same profile, so one fail-open rule/profile/tenant can never create a bypass consumed by another rule targeting the same host. Host-only keying is NOT policy isolation; the scope is.
CONFIRM-COUNT over distinct CLIENT-EVIDENCE tokens. A host is not excluded on the first failure. The cache holds a PENDING observation per (scope, host, reason) accumulating the distinct client-evidence tokens (authenticated identity when available, else client address — the caller decides; the engine treats the token opaquely) that hit a qualifying failure within a rolling window; only when the count reaches confirmN is the host promoted. A single endpoint therefore cannot self-poison.
The CALLER gates BOTH the learn (Observe) and the read (Contains) on the matched rule's fail-open opt-in. This engine stores and answers; it never decides policy. Critical origins kept on fail-close rules are never learned or consulted, so they are un-poisonable by design.
VOLATILE. In-memory only, never persisted, never synced CP->DP, off every config surface. A restart re-learns cheaply. (Per-node exclusions match PAN-OS's per-firewall local cache.)
Concurrency: an RWMutex guards both maps. Contains (the per-CONNECT hot read, fail-open rules only) takes the READ lock and bumps the per-entry hit counter ATOMICALLY, so concurrent reads on different hosts run fully parallel; writers (Observe/Remove/Clear/evict) take the write lock, which excludes all readers so an entry is never mutated or deleted while a reader holds it.
Index ¶
- Constants
- type Cache
- func (c *Cache) ActiveByScope() map[string]int
- func (c *Cache) Clear() int
- func (c *Cache) Contains(scopeID, host string) (Reason, bool)
- func (c *Cache) Len() int
- func (c *Cache) List() []Entry
- func (c *Cache) Observe(scopeID, scopeName, host string, reason Reason, client string) (promoted bool)
- func (c *Cache) PendingLen() int
- func (c *Cache) Reconfigure(cfg Config)
- func (c *Cache) Remove(scopeID, host string) bool
- func (c *Cache) Stats() Stats
- type Config
- type Entry
- type Reason
- type Stats
Constants ¶
const ( DefaultTTL = 12 * time.Hour DefaultPinnedTTL = 1 * time.Hour DefaultConfirmN = 2 DefaultWindow = 10 * time.Minute DefaultMaxEntries = 4096 )
Defaults. TTL matches the PAN-OS local-cache default (12h) for the server-observed reasons; PinnedTTL is shorter because the client signal is the spoofable class. ConfirmN=2 distinct client-evidence tokens blocks single-endpoint poisoning while letting a real fleet-wide incompatibility promote quickly. Window bounds how long partial observations accumulate.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is the learned-exclusion store. The zero value is NOT ready; use New.
func (*Cache) ActiveByScope ¶ added in v1.0.123
ActiveByScope returns the count of currently-active (non-expired) exclusions per scope ID — the per-scope breakdown behind the culvert_decrypt_autoexclude_active{scope} gauge (F6). Scope cardinality is bounded by the admin-created decryption-profile set, so no label cap is needed. A scope with only expired entries is omitted (its live count is 0).
func (*Cache) Clear ¶
Clear evicts every active exclusion and pending observation. Returns the number of active entries removed.
func (*Cache) Contains ¶
Contains reports whether (scopeID, host) is actively excluded, returning the learn reason. On a hit it increments that entry's blast-radius hit counter. Expired entries read as absent (lazy — physical removal happens in evict/List).
func (*Cache) List ¶
List returns a stable, expired-filtered snapshot of active exclusions, sorted by learnedAt (newest first) for a predictable UI ordering.
func (*Cache) Observe ¶
func (c *Cache) Observe(scopeID, scopeName, host string, reason Reason, client string) (promoted bool)
Observe records a qualifying inspect failure for (scopeID, host) under reason, attributing it to the opaque distinct-evidence token `client` (the caller derives it — authenticated identity preferred, else client address). It reports whether this observation PROMOTED the (scope, host) to an active exclusion (promoted=true is the security-relevant "inspection just went dark" event — the caller fires the audit/alert/metric on it). If already actively excluded, it is a no-op returning false. An empty scopeID or host is ignored (fail-safe).
func (*Cache) PendingLen ¶
PendingLen is the current count of in-progress (unconfirmed) observations.
func (*Cache) Reconfigure ¶ added in v1.0.105
Reconfigure atomically applies new tunables to a LIVE cache, preserving learned entries. It is the runtime seam for admin-editable tunables (F10 / ADR-0010): it mutates the five scalar tunables IN PLACE under the write lock and NEVER replaces the cache pointer, so the mutex alone makes it race-free against concurrent Observe/Contains (which take the same lock) — a reader can never observe a half-applied configuration. It never calls out to external code while holding the lock, and it can never partially apply: all fields are resolved to locals first, then assigned together.
Semantics (deliberate, per ADR-0010):
- Each field <= 0 resolves to its Default* (identical to New), so a zeroed field means "reset this tunable to default".
- pinnedTTL is clamped to <= ttl DEFENSIVELY. This is the engine's last line of defense: the API layer (F10 PR3) also validates the merged set, but the engine guarantees a valid state for ANY caller — the spoofable client_pinned class can never outlive the server-observed class even if outer validation is bypassed.
- maxPending is re-established to maxEntries (the New invariant), so the pending bound never desyncs from the active cap.
- TTL / PinnedTTL changes are FORWARD-ONLY (ADR-0010 Model A): existing active entries KEEP their already-computed ExpiresAt; a new TTL applies only to entries promoted AFTER this call. This avoids retroactively moving live expiry times (no coverage blip, clean rollback). To shorten or drop EXISTING exclusions immediately, the operator uses Remove/Clear — TTL is forward policy.
- If maxEntries is lowered below the current active count, the excess is evicted IMMEDIATELY and DETERMINISTICALLY (evictLess: oldest by learnedAt, ties by key) down to EXACTLY the new cap; the pending map is likewise bounded to the new maxPending. Eviction only ever RE-ENABLES inspection (fail-closed).
- cfg.Now is IGNORED: Reconfigure tunes parameters, not the clock.
type Config ¶
type Config struct {
TTL time.Duration
PinnedTTL time.Duration
ConfirmN int
Window time.Duration
MaxEntries int
// Now is injectable for deterministic tests; nil ⇒ time.Now.
Now func() time.Time
}
Config parameterizes a Cache. Zero fields fall back to the Default* constants.
type Entry ¶
type Entry struct {
ScopeID string `json:"scope_id"` // decryption-profile identity that owns this exclusion
ScopeName string `json:"scope_name"` // human-readable scope (profile name) for the UI/audit
Host string `json:"host"`
Reason Reason `json:"reason"`
LearnedAt time.Time `json:"learned_at"`
ExpiresAt time.Time `json:"expires_at"`
// Hits counts sessions that bypassed inspection because of this entry — the
// blast-radius signal a security team triages against.
Hits int64 `json:"hits"`
// ClientCount is how many distinct client-evidence tokens were observed
// failing before promotion (provenance for the learn decision).
ClientCount int `json:"client_count"`
}
Entry is one active exclusion (a (scope, host) inspection is currently OFF for).
type Reason ¶
type Reason string
Reason classifies WHY a host was learned. The set is bounded (safe as a metric label). Only these are ever learned; an untrusted/expired origin cert and every generic/ambiguous origin-controlled TLS alert are dropped by the caller's classifier, because auto-bypassing them is an exfil vector, not a compat fix.
const ( // ReasonClientCertRequired is the origin-demanded-a-client-certificate reason: // the origin sent a CertificateRequest we cannot satisfy (a specific, // structured TLS signal — the one origin-leg reason allowed to live-rescue). ReasonClientCertRequired Reason = "client_cert_required" // ReasonUnsupportedParams is a genuine TLS-parameter incompatibility detected // LOCALLY by our own stack (no supported version overlap / no cipher overlap). // Learn-only: it enters pending learning but never live-rescues the triggering // session (it is lower-confidence than client-cert-required). ReasonUnsupportedParams Reason = "unsupported_params" // ReasonClientPinned is the client-rejected-our-forged-leaf reason (a pinned // app). Spoofable from the client side, so it is the reason most reliant on // the confirm-count and gets the shorter TTL. Learn-only. ReasonClientPinned Reason = "client_pinned" )
func AllReasons ¶ added in v1.0.115
func AllReasons() []Reason
AllReasons returns a copy of the canonical bounded reason set. Exported so callers that must bound an untrusted Reason to the closed vocabulary (the ADR-0011 decryption-observability projection) can check membership without duplicating the list — keeping them drift-free if a reason is ever added. Returns a fresh slice so the canonical set stays immutable.
type Stats ¶
type Stats struct {
Active int `json:"active"`
Pending int `json:"pending"`
ConfirmN int `json:"confirm_n"`
TTLSecs int `json:"ttl_secs"`
PinnedSecs int `json:"pinned_ttl_secs"`
WindowSecs int `json:"window_secs"`
MaxEntries int `json:"max_entries"`
}
Stats reports the cache posture for the read-only governance/API surface, so an operator can prove the feature's configuration (and that a no-fail-open deployment has an inert, empty cache).