feat: add SQLite persistence and REST API for temporary whitelisting

- Migrate IP-based temporary whitelisting from file to SQLite storage
- Add REST API endpoints for managing temporary and permanent whitelists
- Create `.env.example` with required environment variables
- Document API endpoints in README.md and docs/api.md
- Add new dependency `modernc.org/sqlite` for SQLite support
- Update deployment and security documentation
This commit is contained in:
db123-test
2026-05-04 09:08:11 +03:30
parent a5c9c42533
commit 515de33837
18 changed files with 467 additions and 254 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
AUTH_GATE_PORT
AUTH_GATE_API_TOKEN
DATA_DIR
CLEANUP_INTERVAL
AUTH_GATE_DB_PATH

View File

@@ -1,10 +1,9 @@
# auth-gate # auth-gate
A lightweight Go auth gateway that sits in front of sensitive services (phpMyAdmin, Git, A lightweight Go auth gateway that sits in front of sensitive services (phpMyAdmin, Gitea,
uptime monitors) and provides: uptime kuma) and provides:
- IP-based whitelisting (permanent via file, temporary via REST API with TTL) - IP-based whitelisting (permanent and temporary via REST API with TTL, stored in SQLite)
- HTTP Basic Auth as fallback
- No domain changes — NGINX continues to proxy to the same `server_name` - No domain changes — NGINX continues to proxy to the same `server_name`
## Architecture ## Architecture
@@ -39,6 +38,22 @@ vim .env
./auth-gate ./auth-gate
``` ```
## API
The API requires a bearer token set via `AUTH_GATE_API_TOKEN`. All endpoints return `401 Unauthorized` if the token is missing or invalid.
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/whitelist/temp` | Add a temporary whitelisted IP |
| GET | `/api/whitelist/temp/list` | List active temporary entries |
| DELETE | `/api/whitelist/temp/{ip}` | Remove a temporary entry |
| POST | `/api/whitelist/perm` | Add a permanent whitelisted IP/CIDR |
| DELETE | `/api/whitelist/perm/{entry}` | Remove a permanent entry |
| GET | `/api/logs` | Retrieve audit log entries |
| GET | `/status` | Health check |
See `docs/api.md` for full API reference.
## NGINX integration ## NGINX integration
See `docs/deploy.md` for the full NGINX `auth_request` configuration. See `docs/deploy.md` for the full NGINX `auth_request` configuration.

View File

@@ -6,6 +6,7 @@ import (
"log/slog" "log/slog"
"net" "net"
"net/http" "net/http"
"time"
) )
func CheckAuth(db *sql.DB, r *http.Request) bool { func CheckAuth(db *sql.DB, r *http.Request) bool {

View File

@@ -19,7 +19,7 @@ func loadConfig() Config {
APIToken: getEnv("AUTH_GATE_API_TOKEN", ""), APIToken: getEnv("AUTH_GATE_API_TOKEN", ""),
DataDir: getEnv("DATA_DIR", "/data"), DataDir: getEnv("DATA_DIR", "/data"),
CleanupInterval: parseDuration("CLEANUP_INTERVAL", 60*time.Second), CleanupInterval: parseDuration("CLEANUP_INTERVAL", 60*time.Second),
DBPath: getEnv("AUTH_GATE_DB_PATH", "/data/auth-gate.db"), DBPath: getEnv("AUTH_GATE_DB_PATH", "./data/auth-gate.db"),
} }
} }

View File

@@ -1 +0,0 @@
217.219.87.178

2
db.go
View File

@@ -3,6 +3,7 @@ package main
import ( import (
"database/sql" "database/sql"
"log/slog" "log/slog"
"net"
"time" "time"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -189,7 +190,6 @@ func ListTempEntries(db *sql.DB) ([]map[string]interface{}, error) {
} }
func ipMatchesEntry(ip, entry string) (bool, error) { func ipMatchesEntry(ip, entry string) (bool, error) {
// Simple case: exact IP match
if entry == ip { if entry == ip {
return true, nil return true, nil
} }

View File

@@ -29,7 +29,7 @@ curl -X POST http://127.0.0.1:8080/api/whitelist/temp \
-d '{"ip":"203.0.113.10","ttl_seconds":300,"reason":"admin laptop"}' -d '{"ip":"203.0.113.10","ttl_seconds":300,"reason":"admin laptop"}'
``` ```
### GET /api/whitelist ### GET /api/whitelist/temp/list
List all currently active temporary whitelisted IPs. List all currently active temporary whitelisted IPs.
@@ -47,7 +47,7 @@ List all currently active temporary whitelisted IPs.
] ]
``` ```
### DELETE /api/whitelist/{ip} ### DELETE /api/whitelist/temp/{ip}
Remove a temporary whitelisted IP. Remove a temporary whitelisted IP.
@@ -55,6 +55,67 @@ Remove a temporary whitelisted IP.
**Response:** 204 No Content **Response:** 204 No Content
### POST /api/whitelist/perm
Add a permanent whitelisted IP or CIDR range.
**Authorization:** Bearer `{API_TOKEN}`
**Request body:**
```json
{
"entry": "1.2.3.4"
}
```
**Response:** 204 No Content
**Example:**
```bash
curl -X POST http://127.0.0.1:8080/api/whitelist/perm \
-H "Authorization: Bearer CHANGE_ME" \
-H "Content-Type: application/json" \
-d '{"entry":"203.0.113.10"}'
```
### DELETE /api/whitelist/perm/{entry}
Remove a permanent whitelisted IP or CIDR range.
**Authorization:** Bearer `{API_TOKEN}`
**Response:** 204 No Content
### GET /api/logs
Retrieve audit log entries.
**Authorization:** Bearer `{API_TOKEN}`
**Response body:**
```json
[
{
"timestamp": "2024-01-01T12:00:00Z",
"action": "add_temp",
"ip": "1.2.3.4",
"reason": "admin laptop",
"ttl_seconds": 300,
"api_client_ip": "10.0.0.1"
}
]
```
**Example:**
```bash
curl -X GET http://127.0.0.1:8080/api/logs \
-H "Authorization: Bearer CHANGE_ME"
```
### GET /auth ### GET /auth
The NGINX auth_request endpoint. Do NOT call this directly. The NGINX auth_request endpoint. Do NOT call this directly.
@@ -67,19 +128,10 @@ Health check.
**Response:** 200 `ok` **Response:** 200 `ok`
## Auth endpoint
The `/auth` endpoint uses HTTP Basic Auth for the fallback.
**Example:**
```bash
curl -u user:pass http://127.0.0.1:8080/auth
```
## Security notes ## Security notes
- The API is not exposed publicly. It's only accessible from NGINX via Docker - The API is not exposed publicly. It's only accessible from NGINX via Docker
internal networking. internal networking.
- The API token is the only secret for the API. Keep it in the environment. - The API token is the only secret for the API. Keep it in the environment.
- The Basic Auth credentials are set via environment variables. Use strong passwords. - The auth endpoint only checks IP whitelisting (no Basic Auth fallback).
- Always serve the auth endpoint over TLS.

View File

@@ -76,22 +76,9 @@ nginx -s reload
|----------|-------------|---------| |----------|-------------|---------|
| AUTH_GATE_PORT | HTTP listen port | 8080 | | AUTH_GATE_PORT | HTTP listen port | 8080 |
| AUTH_GATE_API_TOKEN | API bearer token | (none) | | AUTH_GATE_API_TOKEN | API bearer token | (none) |
| AUTH_GATE_BASIC_USER | Basic Auth username | admin | | AUTH_GATE_DB_PATH | SQLite database path | ./data/auth-gate.db |
| AUTH_GATE_BASIC_PASSWORD | Basic Auth password | changeme |
| PERMANENT_WHITELIST_FILE | Path to permanent whitelist file | /config/permanent_whitelist.txt |
| DATA_DIR | Data directory | /data | | DATA_DIR | Data directory | /data |
| CLEANUP_INTERVAL | Cleanup interval | 60s | | CLEANUP_INTERVAL | Cleanup interval | 60s |
| WATCH_INTERVAL | Whitelist file watch interval | 30s |
## Permanent whitelist file
```
# /config/permanent_whitelist.txt
203.0.113.10
198.51.100.0/24
```
The file is reloaded every 30 seconds (configurable).
## Graceful shutdown ## Graceful shutdown
@@ -109,5 +96,4 @@ For production, use the Dockerfile from this repository. It:
- Runs the service as a non-root user. - Runs the service as a non-root user.
- Drops all Linux capabilities. - Drops all Linux capabilities.
- Sets the no-new-privileges flag. - Sets the no-new-privileges flag.
- Mounts the whitelist file as read-only.
- Mounts /tmp as tmpfs (no persistence). - Mounts /tmp as tmpfs (no persistence).

View File

@@ -79,6 +79,21 @@ A shell script would work but:
Go's standard library is sufficient for our needs. Go's standard library is sufficient for our needs.
## Why SQLite?
SQLite provides:
- Single file — no external dependencies.
- Persistence across restarts.
- ACID transactions.
- Simple WAL mode for concurrent reads.
For a handful of IPs (dozens at most), SQLite is:
- Simple to deploy.
- Fast (single-file, no network).
- No external dependencies.
## Why in-memory temporary whitelist? ## Why in-memory temporary whitelist?
For a handful of IPs (dozens at most), an in-memory map is: For a handful of IPs (dozens at most), an in-memory map is:
@@ -94,33 +109,12 @@ If you need persistence across restarts, add a disk-backed store later.
Checking expiry on every request adds latency (two map lookups per request). Checking expiry on every request adds latency (two map lookups per request).
The cleanup goroutine is a one-time cost that keeps the hot path fast. The cleanup goroutine is a one-time cost that keeps the hot path fast.
## Why a file watcher?
The permanent whitelist file is the source of truth for permanent whitelisted IPs.
We poll the file every 30 seconds (configurable) and reload the in-memory store
if the file has changed.
## Why poll instead of inotify? ## Why poll instead of inotify?
Polling is simpler and works inside Docker containers where inotify Polling is simpler and works inside Docker containers where inotify
may not be available (no host filesystem access). may not be available (no host filesystem access).
## Why not use a database?
For our use case (a handful of IPs), a database is overkill.
We chose an in-memory map + periodic cleanup.
## Why not use Redis? ## Why not use Redis?
For our use case, Redis is overkill. For our use case, Redis is overkill.
We chose an in-memory map + periodic cleanup. We chose an in-memory map + periodic cleanup.
## Why not use SQLite?
For our use case, SQLite is overkill.
We chose an in-memory map + periodic cleanup.
## Why not use a key-value store?
For our use case, a key-value store is overkill.
We chose an in-memory map + periodic cleanup.

View File

@@ -7,7 +7,6 @@
3. Auth-gate checks: 3. Auth-gate checks:
- Is the IP in the permanent whitelist? → Allow. - Is the IP in the permanent whitelist? → Allow.
- Is the IP in the temporary whitelist and not expired? → Allow. - Is the IP in the temporary whitelist and not expired? → Allow.
- Does the Authorization header contain valid Basic Auth? → Allow.
- Otherwise → 401 Unauthorized. - Otherwise → 401 Unauthorized.
4. NGINX passes the request to the upstream service if auth-gate returned 200. 4. NGINX passes the request to the upstream service if auth-gate returned 200.
5. NGINX returns 401 to the client if auth-gate returned 401/403. 5. NGINX returns 401 to the client if auth-gate returned 401/403.
@@ -16,17 +15,15 @@
### Permanent whitelist ### Permanent whitelist
- Loaded from a file mounted as a volume. - Stored in SQLite `permanent_whitelist` table.
- The file is polled every 30 seconds (configurable).
- Only single IPs and CIDR ranges (as strings) are supported. - Only single IPs and CIDR ranges (as strings) are supported.
- The file is reloaded only when its mtime changes. - Added/removed via the `/api/whitelist/perm` API.
### Temporary whitelist ### Temporary whitelist
- In-memory store. - Stored in SQLite `temp_whitelist` table.
- Entries expire after their TTL. - Entries expire after their TTL.
- The cleanup goroutine runs every 60 seconds (configurable) to remove expired entries. - The cleanup goroutine runs every 60 seconds (configurable) to remove expired entries.
- No persistence across restarts.
## API key ## API key
@@ -37,23 +34,18 @@ in the Authorization header.
- Use a strong random string. - Use a strong random string.
- Rotate it regularly. - Rotate it regularly.
## Basic Auth ## Audit log
The Basic Auth credentials are set via environment variables. The username is `admin` All whitelist operations are logged to the `audit_log` table in SQLite.
by default, and the password is `changeme` by default. Each entry records:
- Change both immediately on first use. - `action` — what happened (`add_temp`, `delete_temp`, `add_perm`, `delete_perm`, `expire_temp`)
- Use strong passwords. - `ip` or `cidr` — the affected IP or CIDR
- The credentials are sent in the Authorization header (base64-encoded). - `ttl_seconds` — for temp entries
- Always serve the auth endpoint over TLS. - `reason` — the reason provided
- `api_client_ip` — the IP that made the API call
## File watcher Retrieve logs via the `/api/logs` endpoint.
The permanent whitelist file watcher polls the file every 30 seconds. It only
reloads if the file's mtime has changed. This means:
- Frequent touch-operations don't cause unnecessary reloads.
- The watcher is simple and doesn't depend on inotify.
## Graceful shutdown ## Graceful shutdown

14
go.mod
View File

@@ -1,3 +1,17 @@
module go.mod module go.mod
go 1.25.0 go 1.25.0
require modernc.org/sqlite v1.50.0
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.42.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

51
go.sum Normal file
View File

@@ -0,0 +1,51 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -1,51 +0,0 @@
// load_whitelist.go — helper to load the permanent whitelist at startup.
//
// This file is separate from watcher.go because the startup-time load
// is a one-time operation (no polling needed).
//
// We also handle the case where the file doesn't exist (create an empty
// one) so the service can start even if the file hasn't been mounted yet.
//
// Why create the file if it doesn't exist?
//
// - The watcher will fail to stat a non-existent file, but the service
// should start anyway. An empty whitelist file is a safe default.
package main
import (
"bufio"
"log"
"os"
)
// loadPermanentWhitelist reads the permanent whitelist file and populates
// the in-memory store.
//
// If the file doesn't exist, we create an empty one so the watcher can
// pick it up on the next poll.
func loadPermanentWhitelist(path string) {
entries := make(map[string]struct{})
f, err := os.Open(path)
if err != nil {
// File doesn't exist — create an empty one.
log.Printf("whitelist file %s not found, creating empty file", path)
if err := os.WriteFile(path, []byte("# Empty whitelist\n"), 0644); err != nil {
log.Fatalf("failed to create whitelist file: %v", err)
}
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line == "" || line[0] == '#' {
continue
}
entries[line] = struct{}{}
}
log.Printf("loaded %d entries from %s", len(entries), path)
}

View File

@@ -7,15 +7,14 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
) )
func main() { func main() {
cfg := loadConfig() cfg := loadConfig()
// Set up structured logging
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil))) slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
// Initialize database
db, err := InitDB(cfg.DBPath) db, err := InitDB(cfg.DBPath)
if err != nil { if err != nil {
slog.Error("failed to init database", "error", err) slog.Error("failed to init database", "error", err)
@@ -23,10 +22,8 @@ func main() {
} }
defer db.Close() defer db.Close()
// Start background cleanup of expired temp entries
go cleanupLoop(db, cfg.CleanupInterval) go cleanupLoop(db, cfg.CleanupInterval)
// Setup HTTP server
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/auth", authHandler(cfg, db)) mux.HandleFunc("/auth", authHandler(cfg, db))
mux.HandleFunc("/api/whitelist/temp", whitelistTempHandler(cfg, db)) mux.HandleFunc("/api/whitelist/temp", whitelistTempHandler(cfg, db))
@@ -40,7 +37,6 @@ func main() {
server := &http.Server{ server := &http.Server{
Addr: ":" + cfg.Port, Addr: ":" + cfg.Port,
Handler: mux, Handler: mux,
// timeouts as before
} }
go func() { go func() {

267
openapi.yaml Normal file
View File

@@ -0,0 +1,267 @@
openapi: "3.0.3"
info:
title: Auth-gate API
description: Lightweight Go auth gateway for IP-based whitelisting with temporary entries and audit logging.
version: "1.0.0"
contact:
name: Auth-gate Project
url: https://github.com/your-org/auth-gate
servers:
- url: http://127.0.0.1:8080
description: Local development server
security:
- BearerAuth: []
paths:
/auth:
get:
summary: Auth check (NGINX auth_request)
description: |
The NGINX auth_request endpoint. Do NOT call this directly.
Returns 200 if the client IP is whitelisted, 401 otherwise.
Only checks permanent and temporary IP whitelists.
operationId: checkAuth
parameters:
- name: X-Real-IP
in: header
required: true
schema:
type: string
description: Client IP set by NGINX
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
example: "ok"
'401':
description: Unauthorized — IP not whitelisted
/status:
get:
summary: Health check
description: Simple health check endpoint.
operationId: healthCheck
security: []
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
example: "ok\n"
/api/whitelist/temp:
post:
summary: Add a temporary whitelisted IP
description: Create a temporary whitelisted IP entry with a TTL.
operationId: addTempWhitelist
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- ip
- ttl_seconds
properties:
ip:
type: string
description: The IP address to whitelist
example: "203.0.113.10"
ttl_seconds:
type: integer
description: Time-to-live in seconds
minimum: 1
example: 300
reason:
type: string
description: Optional reason for whitelisting
example: "admin laptop"
responses:
'204':
description: Created
'400':
description: Bad request — missing or invalid parameters
content:
text/plain:
schema:
type: string
example: "ip and ttl_seconds required"
'401':
description: Unauthorized — invalid API token
'500':
description: Internal error — database failure
/api/whitelist/temp/list:
get:
summary: List temporary whitelisted IPs
description: List all currently active temporary whitelisted IPs.
operationId: listTempWhitelist
responses:
'200':
description: List of temporary whitelisted IPs
content:
application/json:
schema:
type: array
items:
type: object
properties:
ip:
type: string
example: "1.2.3.4"
reason:
type: string
example: "my laptop"
expires:
type: string
format: date-time
example: "2024-01-01T12:05:00Z"
'401':
description: Unauthorized — invalid API token
'500':
description: Internal error — database failure
/api/whitelist/temp/{ip}:
delete:
summary: Remove a temporary whitelisted IP
description: Remove a specific temporary whitelisted IP entry.
operationId: deleteTempWhitelist
parameters:
- name: ip
in: path
required: true
schema:
type: string
description: The IP address to remove
responses:
'204':
description: Deleted
'400':
description: Bad request — IP required
'401':
description: Unauthorized — invalid API token
'500':
description: Internal error
/api/whitelist/perm:
post:
summary: Add a permanent whitelisted IP or CIDR
description: Add a permanent whitelisted IP address or CIDR range.
operationId: addPermWhitelist
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- entry
properties:
entry:
type: string
description: IP address or CIDR range to whitelist
example: "203.0.113.10"
responses:
'204':
description: Created
'400':
description: Bad request — entry required
'401':
description: Unauthorized — invalid API token
'409':
description: Conflict — entry already exists
content:
text/plain:
schema:
type: string
example: "entry already exists"
'500':
description: Internal error
/api/whitelist/perm/{entry}:
delete:
summary: Remove a permanent whitelisted IP or CIDR
description: Remove a permanent whitelisted IP address or CIDR range.
operationId: deletePermWhitelist
parameters:
- name: entry
in: path
required: true
schema:
type: string
description: The IP address or CIDR range to remove
responses:
'204':
description: Deleted
'400':
description: Bad request — entry required
'401':
description: Unauthorized — invalid API token
'500':
description: Internal error
/api/logs:
get:
summary: Retrieve audit log entries
description: |
Retrieve the last 200 audit log entries.
Each entry records a whitelist operation with its timestamp, action, affected IP,
and the IP of the API client that performed the action.
operationId: getLogs
responses:
'200':
description: List of audit log entries
content:
application/json:
schema:
type: array
items:
type: object
properties:
timestamp:
type: string
format: date-time
example: "2024-01-01T12:00:00Z"
action:
type: string
example: "add_temp"
ip:
type: string
nullable: true
example: "1.2.3.4"
cidr:
type: string
nullable: true
example: "10.0.0.0/24"
ttl_seconds:
type: integer
nullable: true
example: 300
reason:
type: string
nullable: true
example: "admin laptop"
api_client_ip:
type: string
nullable: true
example: "10.0.0.1"
'401':
description: Unauthorized — invalid API token
'500':
description: Internal error — database failure
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: token
description: API token for authenticating to the API endpoints

View File

@@ -1,43 +0,0 @@
package main
import (
"database/sql"
"log/slog"
"time"
_ "modernc.org/sqlite"
)
func initDB(dbPath string) (*sql.DB, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}
query := `
CREATE TABLE IF NOT EXISTS whitelist_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
action TEXT NOT NULL,
ip TEXT NOT NULL,
ttl_seconds INTEGER DEFAULT 0,
reason TEXT
);
CREATE INDEX IF NOT EXISTS idx_timestamp ON whitelist_audit(timestamp);
`
_, err = db.Exec(query)
if err != nil {
return nil, err
}
return db, nil
}
func logWhitelistEvent(db *sql.DB, action, ip string, ttl int, reason, apiUser string) {
_, err := db.Exec(
`INSERT INTO whitelist_audit(timestamp, action, ip, ttl_seconds, reason)
VALUES(?, ?, ?, ?, ?)`,
time.Now(), action, ip, ttl, reason,
)
if err != nil {
slog.Error("failed to log whitelist event", "error", err)
}
}

View File

@@ -1,65 +0,0 @@
package main
import (
"bufio"
"log/slog"
"os"
"time"
)
type watcher struct {
path string
interval time.Duration
store *PermanentWhitelist
lastMod time.Time
}
func newPermanentWhitelistWatcher(path string, interval time.Duration, store *PermanentWhitelist) *watcher {
w := &watcher{
path: path,
interval: interval,
store: store,
}
w.reload()
return w
}
func (w *watcher) start() {
go func() {
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
for range ticker.C {
w.reload()
}
}()
}
func (w *watcher) reload() {
info, err := os.Stat(w.path)
if err != nil {
return
}
if info.ModTime().Equal(w.lastMod) {
return
}
w.lastMod = info.ModTime()
entries := make(map[string]struct{})
f, err := os.Open(w.path)
if err != nil {
slog.Error("failed to open whitelist file", "error", err)
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line == "" || line[0] == '#' {
continue
}
entries[line] = struct{}{}
}
w.store.mu.Lock()
w.store.entries = entries
w.store.mu.Unlock()
slog.Info("reloaded permanent whitelist", "count", len(entries))
}