init: Initial commit

This commit is contained in:
Björn Benouarets
2026-02-05 18:37:35 +01:00
commit 31f5b4081a
25 changed files with 1759 additions and 0 deletions

155
.docs/architecture.md Normal file
View File

@@ -0,0 +1,155 @@
# Architecture
## System Overview
The SecNex API Gateway follows a modular architecture with clear separation of concerns. The system is built using the chi/v5 HTTP router and implements a middleware pipeline pattern.
```
┌─────────────────────────────────────────────────────────────┐
│ Client Request │
└─────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Gateway (chi Router) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Middleware Pipeline │ │
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │
│ │ │ Request ID │→ │ Real IP │→ │ Logger │ │ │
│ │ └────────────┘ └────────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Route Handler │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Per-Route Middleware Chain │ │
│ │ ┌────────────┐ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Strip │→ │ WAF │→ │ Auth │ │ │
│ │ │ Prefix │ │ Filter │ │ Middleware │ │ │
│ │ └────────────┘ └─────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Reverse Proxy (httputil.ReverseProxy) │
│ │ │
│ ▼ │
│ Backend Service │
└─────────────────────────────────────────────────────────────┘
```
## Component Architecture
### Directory Structure
```
app/
├── main.go # Application entry point
├── config/ # Configuration management
│ ├── config.go # Config interface
│ ├── file.go # File-based config loader
│ └── types.go # Configuration type definitions
├── server/ # Core server components
│ ├── gateway.go # Main gateway server
│ ├── routes.go # Route handler creation
│ └── proxy.go # Host-based virtual hosting
├── middlewares/ # HTTP middleware
│ ├── auth.go # Authentication middleware
│ └── waf.go # Web Application Firewall
├── handlers/ # Request handlers
│ └── route.go # Route handlers (placeholder)
└── res/ # HTTP responses
└── error.go # Error response utilities
```
### Core Components
#### Gateway (`server/gateway.go`)
The Gateway is the main server component that:
- Initializes the chi router
- Applies global middleware (request_id, real_ip, logger)
- Registers route handlers
- Starts the HTTP server
#### Routes (`server/routes.go`)
The Routes component creates handlers for each configured route:
1. Finds the matching API backend target
2. Creates a reverse proxy using `httputil.NewSingleHostReverseProxy`
3. Optionally strips prefix from the request path
4. Applies middleware chain: WAF → Auth
#### Configuration (`config/`)
Configuration is loaded from YAML and provides:
- Gateway settings (host, port, features)
- Route definitions with security policies
- API backend targets
- Proxy configurations for virtual hosting
## Middleware Chain
### Global Middleware
Applied to all requests via chi middleware:
| Feature | Description |
|---------|-------------|
| `request_id` | Adds unique request ID to context |
| `real_ip` | Determines the real client IP |
| `logger` | Logs HTTP requests |
### Per-Route Middleware
Applied in order to each route handler:
1. **StripPrefix** - Removes specified prefix from request path before proxying
2. **WAF** - Filters requests based on HTTP method
3. **Auth** - Validates authentication credentials
## Request Flow
1. **Client Request** → Gateway receives HTTP request
2. **Global Middleware** → Request ID, Real IP, Logging applied
3. **Route Matching** → Chi matches route pattern (e.g., `/api/v1/dev/*`)
4. **Per-Route Middleware** → StripPrefix → WAF → Auth
5. **Reverse Proxy** → Request forwarded to backend API
6. **Response** → Backend response returned to client
## Authentication Flow
The authentication middleware supports path-based filtering:
```
┌─────────────────────┐
│ Include Set? │
└──────────┬──────────┘
┌─────┴─────┐
│ Yes │ No
▼ ▼
┌─────────┐ ┌─────────────┐
│ Match │ │ Exclude │
│ Include?│ │ Set? │
└────┬────┘ └──────┬──────┘
│ │
┌──┴──┐ ┌──┴──┐
│Yes │No │Yes │No
▼ ▼ ▼ ▼
┌────┐┌────┐ ┌────┐┌────┐
│Auth││Skip│ │Skip││Auth│
└────┘└────┘ └────┘└────┘
```
## Configuration Flow
1. Load `gateway.yaml` via `config.NewFileConfig()`
2. Parse YAML into `Configuration` struct
3. Create Gateway with configuration
4. Create Routes from route definitions and API targets
5. Register handlers with chi router
6. Start HTTP server

203
.docs/configuration.md Normal file
View File

@@ -0,0 +1,203 @@
# Configuration
The gateway is configured via a single YAML file (`gateway.yaml`). This document describes all available configuration options.
## Configuration File Structure
```yaml
gateway:
host: "0.0.0.0"
port: 8080
features:
- request_id
- real_ip
- logger
proxies:
- id: "proxy-id"
host: "example.com"
target: "http://backend:3000"
apis:
- id: "api-id"
target: "https://api.example.com"
routes:
- id: "route-id"
path: "/api/v1/*"
strip_prefix:
enabled: true
prefix: "/api/v1"
security:
auth:
enabled: true
type: "api_key"
header: "X-Api-Key"
path:
include: []
exclude: []
waf:
enabled: true
methods: ["GET", "POST"]
```
## Sections
### Gateway
Global gateway configuration.
| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `host` | string | Host address to bind to | Required |
| `port` | integer | Port number | Required |
| `features` | array | Global middleware features | Required |
#### Features
Available global features:
| Feature | Description |
|---------|-------------|
| `request_id` | Adds unique request ID to each request |
| `real_ip` | Determines real client IP from headers |
| `logger` | Logs all HTTP requests |
### Proxies
Virtual hosting configuration for host-based routing.
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique proxy identifier |
| `host` | string | Domain/host name to match |
| `target` | string | Backend URL to proxy to |
### APIs
Backend service definitions referenced by routes.
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique API identifier (referenced by routes) |
| `target` | string | Backend URL |
### Routes
Route definitions with security policies.
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique route identifier (must match API ID) |
| `path` | string | Chi route pattern (e.g., `/api/v1/*`) |
| `strip_prefix` | object | Prefix stripping configuration |
| `security` | object | Security policies (auth, WAF) |
#### Strip Prefix
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | boolean | Enable prefix stripping |
| `prefix` | string | Prefix to remove from path |
#### Security
##### Authentication
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | boolean | Enable authentication |
| `type` | string | Auth type (`api_key`, `session`, etc.) |
| `header` | string | Header name to validate |
| `path` | object | Path-based filtering |
##### Auth Path Filtering
| Field | Type | Description |
|-------|------|-------------|
| `include` | array | Paths that require auth (empty = all) |
| `exclude` | array | Paths that skip auth |
**Include/Exclude Logic:**
- If `include` is set → only matching paths require auth
- If `include` is empty → all paths require auth except `exclude`
Wildcards (`*`) are supported in path patterns.
##### WAF (Web Application Firewall)
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | boolean | Enable WAF |
| `methods` | array | Allowed HTTP methods (`["*"]` for all) |
## Example Configurations
### Public API (No Auth)
```yaml
routes:
- id: "public-api"
path: "/public/*"
strip_prefix:
enabled: true
prefix: "/public"
security:
auth:
enabled: false
waf:
enabled: true
methods: ["GET", "POST"]
```
### Protected API with API Key
```yaml
routes:
- id: "protected-api"
path: "/api/v1/*"
strip_prefix:
enabled: true
prefix: "/api/v1"
security:
auth:
enabled: true
type: "api_key"
header: "X-Api-Key"
waf:
enabled: true
methods: ["*"]
```
### Mixed Auth (Path-based)
```yaml
routes:
- id: "mixed-api"
path: "/api/*"
security:
auth:
enabled: true
header: "Authorization"
path:
include: ["/api/admin/*", "/api/users/*/profile"]
exclude: ["/api/health", "/api/public/*"]
waf:
enabled: true
methods: ["*"]
```
## Configuration Loading
The gateway loads configuration from a file path relative to the binary:
```go
cfg, err := config.NewFileConfig("../gateway.yaml")
```
For Docker deployments, mount the config file:
```yaml
volumes:
- ./gateway.yaml:/app/gateway.yaml:ro
```

267
.docs/deployment.md Normal file
View File

