webhook

package module
v0.1.13 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package webhook provides a batched HTTP webhook audit.Output implementation with retry, SSRF prevention, and graceful shutdown.

Security

HTTPS is required by default. [Config.AllowInsecureHTTP] MUST NOT be set to true in production — plaintext HTTP exposes credentials in request headers to network observers. Private and loopback IP ranges are blocked unless [Config.AllowPrivateRanges] is explicitly enabled.

Batching and Delivery

Events are buffered in memory and flushed as newline-delimited JSON (application/x-ndjson) when the batch reaches [Config.BatchSize] events or [Config.FlushInterval] elapses. Failed batches are retried with exponential backoff up to [Config.MaxRetries] times. Delivery semantics are at-least-once: a batch may be delivered more than once if the server accepts the payload but the acknowledgement is lost.

Construction

out, err := webhook.New(&webhook.Config{
    URL:       "https://ingest.example.com/audit",
    BatchSize: 50,
    Timeout:   15 * time.Second,
}, nil) // optional audit.Metrics for pipeline delivery reporting

Recommended import alias:

import auditwebhook "github.com/axonops/audit/webhook"

Index

Examples

Constants

View Source
const (
	// DefaultBatchSize is the default maximum events per batch.
	DefaultBatchSize = 100

	// DefaultFlushInterval is the default maximum time between
	// batch flushes.
	DefaultFlushInterval = 5 * time.Second

	// DefaultTimeout is the default HTTP request timeout.
	DefaultTimeout = 10 * time.Second

	// DefaultMaxRetries is the default retry count for 5xx/429.
	DefaultMaxRetries = 3

	// DefaultBufferSize is the default internal buffer capacity.
	DefaultBufferSize = 10_000

	// MaxBatchSize is the upper bound for BatchSize.
	MaxBatchSize = 10_000

	// MaxBufferSize is the upper bound for BufferSize.
	MaxBufferSize = 1_000_000

	// MaxMaxRetries is the upper bound for MaxRetries.
	MaxMaxRetries = 20

	// DefaultMaxBatchBytes is the default maximum accumulated batch
	// payload size in bytes before a flush is triggered. 1 MiB
	// matches [loki.DefaultMaxBatchBytes] and [syslog.DefaultMaxBatchBytes].
	// Events exceeding this threshold alone trigger an immediate
	// flush (the event is sent in its own HTTP POST; it is never
	// dropped).
	DefaultMaxBatchBytes = 1 << 20 // 1 MiB

	// MinMaxBatchBytes is the lower bound for MaxBatchBytes.
	MinMaxBatchBytes = 1 << 10 // 1 KiB

	// MaxMaxBatchBytes is the upper bound for MaxBatchBytes. Matches
	// the range accepted by Loki and syslog; real-world endpoints
	// typically reject request bodies larger than a few MiB anyway.
	MaxMaxBatchBytes = 10 << 20 // 10 MiB

	// DefaultMaxEventBytes is the default per-event size cap at
	// [Output.Write] entry (#688). Events with payload byte length
	// exceeding this are rejected with [audit.ErrEventTooLarge].
	// 1 MiB matches loki and syslog.
	DefaultMaxEventBytes = 1 << 20 // 1 MiB

	// MinMaxEventBytes is the lower bound for MaxEventBytes.
	MinMaxEventBytes = 1 << 10 // 1 KiB

	// MaxMaxEventBytes is the upper bound for MaxEventBytes.
	MaxMaxEventBytes = 10 << 20 // 10 MiB
)

Default values for Config fields.

Variables

This section is empty.

Functions

func NewFactory

func NewFactory(factory audit.OutputMetricsFactory) audit.OutputFactory

NewFactory returns an audit.OutputFactory that creates webhook outputs from YAML configuration and wires per-output metrics via the supplied audit.OutputMetricsFactory. When factory is non-nil, the returned audit.Output receives its per-output audit.OutputMetrics via WithOutputMetrics at construction time. Pass nil to disable per-output metrics.

Signature is identical to the other output modules' `NewFactory` (file, syslog, loki) for consistency (#581).

Types

type Config

type Config struct {
	// URL is the HTTP endpoint to POST batched events to.
	// REQUIRED. MUST be https:// unless [AllowInsecureHTTP] is true.
	URL string

	// Headers are custom HTTP headers added to every request.
	// Common use: "Authorization: Bearer <token>" or
	// "Authorization: Splunk <token>".
	//
	// Header NAMES whose lower-case form contains any of "auth",
	// "key", "secret", or "token" have their VALUES replaced with
	// "[REDACTED]" in [Config.String], [Config.GoString], and
	// [Config.Format] output — a defence-in-depth safety net against
	// `fmt.Sprintf("%+v", cfg)` and `slog.Debug("cfg", cfg)` leakage.
	// Header names themselves are NOT redacted — they appear in full
	// so that operators can identify which headers are configured.
	//
	// Header names must not contain CRLF. Values are sent verbatim in
	// the HTTP request regardless of redaction in debug output.
	Headers map[string]string

	// TLSCA is the path to a custom CA certificate for the webhook
	// endpoint. When empty, the system root CA pool is used.
	TLSCA string

	// TLSCert is the path to a client certificate for mTLS.
	// Both TLSCert and TLSKey must be set for client authentication.
	TLSCert string

	// TLSKey is the path to the client private key for mTLS.
	// Both TLSCert and TLSKey must be set for client authentication.
	TLSKey string

	// TLSPolicy controls TLS version and cipher suite policy. When nil,
	// the default policy (TLS 1.3 only) is used. See [audit.TLSPolicy] for
	// details on enabling TLS 1.2 fallback.
	TLSPolicy *audit.TLSPolicy

	// FlushInterval is the maximum time between batch flushes.
	// The timer resets after every flush (batch-size or timer
	// triggered). Zero defaults to [DefaultFlushInterval] (5s).
	FlushInterval time.Duration

	// Timeout is the HTTP request timeout covering the full
	// request/response lifecycle including body read.
	// Zero defaults to [DefaultTimeout] (10s).
	//
	// The transport-level [http.Transport.ResponseHeaderTimeout] is
	// derived as `max(Timeout/2, 1*time.Second)` — the 1-second floor
	// prevents a misconfigured short Timeout (for example 1 ms) from
	// producing a per-stage timeout too small to complete a real TLS
	// handshake and server response (#485).
	Timeout time.Duration

	// BatchSize is the maximum events per HTTP request.
	// Zero defaults to [DefaultBatchSize] (100).
	// Values above [MaxBatchSize] (10,000) are rejected.
	BatchSize int

	// MaxBatchBytes is the maximum accumulated payload size (sum of
	// event byte lengths) in a single batch. When the threshold is
	// reached, the batch flushes immediately regardless of
	// [Config.BatchSize]. Zero defaults to [DefaultMaxBatchBytes]
	// (1 MiB). Values below [MinMaxBatchBytes] (1 KiB) or above
	// [MaxMaxBatchBytes] (10 MiB) cause [New] to return an error
	// wrapping [audit.ErrConfigInvalid].
	//
	// A single event exceeding MaxBatchBytes is flushed alone — it
	// is never dropped. Matches the conventions established by
	// [loki.Config.MaxBatchBytes] and
	// [github.com/axonops/audit/syslog.Config.MaxBatchBytes].
	MaxBatchBytes int

	// MaxEventBytes is the maximum byte length accepted by
	// [Output.Write] for a single event. Events exceeding this cap
	// are rejected with [audit.ErrEventTooLarge] wrapping
	// [audit.ErrValidation] and [audit.OutputMetrics.RecordDrop] is
	// called. Zero defaults to [DefaultMaxEventBytes] (1 MiB).
	// Values below [MinMaxEventBytes] (1 KiB) or above
	// [MaxMaxEventBytes] (10 MiB) cause [New] to return an error
	// wrapping [audit.ErrConfigInvalid]. Introduced by #688 as a
	// defence against consumer-controlled memory pressure.
	MaxEventBytes int

	// BufferSize is the internal async buffer capacity. When full,
	// new events are dropped and [audit.OutputMetrics.RecordDrop] is called.
	// Zero defaults to [DefaultBufferSize] (10,000).
	// Values above [MaxBufferSize] (1,000,000) are rejected.
	BufferSize int

	// MaxRetries is the retry count for 5xx and 429 responses.
	// Zero defaults to [DefaultMaxRetries] (3).
	// Values above [MaxMaxRetries] (20) are rejected.
	MaxRetries int

	// AllowInsecureHTTP permits http:// URLs. Default: false.
	// MUST NOT be set to true in production. Plaintext HTTP exposes
	// credentials in request headers (including Authorization tokens)
	// to network observers. Use only for local development and testing.
	AllowInsecureHTTP bool

	// AllowPrivateRanges disables SSRF protection for private and
	// loopback IP ranges. Default: false. Enable for webhooks on
	// private networks. Cloud metadata (169.254.169.254) remains
	// blocked regardless.
	AllowPrivateRanges bool
}

