honeypot

package
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 24 Imported by: 0

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

View Source
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

func CollectHeaders(r *http.Request) map[string]string

CollectHeaders extracts request headers into a string map, skipping Authorization and Cookie for safety.

func ConfigureFakeJWT

func ConfigureFakeJWT(issuer, audience string, accessTTL time.Duration)

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 Err

func Err(msg string) error

Err returns a formatted error for honeypot operations.

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

func GenerateFakeRefresh() (string, error)

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

func IsAutomationUA(ua string) bool

IsAutomationUA checks if the User-Agent string suggests an automated tool.

func LoggingMiddleware

func LoggingMiddleware(alerter *Alerter) func(http.Handler) http.Handler

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

func RedactBody(body string) string

RedactBody replaces password-like fields in a JSON body with "[REDACTED]".

func TrapSigningKey added in v1.0.3

func TrapSigningKey() (string, *rsa.PublicKey, error)

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

func TrapSubject(identity string) (string, error)

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

func NewAlerter(webhookURL string, trapUsers []string, auditLog *audit.Logger) *Alerter

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

func (a *Alerter) Alert(ctx context.Context, event Event)

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

func (a *Alerter) IsTrapUser(identifier string) bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL