From 31f5b4081a51f428a9f6d703de27e1ad0dbb8a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Thu, 5 Feb 2026 18:37:35 +0100 Subject: [PATCH] init: Initial commit --- .docs/architecture.md | 155 +++++++++++++++++++++++ .docs/configuration.md | 203 ++++++++++++++++++++++++++++++ .docs/deployment.md | 267 ++++++++++++++++++++++++++++++++++++++++ .docs/development.md | 185 ++++++++++++++++++++++++++++ .docs/index.md | 45 +++++++ .docs/usage.md | 176 ++++++++++++++++++++++++++ .gitignore | 45 +++++++ CLAUDE.md | 103 ++++++++++++++++ Dockerfile | 33 +++++ README.md | 1 + app/config/config.go | 11 ++ app/config/file.go | 60 +++++++++ app/config/types.go | 57 +++++++++ app/go.mod | 9 ++ app/go.sum | 8 ++ app/handlers/route.go | 1 + app/main.go | 29 +++++ app/middlewares/auth.go | 61 +++++++++ app/middlewares/waf.go | 27 ++++ app/res/error.go | 21 ++++ app/server/gateway.go | 67 ++++++++++ app/server/proxy.go | 42 +++++++ app/server/routes.go | 85 +++++++++++++ docker-compose.yml | 11 ++ gateway.yaml | 57 +++++++++ 25 files changed, 1759 insertions(+) create mode 100644 .docs/architecture.md create mode 100644 .docs/configuration.md create mode 100644 .docs/deployment.md create mode 100644 .docs/development.md create mode 100644 .docs/index.md create mode 100644 .docs/usage.md create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/config/config.go create mode 100644 app/config/file.go create mode 100644 app/config/types.go create mode 100644 app/go.mod create mode 100644 app/go.sum create mode 100644 app/handlers/route.go create mode 100644 app/main.go create mode 100644 app/middlewares/auth.go create mode 100644 app/middlewares/waf.go create mode 100644 app/res/error.go create mode 100644 app/server/gateway.go create mode 100644 app/server/proxy.go create mode 100644 app/server/routes.go create mode 100644 docker-compose.yml create mode 100644 gateway.yaml diff --git a/.docs/architecture.md b/.docs/architecture.md new file mode 100644 index 0000000..53f9f4e --- /dev/null +++ b/.docs/architecture.md @@ -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 diff --git a/.docs/configuration.md b/.docs/configuration.md new file mode 100644 index 0000000..e7df178 --- /dev/null +++ b/.docs/configuration.md @@ -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 +``` diff --git a/.docs/deployment.md b/.docs/deployment.md new file mode 100644 index 0000000..eedbbf1 --- /dev/null +++ b/.docs/deployment.md @@ -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 + + @type tail + path /var/log/containers/gateway*.log + pos_file /var/log/gateway.log.pos + tag gateway.* + + @type json + + +``` + +### 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" +``` diff --git a/.docs/development.md b/.docs/development.md new file mode 100644 index 0000000..451d218 --- /dev/null +++ b/.docs/development.md @@ -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 . +``` diff --git a/.docs/index.md b/.docs/index.md new file mode 100644 index 0000000..02c2fe9 --- /dev/null +++ b/.docs/index.md @@ -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. diff --git a/.docs/usage.md b/.docs/usage.md new file mode 100644 index 0000000..4596110 --- /dev/null +++ b/.docs/usage.md @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af6bc93 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2482c4e --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d20aed --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5710737 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# SecNex API Gateway \ No newline at end of file diff --git a/app/config/config.go b/app/config/config.go new file mode 100644 index 0000000..e04d41b --- /dev/null +++ b/app/config/config.go @@ -0,0 +1,11 @@ +package config + +type Config interface { + GetConfiguration() *Configuration + GetGatewayConfiguration() *GatewayConfiguration + GetFeatures() []string + GetRoutes() []RouteConfiguration + GetApis() []ApiConfiguration + GetHost() string + GetPort() int +} diff --git a/app/config/file.go b/app/config/file.go new file mode 100644 index 0000000..5046ad0 --- /dev/null +++ b/app/config/file.go @@ -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 +} diff --git a/app/config/types.go b/app/config/types.go new file mode 100644 index 0000000..10db873 --- /dev/null +++ b/app/config/types.go @@ -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"` +} diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..c3c011f --- /dev/null +++ b/app/go.mod @@ -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 +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..9fa15bf --- /dev/null +++ b/app/go.sum @@ -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= diff --git a/app/handlers/route.go b/app/handlers/route.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/app/handlers/route.go @@ -0,0 +1 @@ +package handlers diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..e16178c --- /dev/null +++ b/app/main.go @@ -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() +} diff --git a/app/middlewares/auth.go b/app/middlewares/auth.go new file mode 100644 index 0000000..9078fd3 --- /dev/null +++ b/app/middlewares/auth.go @@ -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) + }) +} diff --git a/app/middlewares/waf.go b/app/middlewares/waf.go new file mode 100644 index 0000000..aea74b0 --- /dev/null +++ b/app/middlewares/waf.go @@ -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) + }) +} diff --git a/app/res/error.go b/app/res/error.go new file mode 100644 index 0000000..b31eed3 --- /dev/null +++ b/app/res/error.go @@ -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}) +} diff --git a/app/server/gateway.go b/app/server/gateway.go new file mode 100644 index 0000000..d78e76a --- /dev/null +++ b/app/server/gateway.go @@ -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) +} diff --git a/app/server/proxy.go b/app/server/proxy.go new file mode 100644 index 0000000..b9ceaf2 --- /dev/null +++ b/app/server/proxy.go @@ -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) +} diff --git a/app/server/routes.go b/app/server/routes.go new file mode 100644 index 0000000..eee3ca4 --- /dev/null +++ b/app/server/routes.go @@ -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 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f8ed43b --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/gateway.yaml b/gateway.yaml new file mode 100644 index 0000000..37387a1 --- /dev/null +++ b/gateway.yaml @@ -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"] \ No newline at end of file