Gatekeeper is a flexible and performant Go middleware library designed to enhance the security and control of your web applications. It offers seamless integration with the standard net/http
library and popular Go web frameworks, providing essential security features out-of-the-box.
Gatekeeper provides five key security and control features:
-
User-Agent Blacklisting/Whitelisting:
- Purpose: Block or allow requests based on the
User-Agent
header. - Configuration: Define lists of exact User-Agent strings or regular expression patterns. Operates in
BLACKLIST
(block if matched) orWHITELIST
(allow only if matched) mode.
- Purpose: Block or allow requests based on the
-
IP Address Blacklisting/Whitelisting:
- Purpose: Control access based on client IP address.
- Configuration: Define lists of individual IP addresses or CIDR ranges. Operates in
BLACKLIST
orWHITELIST
mode. Supports trustingX-Forwarded-For
/X-Real-IP
headers from configured trusted proxies.
-
Referer Blacklisting/Whitelisting:
- Purpose: Control access based on the HTTP
Referer
header to prevent hotlinking, block malicious referrers, or enforce HTTPS. - Configuration: Define lists of exact Referer URLs or regular expression patterns. Operates in
BLACKLIST
(block if matched) orWHITELIST
(allow only if matched) mode. - Use Cases: Block spam domains, enforce HTTPS-only referers, prevent hotlinking, block known phishing sites.
- Purpose: Control access based on the HTTP
-
IP Address Rate Limiting (with Exceptions):
- Purpose: Prevent abuse and brute-force attacks by limiting request rates per IP.
- Configuration: Define requests per period (e.g., 100 requests/minute).
- Storage: Defaults to an in-memory store (suitable for single instances). Pluggable
RateLimiterStore
interface allows for custom backends (e.g., Redis, Memcached) for distributed environments. - Exceptions: Whitelist specific IPs/CIDRs or URL route patterns to bypass or have different rate limits.
-
Profanity Firewall (with Whitelisting):
- Purpose: Filter requests containing undesirable language.
- Configuration: Define lists of profane words/phrases and a whitelist of words to ignore (e.g., "Scunthorpe").
- Scope: Can check query parameters, form data (urlencoded/multipart), and JSON request bodies.
go get github.com/4nkitd/gatekeeper
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/4nkitd/gatekeeper"
"github.com/4nkitd/gatekeeper/store"
)
func main() {
// Configure Gatekeeper
gk, err := gatekeeper.New(gatekeeper.Config{
IPPolicy: &gatekeeper.IPPolicyConfig{
Mode: gatekeeper.ModeBlacklist,
IPs: []string{"1.2.3.4"}, // Block this specific IP
CIDRs: []string{"192.168.100.0/24"}, // Block this CIDR range
},
UserAgentPolicy: &gatekeeper.UserAgentPolicyConfig{
Mode: gatekeeper.ModeBlacklist,
Patterns: []string{`^curl/.*`, `(?i)^.*bot.*$`}, // Block curl and bots
},
RefererPolicy: &gatekeeper.RefererPolicyConfig{
Mode: gatekeeper.ModeBlacklist,
Exact: []string{"http://malicious-site.com"},
Patterns: []string{`(?i).*evil\.com.*`, `^http://.*`}, // Block evil.com and non-HTTPS
},
RateLimiter: &gatekeeper.RateLimiterConfig{
Requests: 60,
Period: 1 * time.Minute, // 60 requests per minute per IP
Store: store.NewMemoryStore(5 * time.Minute),
Exceptions: &gatekeeper.RateLimiterExceptions{
IPWhitelist: []string{"127.0.0.1", "::1"}, // Localhost bypasses rate limiting
RouteWhitelistPatterns: []string{`^/health$`}, // Health endpoint exempt
},
},
ProfanityFilter: &gatekeeper.ProfanityFilterConfig{
BlockWords: []string{"badword", "spam", "offensive"},
AllowWords: []string{"scunthorpe"}, // Avoid false positives
CheckQueryParams: true,
CheckFormFields: true,
CheckJSONBody: true,
},
DefaultBlockStatusCode: http.StatusForbidden,
DefaultBlockMessage: "Access denied by security policy",
})
if err != nil {
log.Fatalf("Failed to initialize Gatekeeper: %v", err)
}
// Your main handler
myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, you've passed Gatekeeper!")
})
// Apply all configured Gatekeeper protections
protectedHandler := gk.Protect(myHandler)
http.Handle("/", protectedHandler)
log.Println("Server starting on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
package main
import (
"net/http"
"time"
"github.com/4nkitd/gatekeeper"
"github.com/4nkitd/gatekeeper/store"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Configure Gatekeeper with comprehensive security
config := gatekeeper.Config{
IPPolicy: &gatekeeper.IPPolicyConfig{
Mode: gatekeeper.ModeBlacklist,
IPs: []string{"1.2.3.4", "5.6.7.8"},
CIDRs: []string{"192.168.100.0/24"},
},
UserAgentPolicy: &gatekeeper.UserAgentPolicyConfig{
Mode: gatekeeper.ModeBlacklist,
Exact: []string{"BadBot/1.0", "EvilScraper/2.0"},
Patterns: []string{
`^curl/.*`, // Block curl
`(?i)^.*bot.*scanner.*$`, // Block bot scanners
`(?i)^.*scraper.*$`, // Block scrapers
},
},
RateLimiter: &gatekeeper.RateLimiterConfig{
Requests: 60,
Period: 1 * time.Minute,
Store: store.NewMemoryStore(5 * time.Minute),
Exceptions: &gatekeeper.RateLimiterExceptions{
IPWhitelist: []string{"127.0.0.1", "::1"},
RouteWhitelistPatterns: []string{
`^/health$`, // Health checks
`^/metrics$`, // Monitoring
`^/static/.*`, // Static assets
},
},
},
ProfanityFilter: &gatekeeper.ProfanityFilterConfig{
BlockWords: []string{"badword", "spam", "offensive"},
CheckQueryParams: true,
CheckFormFields: true,
CheckJSONBody: true,
},
DefaultBlockStatusCode: http.StatusForbidden,
DefaultBlockMessage: "Access denied by security policy",
}
// Apply Gatekeeper middleware
gk, err := gatekeeper.New(config)
if err != nil {
e.Logger.Fatal("Failed to initialize Gatekeeper: ", err)
}
e.Use(gk.EchoMiddleware())
// Add other Echo middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Define routes
e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome! You passed all security checks.",
})
})
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "healthy",
})
})
e.Logger.Fatal(e.Start(":8080"))
}
Gatekeeper provides built-in middleware support for popular Go web frameworks:
Gatekeeper provides native Echo middleware support with two convenient methods:
Method 1: Create instance then use middleware
import (
"github.com/4nkitd/gatekeeper"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Create Gatekeeper instance
gk, err := gatekeeper.New(config)
if err != nil {
log.Fatal(err)
}
// Apply middleware
e.Use(gk.EchoMiddleware())
// Your routes...
}
Method 2: One-step creation
func main() {
e := echo.New()
// Create and apply middleware in one step
middleware, err := gatekeeper.EchoMiddlewareFromConfig(config)
if err != nil {
log.Fatal(err)
}
e.Use(middleware)
// Your routes...
}
net/http
(Standard Library): Usegk.Protect(handler)
or individual policies- Gin: Use
gk.Protect()
with Gin'sWrapH()
function - Fiber: Use
gk.Protect()
with Fiber'sadaptor.HTTPHandler()
- Chi: Compatible with standard
net/http
middleware usinggk.Protect()
For complete Echo example, see example/echo.go
in the repository.
Gatekeeper is configured using the gatekeeper.Config
struct passed to gatekeeper.New()
.
type Config struct {
UserAgentPolicy *UserAgentPolicyConfig
IPPolicy *IPPolicyConfig
RateLimiter *RateLimiterConfig
ProfanityFilter *ProfanityFilterConfig
Logger *log.Logger // Optional: Custom logger
DefaultBlockStatusCode int // Optional: Defaults to 403 Forbidden
DefaultBlockMessage string // Optional: Defaults to "Forbidden"
}
Mode
:gatekeeper.ModeBlacklist
orgatekeeper.ModeWhitelist
.Exact
:[]string
of exact User-Agent strings (case-insensitive match).Patterns
:[]string
of regular expressions to match User-Agents (case-sensitive by default, use(?i)
in regex for insensitivity).
Mode
:gatekeeper.ModeBlacklist
orgatekeeper.ModeWhitelist
.IPs
:[]string
of individual IP addresses (e.g., "1.2.3.4").CIDRs
:[]string
of IP ranges in CIDR notation (e.g., "10.0.0.0/8").TrustProxyHeaders
:bool
(defaultfalse
). Iftrue
, attempts to get client IP fromX-Forwarded-For
orX-Real-IP
.TrustedProxies
:[]string
of trusted proxy IPs/CIDRs. IfTrustProxyHeaders
is true, headers are only trusted if the direct connection is from one of these proxies. If empty andTrustProxyHeaders
is true, headers from private IPs are typically trusted.
Requests
:int64
- Maximum number of requests allowed.Period
:time.Duration
- The time window for the request limit (e.g.,1 * time.Minute
).Store
:store.RateLimiterStore
- Storage backend. Defaults tostore.NewMemoryStore()
if not provided.LimitExceededMessage
:string
(defaults to "Rate limit exceeded. Please slow down!").LimitExceededStatusCode
:int
(defaults tohttp.StatusTooManyRequests
).Exceptions
:*RateLimiterExceptions
IPWhitelist
:[]string
of IPs/CIDRs exempt from rate limiting.RouteWhitelistPatterns
:[]string
of regex patterns for URL paths exempt from rate limiting (e.g.,^/health$
,^/static/.*
).
BlockWords
:[]string
of words/phrases to block (case-insensitive).AllowWords
:[]string
of words/phrases to explicitly allow, helping avoid false positives (e.g., "scunthorpe" problem).CheckQueryParams
:bool
- Scan URL query parameters for profanity.CheckFormFields
:bool
- Scanapplication/x-www-form-urlencoded
andmultipart/form-data
fields.CheckJSONBody
:bool
- Scan JSON request bodies for profanity.BlockedMessage
:string
(defaults to "Content contains inappropriate language").BlockedStatusCode
:int
(defaults tohttp.StatusBadRequest
).
The rate limiter uses an in-memory store by default (store.NewMemoryStore()
). For distributed systems, you can implement the store.RateLimiterStore
interface using a shared backend like Redis or Memcached.
package store
type RateLimiterStore interface {
Allow(key string, limit int64, window time.Duration) (allowed bool, retryAfter time.Duration, err error)
Cleanup() // Optional, for stores that need explicit cleanup
}
Example custom store implementation:
// RedisStore implements RateLimiterStore using Redis
type RedisStore struct {
client *redis.Client
}
func (r *RedisStore) Allow(key string, limit int64, window time.Duration) (bool, time.Duration, error) {
// Implement sliding window rate limiting using Redis
// Return whether request is allowed and retry-after duration
}
You can apply policies individually instead of using gk.Protect()
:
handler := myHandler
if gk.ConfiguredIPPolicy() {
handler = gk.IPPolicy(handler)
}
if gk.ConfiguredUserAgentPolicy() {
handler = gk.UserAgentPolicy(handler)
}
if gk.ConfiguredRateLimiter() {
handler = gk.RateLimit(handler)
}
if gk.ConfiguredProfanityFilter() {
handler = gk.ProfanityPolicy(handler)
}
Gin Framework:
import "github.com/gin-gonic/gin"
r := gin.Default()
gk, _ := gatekeeper.New(config)
r.Use(gin.WrapH(gk.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This will be called for each request that passes Gatekeeper
}))))
Fiber Framework:
import "github.com/gofiber/fiber/v2/middleware/adaptor"
app := fiber.New()
gk, _ := gatekeeper.New(config)
app.Use(adaptor.HTTPMiddleware(gk.Protect))
# Test User-Agent blocking
curl -H "User-Agent: curl/7.68.0" http://localhost:8080/
# Expected: Access denied by security policy
# Test with allowed User-Agent
curl -H "User-Agent: Chrome/91.0" http://localhost:8080/
# Expected: Normal response
# Test rate limiting (run multiple times quickly)
for i in {1..70}; do curl -s http://localhost:8080/ >/dev/null; done
curl http://localhost:8080/
# Expected: Rate limit exceeded after 60 requests
# Test profanity filter
curl -X POST -d "message=badword" http://localhost:8080/api/submit
# Expected: Content contains inappropriate language
When using gk.Protect(handler)
, the middleware is applied in the following default order (from outermost to innermost):
- IP Policy - First line of defense, blocks malicious IPs
- User-Agent Policy - Blocks bots and scrapers
- Rate Limiter - Prevents abuse and DDoS attacks
- Profanity Filter - Content moderation (innermost, closest to your handler)
This order ensures maximum security efficiency - cheaper checks (IP, User-Agent) happen before more expensive ones (rate limiting, content scanning).
Gatekeeper uses the standard log
package by default, prefixed with [Gatekeeper]
. You can provide your own *log.Logger
instance in gatekeeper.Config.Logger
.
Example logs:
[Gatekeeper] Request blocked: GET /api/data from 1.2.3.4. Reason: IP address in blacklist
[Gatekeeper] Request blocked: POST /submit from 10.0.0.1. Reason: User-Agent 'curl/7.68.0' matches blocked pattern
[Gatekeeper] Request blocked: GET /api/data from 127.0.0.1. Reason: Rate limit exceeded (60 requests/minute)
[Gatekeeper] Request blocked: POST /comment from 192.168.1.100. Reason: Profanity detected in request body
Contributions are welcome! Please feel free to submit issues, fork the repository, and send pull requests.
- Fork the repository.
- Create your feature branch (
git checkout -b feature/my-new-feature
). - Commit your changes (
git commit -am 'Add some feature'
). - Push to the branch (
git push origin feature/my-new-feature
). - Create a new Pull Request.