@@ -0,0 +1,267 @@
# Deployment
## Docker Deployment
### Using Docker Compose (Recommended)
```bash
docker compose up -d
```
This will:
1. Pull the image `git.secnex.io/secnex/api-gateway:latest`
2. Build locally if the image cannot be pulled
3. Start the gateway on port 8080
### Manual Docker Build
```bash
# Build the image
docker build -t secnex-gateway .
# Run the container
docker run -d \
-p 8080:8080 \
-v $(pwd)/gateway.yaml:/app/gateway.yaml:ro \
--name gateway \
secnex-gateway
```
### Docker Image from Registry
```bash
docker pull git.secnex.io/secnex/api-gateway:latest
docker run -d \
-p 8080:8080 \
-v $(pwd)/gateway.yaml:/app/gateway.yaml:ro \
--name gateway \
git.secnex.io/secnex/api-gateway:latest
```
## Configuration Management
### Production Configuration
Create a production-specific `gateway.yaml`:
```yaml
gateway:
host: "0.0.0.0"
port: 8080
features:
- request_id
- real_ip
- logger
# ... rest of configuration
```
### Environment-Specific Configs
Use multiple config files:
```bash
# Development
docker run -v $(pwd)/gateway.dev.yaml:/app/gateway.yaml ...
# Production
docker run -v $(pwd)/gateway.prod.yaml:/app/gateway.yaml ...
```
## Kubernetes Deployment
### Deployment Manifest
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: gateway
image: git.secnex.io/secnex/api-gateway:latest
ports:
- containerPort: 8080
volumeMounts:
- name: config
mountPath: /app/gateway.yaml
subPath: gateway.yaml
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: config
configMap:
name: gateway-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gateway-config
data:
gateway.yaml: |
gateway:
host: "0.0.0.0"
port: 8080
features:
- request_id
- real_ip
- logger
# ... rest of configuration
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
spec:
selector:
app: api-gateway
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
```
### Apply to Kubernetes
```bash
kubectl apply -f k8s-deployment.yaml
```
## Health Checks
The gateway provides a health check endpoint:
```bash
curl http://localhost:8080/_/health
```
### Kubernetes Liveness/Readiness Probes
```yaml
livenessProbe:
httpGet:
path: /_/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /_/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
```
## Reverse Proxy Setup
### Nginx Reverse Proxy
```nginx
upstream gateway {
server localhost:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Monitoring
### Log Collection
Logs are output in JSON format. Configure your log collector:
**Fluentd example:**
```xml
<source>
@type tail
path /var/log/containers/gateway*.log
pos_file /var/log/gateway.log.pos
tag gateway.*
<parse>
@type json
</parse>
</source>
```
### Metrics
Consider adding Prometheus metrics for production deployments:
- Request count by route
- Response time histograms
- Error rates
- Backend connection status
## Security Considerations
### Production Checklist
- [ ] Use TLS/HTTPS in production
- [ ] Restrict gateway configuration file permissions
- [ ] Use secrets management for API keys
- [ ] Enable rate limiting
- [ ] Configure WAF rules appropriately
- [ ] Regular security updates
- [ ] Log monitoring and alerting
- [ ] Backup configuration files
### TLS Termination
Options for TLS:
1. **At the gateway** - Configure Go server with TLS
2. **At reverse proxy** - Use Nginx/HAProxy with TLS
3. **At cloud load balancer** - Use AWS ALB, GCP LB, etc.
## Scaling
### Horizontal Scaling
Run multiple instances behind a load balancer:
```yaml
# docker-compose.yml
services:
gateway:
deploy:
replicas: 3
```
### Resource Limits
Configure appropriate resource limits:
```yaml
resources:
limits:
memory: "512Mi"
cpu: "1000m"
reservations:
memory: "256Mi"
cpu: "500m"
```

185
.docs/development.md Normal file
View File

@@ -0,0 +1,185 @@
# Development
## Prerequisites
- Go 1.25.5 or later
- Git
## Setup
1. Clone the repository:
```bash
git clone git.secnex.io/secnex/gateway.git
cd gateway
```
2. Install dependencies:
```bash
cd app
go mod download
```
## Project Structure
```
gateway/
├── app/ # Application source code
│ ├── main.go # Entry point
│ ├── config/ # Configuration management
│ ├── server/ # Core server components
│ ├── middlewares/ # HTTP middleware
│ ├── handlers/ # Request handlers
│ └── res/ # Response utilities
├── .docs/ # Documentation
├── gateway.yaml # Configuration file
├── Dockerfile # Docker image definition
├── docker-compose.yml # Docker Compose configuration
└── .gitignore # Git ignore rules
```
## Running Locally
```bash
cd app
go run main.go
```
## Testing
```bash
# Run all tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run tests for specific package
go test ./config
go test ./server
go test ./middlewares
```
## Building
```bash
# Build binary
cd app
go build -o gateway main.go
# Build for Linux
GOOS=linux go build -o gateway-linux main.go
# Build for macOS (ARM64)
GOOS=darwin GOARCH=arm64 go build -o gateway-darwin-arm64 main.go
```
## Code Style
- Follow standard Go conventions
- Use `gofmt` for formatting
- Keep functions focused and small
- Add comments for exported types and functions
- Use meaningful variable names
## Adding Features
### Adding a New Middleware
Create a new file in `app/middlewares/`:
```go
package middlewares
import "net/http"
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Your logic here
next.ServeHTTP(w, r)
})
}
```
Then apply it in `server/routes.go`:
```go
if route.Security.CustomMiddleware.Enabled {
handlers[route.Path] = middlewares.MyMiddleware(handlers[route.Path])
}
```
### Adding a New Configuration Field
1. Update `config/types.go` to add the field to the struct
2. Update `gateway.yaml` with the new configuration
3. Update configuration documentation in `.docs/configuration.md`
### Adding a New Handler
Add your handler in `app/handlers/route.go`:
```go
package handlers
import "net/http"
func MyCustomHandler(w http.ResponseWriter, r *http.Request) {
// Your logic here
}
```
## Documentation
When making changes to the codebase, update the relevant documentation:
- **Architecture changes** → `.docs/architecture.md`
- **Configuration changes** → `.docs/configuration.md`
- **New features or usage changes** → `.docs/usage.md`
- **Deployment changes** → `.docs/deployment.md`
## Debugging
### Enable Debug Logging
Debug logging is enabled by default in `main.go`:
```go
masterlog.SetLevel(masterlog.LevelDebug)
```
### Using Delve
```bash
# Install Delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Debug main.go
cd app
dlv debug main.go
```
## Internal Module
The project uses an internal Go module:
```
git.secnex.io/secnex/gateway
```
When importing packages within the project, use this path:
```go
import "git.secnex.io/secnex/gateway/config"
```
## Release Process
1. Update version in documentation
2. Tag the release:
```bash
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
3. Build Docker image:
```bash
docker build -t git.secnex.io/secnex/api-gateway:v1.0.0 .
```

45
.docs/index.md Normal file
View File

@@ -0,0 +1,45 @@
# SecNex API Gateway Documentation
Welcome to the official documentation for the SecNex API Gateway.
## Overview
SecNex API Gateway is a high-performance, configurable API gateway built in Go. It provides reverse proxy capabilities with built-in security features including authentication and web application firewall (WAF) functionality.
## Quick Start
```bash
# Run locally
cd app
go run main.go
# Run with Docker
docker compose up -d
```
## Documentation
- [Architecture](./architecture.md) - System architecture and component design
- [Configuration](./configuration.md) - Configuration reference and examples
- [Usage](./usage.md) - How to use the gateway
- [Development](./development.md) - Development setup and guide
- [Deployment](./deployment.md) - Deployment instructions
## Features
- **Reverse Proxy** Route requests to backend services
- **Authentication** Header-based authentication (API key, session tokens)
- **WAF** Web Application Firewall with method filtering and IP-based access control
- **Path-based Routing** Configurable route patterns with prefix stripping
- **Middleware Pipeline** Extensible middleware chain for custom logic
- **Logging** Structured logging with sensitive field pseudonymization
## Health Check
The gateway provides a health check endpoint:
```
GET /_/health
```
Returns `200 OK` when the gateway is running.

176
.docs/usage.md Normal file
View File

@@ -0,0 +1,176 @@
# Usage
## Running the Gateway
### Local Development
```bash
cd app
go run main.go
```
The gateway will start on the configured host and port (default: `0.0.0.0:8080`).
### Docker
```bash
# Pull and run (builds locally if image not available)
docker compose up -d
# View logs
docker compose logs -f gateway
# Stop
docker compose down
```
### Build from Source
```bash
cd app
go build -o gateway main.go
./gateway
```
## Health Check
Check if the gateway is running:
```bash
curl http://localhost:8080/_/health
```
Response: `OK`
## Making Requests
After configuring routes in `gateway.yaml`, make requests to the gateway:
```bash
# Example: Request to a route configured at /api/v1/dev/*
curl http://localhost:8080/api/v1/dev/users
# With API key authentication
curl -H "X-Api-Key: your-key-here" http://localhost:8080/api/v1/dev/data
# With session authentication
curl -H "Authorization: Bearer token" http://localhost:8080/api/v1/protected
```
## Route Examples
### Strip Prefix Example
**Configuration:**
```yaml
routes:
- id: "api"
path: "/api/v1/*"
strip_prefix:
enabled: true
prefix: "/api/v1"
```
**Request flow:**
- Client requests: `/api/v1/users/123`
- Gateway strips: `/api/v1`
- Backend receives: `/users/123`
### Authentication Example
**Configuration:**
```yaml
security:
auth:
enabled: true
type: "api_key"
header: "X-Api-Key"
```
**Valid request:**
```bash
curl -H "X-Api-Key: secret123" http://localhost:8080/api/v1/data
```
**Invalid request (missing header):**
```bash
curl http://localhost:8080/api/v1/data
# Returns: 401 Unauthorized
```
### WAF Method Filtering Example
**Configuration:**
```yaml
security:
waf:
enabled: true
methods: ["GET", "POST"]
```
**Allowed:**
```bash
curl -X GET http://localhost:8080/api/v1/data
curl -X POST http://localhost:8080/api/v1/data
```
**Blocked:**
```bash
curl -X DELETE http://localhost:8080/api/v1/data
# Returns: 403 Forbidden
```
## Logging
The gateway uses structured logging via `masterlog`. Logs include:
- Request ID (if enabled)
- Client IP (if real_ip enabled)
- Request method and path
- Response status
**Example log output:**
```json
{
"level": "info",
"msg": "Registering route",
"path": "/api/v1/dev/*"
}
```
## Troubleshooting
### Gateway fails to start
1. Check if port is already in use:
```bash
lsof -i :8080
```
2. Verify configuration file exists and is valid YAML
3. Check logs for detailed error messages
### 401 Unauthorized
- Verify auth header is included
- Check header name matches configuration
- Verify path is not excluded from auth
### 403 Forbidden
- Check WAF method configuration
- Verify HTTP method is allowed
### Backend connection errors
- Verify API target URL is correct
- Check backend service is running
- Verify network connectivity
## Performance Considerations
- The gateway uses Go's `httputil.ReverseProxy` for efficient proxying
- Keep-alive connections are reused by default
- Consider connection pooling for high-traffic scenarios
- Monitor memory usage with high request volumes

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
cover.html
# Go workspace file
go.work
go.work.sum
# Dependency directories
vendor/
# Build output
/bin/
/dist/
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Environment files
.env
.env.local
.env.*.local
# Log files
*.log
logs/
# Temporary files
tmp/
temp/

103
CLAUDE.md Normal file
View File

@@ -0,0 +1,103 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
SecNex API Gateway is a Go-based API gateway built with chi/v5 routing. It acts as a reverse proxy with configurable security features including authentication and WAF (Web Application Firewall) capabilities.
## Build and Run
```bash
cd app
go run main.go
```
The gateway loads configuration from `gateway.yaml` (sibling to the `app/` directory).
## Architecture
### Directory Structure
```
app/
├── main.go # Entry point
├── config/ # Configuration loading and types
├── server/ # Gateway, routes, and proxy setup
├── middlewares/ # Auth and WAF middleware
├── handlers/ # Route handlers (currently empty)
└── res/ # HTTP error responses
```
### Core Components
**Gateway (`server/gateway.go`)**
- Main server using chi/v5 router
- Dynamically enables features via config: `request_id`, `real_ip`, `logger`
- Registers route handlers and starts HTTP server
**Routes (`server/routes.go`)**
- Creates handlers for each route configured in `gateway.yaml`
- For each route:
1. Finds the matching API backend target
2. Creates a reverse proxy using `httputil.NewSingleHostReverseProxy`
3. Optionally strips prefix from the request path
4. Applies WAF middleware (method filtering)
5. Applies Auth middleware (header-based authentication)
**Proxy (`server/proxy.go`)**
- Host-based virtual hosting proxy (separate from routes)
- Maps `proxy.host` to a backend target
- Currently unused in main.go but available
**Configuration Flow**
1. `config.NewFileConfig("../gateway.yaml")` loads YAML
2. `server.NewGateway(cfg)` creates gateway with chi router
3. `server.NewRoutes(cfg.GetRoutes(), cfg.GetApis())` creates handlers
4. `gateway.SetRoutes(routes)` registers handlers
### Middleware Chain (per route)
Middlewares are applied in order via decorator pattern in `server/routes.go:createHandlers`:
1. StripPrefix (if enabled) - removes prefix from path before proxying
2. WAF - filters by HTTP method (or `["*"]` for all methods)
3. Auth - validates presence of auth header
### Key Implementation Details
**Auth Middleware** (`middlewares/auth.go`)
- Checks for presence of configured header (e.g., `X-Api-Key`, `Authorization`)
- Path-based filtering via `include`/`exclude` patterns:
- If `include` is set → only those paths require auth
- If `include` is empty → all paths require auth except `exclude` patterns
- Supports wildcard patterns (`*`) in path matching
- Deletes the auth header before forwarding to backend
**WAF Configuration** (`middlewares/waf.go`)
- Currently only implements HTTP method filtering
- `methods: ["*"]` allows all methods
- `methods: ["GET"]` only allows GET
**Route Handler Pattern**
Each route in `gateway.yaml` must have:
- `id` - must match an API entry
- `path` - chi route pattern (e.g., `/api/v1/dev/*`)
- `strip_prefix` - optional prefix removal
- `security.auth` - optional auth middleware
- `security.waf` - optional method filtering
### Module Structure
The codebase uses an internal Go module:
```
git.secnex.io/secnex/gateway
```
Internal imports use this path (e.g., `git.secnex.io/secnex/gateway/config`).
### Logging
Uses `git.secnex.io/secnex/masterlog` with:
- JSON encoder
- Pseudonymizer for sensitive fields (`user_id`, `email`, `ip`)
- Debug-level logging enabled in main.go

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Build stage
FROM golang:1.25.5-alpine AS builder
WORKDIR /build
# Copy go mod files (for better caching)
COPY app/go.mod app/go.sum ./
RUN go mod download
# Copy source code
COPY app/ ./
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gateway .
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
# Copy binary from builder
COPY --from=builder /build/gateway .
# Copy configuration
COPY gateway.yaml .
# Expose port
EXPOSE 8080
# Run the gateway
CMD ["./gateway"]

1
README.md Normal file
View File

@@ -0,0 +1 @@
# SecNex API Gateway

11
app/config/config.go Normal file
View File

@@ -0,0 +1,11 @@
package config
type Config interface {
GetConfiguration() *Configuration
GetGatewayConfiguration() *GatewayConfiguration
GetFeatures() []string
GetRoutes() []RouteConfiguration
GetApis() []ApiConfiguration
GetHost() string
GetPort() int
}

60
app/config/file.go Normal file
View File

@@ -0,0 +1,60 @@
package config
import (
"os"
"go.yaml.in/yaml/v3"
)
type FileConfig struct {
filePath string
config *Configuration
}
func NewFileConfig(filePath string) (*FileConfig, error) {
c := &FileConfig{filePath: filePath, config: &Configuration{}}
if err := c.loadConfig(); err != nil {
return nil, err
}
return c, nil
}
func (c *FileConfig) loadConfig() error {
data, err := os.ReadFile(c.filePath)
if err != nil {
return err
}
return yaml.Unmarshal(data, c.config)
}
func (c *FileConfig) GetConfiguration() *Configuration {
return c.config
}
func (c *FileConfig) GetGatewayConfiguration() *GatewayConfiguration {
return &c.config.Gateway
}
func (c *FileConfig) GetRoutes() []RouteConfiguration {
return c.config.Routes
}
func (c *FileConfig) GetProxies() []ProxyConfiguration {
return c.config.Proxies
}
func (c *FileConfig) GetHost() string {
return c.config.Gateway.Host
}
func (c *FileConfig) GetPort() int {
return c.config.Gateway.Port
}
func (c *FileConfig) GetApis() []ApiConfiguration {
return c.config.Apis
}
func (c *FileConfig) GetFeatures() []string {
return c.config.Gateway.Features
}

57
app/config/types.go Normal file
View File

@@ -0,0 +1,57 @@
package config
type Configuration struct {
Gateway GatewayConfiguration `yaml:"gateway"`
Apis []ApiConfiguration `yaml:"apis"`
Routes []RouteConfiguration `yaml:"routes"`
Proxies []ProxyConfiguration `yaml:"proxies"`
}
type GatewayConfiguration struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Features []string `yaml:"features"`
}
type RouteConfiguration struct {
ID string `yaml:"id"`
Path string `yaml:"path"`
StripPrefix struct {
Enabled bool `yaml:"enabled"`
Prefix string `yaml:"prefix"`
} `yaml:"strip_prefix"`
Security SecurityConfiguration `yaml:"security"`
}
type SecurityConfiguration struct {
Auth AuthConfiguration `yaml:"auth"`
WAF WAFConfiguration `yaml:"waf"`
}
type WAFConfiguration struct {
Enabled bool `yaml:"enabled"`
Methods []string `yaml:"methods"`
}
type AuthConfiguration struct {
Enabled bool `yaml:"enabled"`
Type string `yaml:"type"`
Header string `yaml:"header"`
Path AuthPathConfiguration `yaml:"path"`
}
type AuthPathConfiguration struct {
Include []string `yaml:"include"`
Exclude []string `yaml:"exclude"`
}
type ProxyConfiguration struct {
ID string `yaml:"id"`
Host string `yaml:"host"`
Target string `yaml:"target"`
}
type ApiConfiguration struct {
ID string `yaml:"id"`
Target string `yaml:"target"`
}

9
app/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module git.secnex.io/secnex/api-gateway
go 1.25.5
require (
git.secnex.io/secnex/masterlog v0.1.0
github.com/go-chi/chi/v5 v5.2.4
go.yaml.in/yaml/v3 v3.0.4
)

8
app/go.sum Normal file
View File

@@ -0,0 +1,8 @@
git.secnex.io/secnex/masterlog v0.1.0 h1:74j9CATpfeK0lxpWIQC9ag9083akwG8khi5BwLedD8E=
git.secnex.io/secnex/masterlog v0.1.0/go.mod h1:OnDlwEzdkKMnqY+G5O9kHdhoJ6fH1llbVdXpgSc5SdM=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

1
app/handlers/route.go Normal file
View File

@@ -0,0 +1 @@
package handlers

29
app/main.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"os"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/server"
"git.secnex.io/secnex/masterlog"
)
func main() {
masterlog.SetLevel(masterlog.LevelDebug)
masterlog.AddEncoder(&masterlog.JSONEncoder{})
pseudonymizer := masterlog.NewPseudonymizerFromString("your-secret-key")
masterlog.SetPseudonymizer(pseudonymizer)
masterlog.AddSensitiveFields("user_id", "email", "ip")
cfg, err := config.NewFileConfig("../gateway.yaml")
if err != nil {
masterlog.Error("Failed to load config", map[string]interface{}{
"error": err,
})
os.Exit(1)
}
gateway := server.NewGateway(cfg)
routes := server.NewRoutes(cfg.GetRoutes(), cfg.GetApis())
gateway.SetRoutes(routes)
gateway.Start()
}

61
app/middlewares/auth.go Normal file
View File

@@ -0,0 +1,61 @@
package middlewares
import (
"net/http"
"path"
"strings"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/res"
"git.secnex.io/secnex/masterlog"
)
func authPathMatches(pattern, requestPath string) bool {
if pattern == "*" {
return true
}
if pattern == requestPath {
return true
}
if strings.Contains(pattern, "*") {
matched, _ := path.Match(pattern, requestPath)
return matched
}
return false
}
func Auth(next http.Handler, authType string, authHeader string, authPath config.AuthPathConfiguration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
masterlog.Debug("Auth middleware", map[string]interface{}{
"path": r.URL.Path,
"include": authPath.Include,
"exclude": authPath.Exclude,
})
if len(authPath.Include) > 0 {
matched := false
for _, include := range authPath.Include {
if authPathMatches(include, r.URL.Path) {
matched = true
break
}
}
if !matched {
next.ServeHTTP(w, r)
return
}
} else {
for _, exclude := range authPath.Exclude {
if authPathMatches(exclude, r.URL.Path) {
next.ServeHTTP(w, r)
return
}
}
}
if r.Header.Get(authHeader) == "" {
res.Unauthorized(w)
return
}
r.Header.Del(authHeader)
next.ServeHTTP(w, r)
})
}

27
app/middlewares/waf.go Normal file
View File

@@ -0,0 +1,27 @@
package middlewares
import (
"net/http"
"slices"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/res"
)
func WAF(next http.Handler, wafConfig config.WAFConfiguration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !wafConfig.Enabled {
next.ServeHTTP(w, r)
return
}
if slices.Contains(wafConfig.Methods, "*") {
next.ServeHTTP(w, r)
return
}
if !slices.Contains(wafConfig.Methods, r.Method) {
res.Forbidden(w)
return
}
next.ServeHTTP(w, r)
})
}

21
app/res/error.go Normal file
View File

@@ -0,0 +1,21 @@
package res
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Message string `json:"message"`
Code int `json:"code"`
}
func Unauthorized(w http.ResponseWriter) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(ErrorResponse{Message: "Unauthorized", Code: http.StatusUnauthorized})
}
func Forbidden(w http.ResponseWriter) {
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(ErrorResponse{Message: "Forbidden", Code: http.StatusForbidden})
}

67
app/server/gateway.go Normal file
View File

@@ -0,0 +1,67 @@
package server
import (
"fmt"
"net/http"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/masterlog"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Gateway struct {
router *chi.Mux
config config.Config
routes *Routes
proxy *Proxy
}
func NewGateway(config config.Config) *Gateway {
r := chi.NewRouter()
for _, feature := range config.GetFeatures() {
switch feature {
case "request_id":
r.Use(middleware.RequestID)
case "real_ip":
r.Use(middleware.RealIP)
case "logger":
r.Use(middleware.Logger)
}
}
r.Route("/_/health", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
})
return &Gateway{config: config, router: r, routes: nil, proxy: nil}
}
func (g *Gateway) SetRoutes(routes *Routes) {
g.routes = routes
}
func (g *Gateway) SetProxy(proxy *Proxy) {
g.proxy = proxy
}
func (g *Gateway) Start() {
masterlog.Info("Starting gateway", map[string]interface{}{
"host": g.config.GetGatewayConfiguration().Host,
"port": g.config.GetGatewayConfiguration().Port,
})
for path, handler := range g.routes.handlers {
masterlog.Info("Registering route", map[string]interface{}{
"path": path,
})
g.router.Handle(path, handler)
}
gatewayConfig := g.config.GetGatewayConfiguration()
http.ListenAndServe(fmt.Sprintf("%s:%d", gatewayConfig.Host, gatewayConfig.Port), g.router)
}

42
app/server/proxy.go Normal file
View File

@@ -0,0 +1,42 @@
package server
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"git.secnex.io/secnex/api-gateway/config"
)
type Proxy struct {
proxies []config.ProxyConfiguration
handlers map[string]http.Handler
}
func NewProxy(proxies []config.ProxyConfiguration) *Proxy {
handlers := make(map[string]http.Handler)
for _, proxy := range proxies {
backend, err := url.Parse(proxy.Target)
if err != nil {
log.Fatalf("Failed to parse proxy target: %v", err)
}
p := httputil.NewSingleHostReverseProxy(backend)
originalDirector := p.Director
p.Director = func(r *http.Request) {
originalDirector(r)
r.Host = backend.Host
}
handlers[proxy.Host] = p
}
return &Proxy{proxies: proxies, handlers: handlers}
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler, ok := p.handlers[r.Host]
if !ok {
http.NotFound(w, r)
return
}
handler.ServeHTTP(w, r)
}

85
app/server/routes.go Normal file
View File

@@ -0,0 +1,85 @@
package server
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/middlewares"
"git.secnex.io/secnex/masterlog"
)
type Routes struct {
routes []config.RouteConfiguration
handlers map[string]http.Handler
}
func NewRoutes(routes []config.RouteConfiguration, apis []config.ApiConfiguration) *Routes {
handlers := createHandlers(routes, apis)
return &Routes{routes: routes, handlers: handlers}
}
func findApi(apis []config.ApiConfiguration, id string) *config.ApiConfiguration {
for _, api := range apis {
if api.ID == id {
return &api
}
}
return nil
}
func createHandlers(routes []config.RouteConfiguration, apis []config.ApiConfiguration) map[string]http.Handler {
handlers := make(map[string]http.Handler)
for _, route := range routes {
masterlog.Debug("Creating handler for route", map[string]interface{}{
"path": route.Path,
"id": route.ID,
})
api := findApi(apis, route.ID)
if api == nil {
log.Fatalf("API not found: %s", route.ID)
continue
}
backendUrl, err := url.Parse(
api.Target,
)
if err != nil {
log.Fatalf("Failed to parse backend URL: %v", err)
}
proxy := httputil.NewSingleHostReverseProxy(backendUrl)
handlers[route.Path] = proxy
if route.StripPrefix.Enabled {
masterlog.Debug("Stripping prefix", map[string]interface{}{
"id": route.ID,
"path": route.Path,
"prefix": route.StripPrefix.Prefix,
})
handlers[route.Path] = http.StripPrefix(route.StripPrefix.Prefix, handlers[route.Path])
}
if route.Security.WAF.Enabled {
masterlog.Debug("Applying WAF middleware", map[string]interface{}{
"id": route.ID,
"path": route.Path,
"methods": route.Security.WAF.Methods,
})
handlers[route.Path] = middlewares.WAF(handlers[route.Path], route.Security.WAF)
}
if route.Security.Auth.Enabled {
masterlog.Debug("Applying auth middleware", map[string]interface{}{
"id": route.ID,
"path": route.Path,
"type": route.Security.Auth.Type,
"header": route.Security.Auth.Header,
})
handlers[route.Path] = middlewares.Auth(
handlers[route.Path],
route.Security.Auth.Type,
route.Security.Auth.Header,
route.Security.Auth.Path,
)
}
}
return handlers
}

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
gateway:
image: git.secnex.io/secnex/api-gateway:latest
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
volumes:
- ./gateway.yaml:/app/gateway.yaml:ro
restart: unless-stopped

57
gateway.yaml Normal file
View File

@@ -0,0 +1,57 @@
gateway:
host: "0.0.0.0"
port: 8080
features:
- request_id
- real_ip
- logger
proxies:
- id: "secnex-dev"
host: "dev.secnex.io"
target: "http://localhost:3003"
apis:
- id: "secnex-dev"
target: "https://httpbin.org"
- id: "secnex-public"
target: "https://httpbin.org"
routes:
- id: "secnex-dev"
path: "/api/v1/dev/*"
strip_prefix:
enabled: true
prefix: "/api/v1/dev"
security:
auth:
enabled: true
type: "api_key"
header: "X-Api-Key"
path:
include: []
exclude: []
waf:
enabled: true
methods: ["*"]
ip_addresses:
allow:
- "127.0.0.1"
block:
- "127.0.0.2"
- id: "secnex-public"
path: "/api/v1/public/*"
strip_prefix:
enabled: true
prefix: "/api/v1/public"
security:
auth:
enabled: false
type: "session"
header: "Authorization"
path:
include: []
exclude: []
waf:
enabled: true
methods: ["GET"]