Config holds configuration for Output.

Example (Basic)
package main

import (
	"fmt"
	"time"

	"github.com/axonops/audit/webhook"
)

func main() {
	// HTTPS webhook — the minimum production configuration.
	cfg := &webhook.Config{
		URL:       "https://ingest.example.com/audit",
		BatchSize: 50,
		Timeout:   15 * time.Second,
	}
	fmt.Printf("url=%s batch=%d timeout=%s\n", cfg.URL, cfg.BatchSize, cfg.Timeout)
}
Output:
url=https://ingest.example.com/audit batch=50 timeout=15s
Example (Retry)
package main

import (
	"fmt"
	"time"

	"github.com/axonops/audit/webhook"
)

func main() {
	// Webhook with custom retry and flush settings.
	cfg := &webhook.Config{
		URL:           "https://ingest.example.com/audit",
		BatchSize:     100,
		FlushInterval: 10 * time.Second,
		MaxRetries:    5,
		Timeout:       30 * time.Second,
	}
	fmt.Printf("url=%s batch=%d flush=%s retries=%d timeout=%s\n",
		cfg.URL, cfg.BatchSize, cfg.FlushInterval, cfg.MaxRetries, cfg.Timeout)
}
Output:
url=https://ingest.example.com/audit batch=100 flush=10s retries=5 timeout=30s
Example (Tls)
package main

import (
	"fmt"

	"github.com/axonops/audit/webhook"
)

func main() {
	// Webhook with mTLS client certificate and bearer token.
	// Header values are plain strings — use os.Getenv for secrets.
	cfg := &webhook.Config{
		URL:     "https://ingest.example.com/audit",
		TLSCert: "/etc/audit/client-cert.pem",
		TLSKey:  "/etc/audit/client-key.pem",
		TLSCA:   "/etc/audit/ca.pem",
		Headers: map[string]string{
			"Authorization": "Bearer my-token",
		},
	}
	fmt.Printf("url=%s cert=%s key=%s ca=%s headers=%d\n",
		cfg.URL, cfg.TLSCert, cfg.TLSKey, cfg.TLSCA, len(cfg.Headers))
}
Output:
url=https://ingest.example.com/audit cert=/etc/audit/client-cert.pem key=/etc/audit/client-key.pem ca=/etc/audit/ca.pem headers=1

func (Config) Format added in v0.1.2

func (c Config) Format(f fmt.State, _ rune)

Format writes the redacted representation to the formatter. This prevents credential leakage via %+v and all other format verbs.

func (Config) GoString added in v0.1.2

func (c Config) GoString() string

GoString returns the same redacted representation as Config.String. This prevents credential leakage when configs are formatted via %#v.

func (Config) String

func (c Config) String() string

String returns a human-readable representation of the config with credentials redacted. This prevents credential leakage when configs are accidentally logged via %v or %+v.

URL is sanitised to scheme+host only — path, query, and fragment are dropped (common token placements: Slack /services/.../<TOKEN>, Datadog ?dd-api-key=, Splunk HEC ?token=). Header values for names matching the credential-name rule described on [Config.Headers] are replaced with [REDACTED]. Network traffic itself is unaffected; this is a debug-log safety net, not the primary defence.

type Option added in v0.1.11

type Option func(*options)

Option configures a webhook Output at construction time. Options are passed as variadic arguments to New and applied in order before any configuration validation, TLS setup, or warning emission.

func WithDiagnosticLogger added in v0.1.11

func WithDiagnosticLogger(l *slog.Logger) Option

WithDiagnosticLogger routes construction-time and runtime warnings (TLS policy, connection, retry, and buffer-full drop notices) to the given logger. When nil or not supplied, warnings go to slog.Default.

Consumers normally do not call this directly when using github.com/axonops/audit/outputconfig.Load — outputconfig plumbs the auditor's diagnostic logger into every output it constructs. Use this option when constructing a webhook output programmatically and you want its warnings to match your application's log handler.

