Documentation
¶
Overview ¶
Package honeypot provides threat observation capabilities for the Vault's honeypot deployment profile. It detects trap credential usage, suspicious request patterns, and dispatches alerts via webhook.
Index ¶
- Constants
- func CollectHeaders(r *http.Request) map[string]string
- func ConfigureFakeJWT(issuer, audience string, accessTTL time.Duration)
- func Err(msg string) error
- func GenerateFakeJWTForIdentity(caller TrapCaller) (string, error)
- func GenerateFakeRefresh() (string, error)
- func IsAutomationUA(ua string) bool
- func LoggingMiddleware(alerter *Alerter) func(http.Handler) http.Handler
- func RedactBody(body string) string
- func TrapSigningKey() (string, *rsa.PublicKey, error)
- func TrapSubject(identity string) (string, error)
- type Alerter
- type Event
- type TrapCaller
Constants ¶
const ( // EventCredentialPresented is a caller spending something they believe is a // credential against the trap. // // The value is an audit event name, not a credential. gosec matches on the // identifier rather than the contents, and an event that reports a // credential being presented cannot be named without the word. EventCredentialPresented = "honeypot_credential_presented" // #nosec G101 -- audit event name, not a credential // EventTrapTokenReplayed is a token this process minted arriving back at it. // It is not an inference about intent: the bait was taken and spent. // // The value is an audit event name, not a token. gosec matches the // identifier rather than the contents, and every event name this package // raises has to say what happened, so the word is not removable. EventTrapTokenReplayed = "honeypot_trap_token_replayed" // #nosec G101 -- audit event name, not a credential )
The event types the HTTP surface raises, and what each one is worth.
Choosing what a deception surface alerts on is the whole design problem. Every request that arrives is by definition unexpected -- nobody has a reason to visit a trap -- so "alert on the unexpected" degenerates into alerting on all of it, which is an amplifier the attacker points at the operator's own channel and which buries the first alert worth reading under a week of internet background scanning. Alerting on none of it is what shipped.
What separates the two is whether the caller has spent something. Scanning is free and constant. Presenting a credential is not: it means the caller has stopped enumerating and is spending a value they obtained somewhere, and on a honeypot there is no legitimate user for that value to belong to. That is exceptional on a single occurrence, which is what makes it a threshold-one rule rather than a windowed count.
Volume-shaped detection -- one source failing over and over, one subject attacked from everywhere -- is deliberately not built here. It is a windowed counter with per-class thresholds and a cooldown, it belongs to the whole service rather than to the honeypot profile, and a second one built inside this package would be the copy that has to be deleted later.
Variables ¶
This section is empty.
Functions ¶
func CollectHeaders ¶
CollectHeaders extracts request headers into a string map, skipping Authorization and Cookie for safety.
func ConfigureFakeJWT ¶
ConfigureFakeJWT publishes the iss, aud and access-token lifetime every trap token must carry. Must be called once at startup before the server begins accepting requests. Safe for concurrent use; subsequent calls are no-ops.
The lifetime is here because the login response quotes the configured TTL as expires_in while the token carried a hardcoded fifteen minutes. The two agreed only on a deployment that had never set VAULT_ACCESS_TOKEN_TTL.
func GenerateFakeJWTForIdentity ¶ added in v1.0.3
func GenerateFakeJWTForIdentity(caller TrapCaller) (string, error)
GenerateFakeJWTForIdentity mints the access token a trap credential is answered with.
The claims are the struct the real token service signs and the signature comes from the same SignToken call, so the two agree on which claims exist, on their JSON types, on the order they are emitted in, and on the header. Assembling the payload from a map instead put the claims in alphabetical order, spelled aud as a bare string where a real token always spells it as an array, and omitted nbf, jti, scopes, the fingerprint and token_type outright. vault42 is public source, so any of those is a complete answer to "is this the honeypot" from one base64 decode of the token the trap just handed over.
func GenerateFakeRefresh ¶
GenerateFakeRefresh creates a random hex string that looks like a real refresh token. The real one is crypto.RandomToken(32), which is the same 32 bytes of hex.
func IsAutomationUA ¶
IsAutomationUA checks if the User-Agent string suggests an automated tool.
func LoggingMiddleware ¶
LoggingMiddleware wraps an HTTP handler to log every request and response in honeypot mode, and raises an alert for the requests that deserve one.
Every request is logged; that record is the threat-analysis surface and is unbounded on purpose. Only credential presentation is alerted, and only within a budget. See the event constants above for why that is the line.
func RedactBody ¶
RedactBody replaces password-like fields in a JSON body with "[REDACTED]".
func TrapSigningKey ¶ added in v1.0.3
TrapSigningKey returns the key id and public half of the honeypot-only signing key so startup wiring can publish it in the JWKS the trap serves.
Calling it at startup also pays the RSA generation cost before the first attacker request rather than inside it, where a first login several hundred milliseconds slower than every later one is its own signal.
func TrapSubject ¶ added in v1.0.3
TrapSubject returns the user id a trap identity is answered with, on every login, for as long as the process lives.
sub is an account id: one address has one and keeps it. jti is the claim that is fresh per token. Drawing sub from the CSPRNG per mint meant two logins with one planted credential came back as two different accounts, which no real deployment does and which an attacker reads off two requests.
The trap path uses it as the user id for its own database round trips too, so the id in the token and the id the honeypot looked up are one value.
Types ¶
type Alerter ¶
type Alerter struct {
// contains filtered or unexported fields
}
Alerter sends honeypot alerts via webhook and logs them to the audit trail.
func NewAlerter ¶
NewAlerter creates a honeypot alerter. The trapUsers slice is normalized to lowercase for case-insensitive matching. The webhookURL must use https:// or http:// scheme; invalid URLs are silently dropped (alerts will only be logged).
func (*Alerter) Alert ¶
Alert sends a JSON POST to the webhook URL with attack details and logs an audit event. Webhook dispatch is best-effort — errors are logged but do not propagate.
func (*Alerter) IsTrapUser ¶
IsTrapUser checks if the given identifier (email or username) matches a configured trap account.
type Event ¶ added in v1.0.3
type Event struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Email string `json:"email,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
RequestBody string `json:"request_body,omitempty"`
RiskScore int `json:"risk_score"`
}
Event contains details about a suspicious activity detected in honeypot mode.
type TrapCaller ¶ added in v1.0.3
type TrapCaller struct {
// Identity is the address the caller logged in with. It fixes sub.
Identity string
// ClientID is the client_id they sent. A real token echoes it and omits the
// claim entirely when none was sent, so a trap token that always omits it is
// a payload one member short of the real thing for any caller who sends one.
ClientID string
// Fingerprint is crypto.ComputeFingerprint over the request. A real one moves
// when the caller's IP or User-Agent moves; one value for every client the
// process ever answers says the issuer never looked at the request.
Fingerprint string
}
TrapCaller is what the trap login path knows about the caller it is answering, and it is exactly what a real access token records about them.