The option mirrors audit.WithDiagnosticLogger at the auditor level; the same logger may be passed to both for consistent routing.

func WithOutputMetrics added in v0.1.12

func WithOutputMetrics(m audit.OutputMetrics) Option

WithOutputMetrics sets the audit.OutputMetrics sink for this output. When omitted or nil, metrics calls become no-ops via audit.NoOpOutputMetrics. Mirrors WithDiagnosticLogger in usage and zero-value semantics.

Consumers normally do not call this directly when using github.com/axonops/audit/outputconfig.Load — outputconfig wires per-output metrics through the audit.OutputMetricsFactory supplied via outputconfig.WithOutputMetricsFactory.

type Output

type Output struct {
	// contains filtered or unexported fields
}

Output sends batched audit events to an HTTP endpoint with retry, SSRF prevention, and graceful shutdown.

See the package-level architecture comment for the two-buffer design. Events are formatted as line-delimited JSON (application/x-ndjson).

Retry

On HTTP 5xx or 429, the batch is retried with exponential backoff and jitter (100ms to 5s). On 4xx (other than 429), the batch is dropped immediately. On retry exhaustion, the batch is dropped and audit.OutputMetrics.RecordDrop is called for each event.

SSRF Prevention

The HTTP client uses audit.NewSSRFDialControl to block connections to private, loopback, link-local, and cloud metadata addresses. Redirects are rejected entirely. Keep-alives are disabled to force fresh DNS resolution per request, preventing DNS rebinding.

Redirects Not Supported

Redirects (301/302/303/307/308 with a Location header) are always rejected by net/http.Client.CheckRedirect — following a redirect would reopen the SSRF surface. For any other 3xx response reaching the response-drain path, the body is drained at most 4 KiB so an attacker-controlled endpoint cannot force up to maxResponseDrain (1 MiB) of traffic per retry. See issue #484.

At-Least-Once Semantics

Retries may cause duplicate delivery if the server processes a batch but returns 5xx due to a timeout. Receivers SHOULD be idempotent.

Output is safe for concurrent use.

func New

func New(cfg *Config, metrics audit.Metrics, opts ...Option) (*Output, error)

New creates a new Output from the given config. It validates the config, builds an SSRF-safe HTTP client, and starts the background batch goroutine. The metrics parameter is optional (may be nil). Per-output metrics may be supplied at construction via WithOutputMetrics.

Optional Option arguments tune construction-time behaviour. Pass WithDiagnosticLogger to route TLS-policy warnings to a custom logger.

func (*Output) Close

func (w *Output) Close() error

Close signals the batch goroutine to drain and flush, then waits for completion. In-flight HTTP requests complete using the live context before the context is cancelled. Close is idempotent.

func (*Output) DestinationKey

func (w *Output) DestinationKey() string

DestinationKey returns the webhook URL with query parameters and fragment stripped, enabling duplicate destination detection via audit.DestinationKeyer. Query parameters are stripped to avoid leaking auth tokens in error messages if two outputs collide.

func (*Output) LastDeliveryNanos added in v0.1.12

func (w *Output) LastDeliveryNanos() int64

LastDeliveryNanos returns the wall-clock UnixNano of the most recent HTTP 2xx response, or 0 if no batch has yet been delivered. Updated from the batch goroutine after the server confirms receipt — failed batches (network errors, 4xx, retries-exhausted) leave the timestamp frozen. Implements audit.LastDeliveryReporter (#753).

func (*Output) Name

func (w *Output) Name() string

Name returns the human-readable identifier for this output. The name is cached at construction time to avoid per-call url.Parse.

func (*Output) ReportsDelivery

func (w *Output) ReportsDelivery() bool

ReportsDelivery returns true, indicating that Output reports its own delivery metrics from the batch goroutine after actual HTTP delivery, not from the Write enqueue path.

func (*Output) Write

func (w *Output) Write(data []byte) error

Write enqueues a serialised audit event for batched delivery. Events exceeding MaxEventBytes are rejected with audit.ErrEventTooLarge before the defensive copy (#688). The data is copied before enqueuing. If the internal buffer is full, the event is dropped and audit.OutputMetrics.RecordDrop is called. Write never blocks the caller.

Jump to

Keyboard shortcuts

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