From 7116528ad763d8223b9fadf2d5ea3cc454496db2 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 15:38:05 +0100 Subject: [PATCH 1/6] chore: update health check link --- internal/routes.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/routes.go b/internal/routes.go index 4047b26..0fc73f0 100644 --- a/internal/routes.go +++ b/internal/routes.go @@ -83,11 +83,12 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } // Routes health check if !gateway.DisableHealthCheckStatus { - r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") + r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") // Deprecated + r.HandleFunc("/healthz/routes", heath.HealthCheckHandler).Methods("GET") } // Health check - r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") + r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") // Deprecated r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET") r.HandleFunc("/healthz", heath.HealthReadyHandler).Methods("GET") // Enable common exploits From 0fc5ef52ff116258e6eb4dcc82cabe2f0459d0b1 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 15:59:59 +0100 Subject: [PATCH 2/6] docs: add access policy middleware --- README.md | 3 +++ docs/index.md | 3 +++ docs/middleware/access-policy.md | 32 ++++++++++++++++++++++++++++++++ docs/middleware/basic.md | 2 +- docs/middleware/jwt.md | 2 +- docs/middleware/oauth.md | 2 +- docs/middleware/rate-limit.md | 6 +++--- 7 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 docs/middleware/access-policy.md diff --git a/README.md b/README.md index 53991ee..69f40c3 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,9 @@ It's designed to be straightforward and efficient, offering a rich set of featur - **Authentication Middleware** - Support for **JWT** with client authorization based on request results. - **Basic-Auth** and **OAuth** authentication mechanisms. +- **Access Policy Middleware** + + The Access Policy middleware controls route access by either `allowing` or `denying` requests based on defined rules. ### Monitoring and Performance - **Logging** diff --git a/docs/index.md b/docs/index.md index 8776ea7..3c65194 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,9 @@ It's designed to be straightforward and efficient, offering a rich set of featur - **Authentication Middleware** - Support for **JWT** with client authorization based on request results. - **Basic-Auth** and **OAuth** authentication mechanisms. +- **Access Policy Middleware** + + The Access Policy middleware controls route access by either `allowing` or `denying` requests based on defined rules. ### Monitoring and Performance - **Logging** diff --git a/docs/middleware/access-policy.md b/docs/middleware/access-policy.md new file mode 100644 index 0000000..e7d255b --- /dev/null +++ b/docs/middleware/access-policy.md @@ -0,0 +1,32 @@ +--- +title: Access Policy +layout: default +parent: Middleware +nav_order: 3 +--- + + +### Access Policy Middleware +The Access Policy middleware controls route access by either allowing or denying requests based on defined rules. +It supports two actions: `ALLOW` and `DENY`. + +### How It Works +1. **Define an action:** Specify whether the middleware should `ALLOW` or `DENY` access. + +2. **Set sourceRanges:** Provide a list of IP addresses or IP ranges to which the policy applies. + + Requests originating from these sources will be evaluated according to the specified action. + +#### Example Configuration +Here’s an example of an Access Policy middleware configuration in YAML: + +```yaml +middlewares: + - name: access-policy + type: accessPolicy + rule: + action: DENY # Specify either DENY or ALLOW + sourceRanges: + - 192.168.1.1 # Single IP address + - 172.18.0.0-172.18.0.10 # IP range +``` diff --git a/docs/middleware/basic.md b/docs/middleware/basic.md index 41c758c..e96dedd 100644 --- a/docs/middleware/basic.md +++ b/docs/middleware/basic.md @@ -2,7 +2,7 @@ title: Basic auth layout: default parent: Middleware -nav_order: 3 +nav_order: 4 --- diff --git a/docs/middleware/jwt.md b/docs/middleware/jwt.md index 7628510..6e25479 100644 --- a/docs/middleware/jwt.md +++ b/docs/middleware/jwt.md @@ -2,7 +2,7 @@ title: JWT Middleware layout: default parent: Middleware -nav_order: 4 +nav_order: 5 --- diff --git a/docs/middleware/oauth.md b/docs/middleware/oauth.md index 13e4068..32f3e34 100644 --- a/docs/middleware/oauth.md +++ b/docs/middleware/oauth.md @@ -2,7 +2,7 @@ title: OAuth auth layout: default parent: Middleware -nav_order: 5 +nav_order: 6 --- # OAuth middleware diff --git a/docs/middleware/rate-limit.md b/docs/middleware/rate-limit.md index 35eec6a..f78c43a 100644 --- a/docs/middleware/rate-limit.md +++ b/docs/middleware/rate-limit.md @@ -1,8 +1,8 @@ --- -title: Rate Limit +title: Rate Limiting layout: default parent: Middleware -nav_order: 6 +nav_order: 7 --- @@ -15,7 +15,7 @@ Example of rate limiting middleware ```yaml middlewares: - name: rate-limit - type: ratelimit #or rateLimit + type: rateLimit #or ratelimit paths: - /* rule: From af2b0cbce1747db48f247a0c617dabd781961c9a Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 17:39:51 +0100 Subject: [PATCH 3/6] feat: add access middleware support ip range --- .../middlewares/access_policy_middleware.go | 115 +++++++++++++++--- internal/routes.go | 8 +- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/internal/middlewares/access_policy_middleware.go b/internal/middlewares/access_policy_middleware.go index aaef055..48d6df2 100644 --- a/internal/middlewares/access_policy_middleware.go +++ b/internal/middlewares/access_policy_middleware.go @@ -21,6 +21,7 @@ import ( "github.com/jkaninda/goma-gateway/pkg/logger" "net" "net/http" + "strings" ) type AccessPolicy struct { @@ -30,33 +31,109 @@ type AccessPolicy struct { func (access AccessPolicy) AccessPolicyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - iPs := make(map[string]struct{}) - for _, ip := range access.SourceRanges { - iPs[ip] = struct{}{} - } // Get the client's IP address - ip, _, err := net.SplitHostPort(getRealIP(r)) + clientIP, _, err := net.SplitHostPort(getRealIP(r)) if err != nil { logger.Error("Unable to parse IP address") RespondWithError(w, http.StatusUnauthorized, "Unable to parse IP address") return } - // Check if the IP is in the blocklist - if access.Action == "DENY" { - if _, ok := iPs[ip]; ok { - logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return + for index, entry := range access.SourceRanges { + // Check if the IP is in the blocklist + if access.Action == "DENY" { + if strings.Contains(entry, "-") { + // Handle IP range + startIP, endIP, err := parseIPRange(entry) + if err == nil && ipInRange(clientIP, startIP, endIP) { + logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + return + } + continue + } else { + // Handle single IP + if clientIP == entry { + logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + return + } + if index == len(access.SourceRanges)-1 { + next.ServeHTTP(w, r) + return + } + continue + } + + } else { + // Check if the IP is in the allowlist + if strings.Contains(entry, "-") { + // Handle IP range + startIP, endIP, err := parseIPRange(entry) + if err == nil && ipInRange(clientIP, startIP, endIP) { + next.ServeHTTP(w, r) + return + } + continue + } else { + // Handle single IP + if clientIP == entry { + next.ServeHTTP(w, r) + return + } + if index == len(access.SourceRanges)-1 { + next.ServeHTTP(w, r) + return + } + continue + } } } - // Check if the IP is in the allowlist - if _, ok := iPs[ip]; !ok { - logger.Error("%s: IP address not allowed ", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return - } - // Continue to the next handler if the authentication is successful - next.ServeHTTP(w, r) + logger.Error("%s: IP address not allowed ", getRealIP(r)) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + return }) } + +// / Parse a range string into start and end IPs +func parseIPRange(rangeStr string) (string, string, error) { + parts := strings.Split(rangeStr, "-") + if len(parts) != 2 { + return "", "", http.ErrAbortHandler + } + + startIP := strings.TrimSpace(parts[0]) + endIP := strings.TrimSpace(parts[1]) + + if net.ParseIP(startIP) == nil || net.ParseIP(endIP) == nil { + return "", "", http.ErrAbortHandler + } + + return startIP, endIP, nil +} + +// Check if an IP is in range +func ipInRange(ipStr, startIP, endIP string) bool { + ip := net.ParseIP(ipStr) + start := net.ParseIP(startIP) + end := net.ParseIP(endIP) + + if ip == nil || start == nil || end == nil { + return false + } + + ipBytes := ip.To4() + startBytes := start.To4() + endBytes := end.To4() + + if ipBytes == nil || startBytes == nil || endBytes == nil { + return false + } + + for i := 0; i < 4; i++ { + if ipBytes[i] < startBytes[i] || ipBytes[i] > endBytes[i] { + return false + } + } + return true +} diff --git a/internal/routes.go b/internal/routes.go index 0fc73f0..f83b31c 100644 --- a/internal/routes.go +++ b/internal/routes.go @@ -250,14 +250,14 @@ func attachMiddlewares(rIndex int, route Route, gateway Gateway, router *mux.Rou } // AccessPolicy if accessPolicy == mid.Type { - accessPolicy, err := getAccessPoliciesMiddleware(mid.Rule) + a, err := getAccessPoliciesMiddleware(mid.Rule) if err != nil { logger.Error("Error: %v, middleware not applied", err.Error()) } - if len(accessPolicy.SourceRanges) != 0 { + if len(a.SourceRanges) != 0 { access := middlewares.AccessPolicy{ - SourceRanges: accessPolicy.SourceRanges, - Action: accessPolicy.Action, + SourceRanges: a.SourceRanges, + Action: a.Action, } router.Use(access.AccessPolicyMiddleware) } From 36fb317367c8a3401024d9e2cde12d733f76f059 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 18:13:24 +0100 Subject: [PATCH 4/6] refactor: improvement of access policy middleware --- internal/middlewares/access_policy_middleware.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/middlewares/access_policy_middleware.go b/internal/middlewares/access_policy_middleware.go index 48d6df2..d979ab0 100644 --- a/internal/middlewares/access_policy_middleware.go +++ b/internal/middlewares/access_policy_middleware.go @@ -49,6 +49,10 @@ func (access AccessPolicy) AccessPolicyMiddleware(next http.Handler) http.Handle RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) return } + if index == len(access.SourceRanges)-1 { + next.ServeHTTP(w, r) + return + } continue } else { // Handle single IP From 7e3489e201bcc457177ddc751c517bb72015b8f4 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 18:19:24 +0100 Subject: [PATCH 5/6] refactor: improvement of access policy middleware --- .../middlewares/access_policy_middleware.go | 81 +++++++------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/internal/middlewares/access_policy_middleware.go b/internal/middlewares/access_policy_middleware.go index d979ab0..7858b97 100644 --- a/internal/middlewares/access_policy_middleware.go +++ b/internal/middlewares/access_policy_middleware.go @@ -38,65 +38,40 @@ func (access AccessPolicy) AccessPolicyMiddleware(next http.Handler) http.Handle RespondWithError(w, http.StatusUnauthorized, "Unable to parse IP address") return } - for index, entry := range access.SourceRanges { - // Check if the IP is in the blocklist - if access.Action == "DENY" { - if strings.Contains(entry, "-") { - // Handle IP range - startIP, endIP, err := parseIPRange(entry) - if err == nil && ipInRange(clientIP, startIP, endIP) { - logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return - } - if index == len(access.SourceRanges)-1 { - next.ServeHTTP(w, r) - return - } - continue - } else { - // Handle single IP - if clientIP == entry { - logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return - } - if index == len(access.SourceRanges)-1 { - next.ServeHTTP(w, r) - return - } - continue - } - } else { - // Check if the IP is in the allowlist - if strings.Contains(entry, "-") { - // Handle IP range - startIP, endIP, err := parseIPRange(entry) - if err == nil && ipInRange(clientIP, startIP, endIP) { - next.ServeHTTP(w, r) - return - } - continue + // Check IP against source ranges + isAllowed := access.Action != "DENY" + for _, entry := range access.SourceRanges { + if isIPAllowed(clientIP, entry) { + if isAllowed { + next.ServeHTTP(w, r) } else { - // Handle single IP - if clientIP == entry { - next.ServeHTTP(w, r) - return - } - if index == len(access.SourceRanges)-1 { - next.ServeHTTP(w, r) - return - } - continue + logger.Error("%s: IP address in the blocklist, access not allowed", clientIP) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) } + return } } - logger.Error("%s: IP address not allowed ", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return - }) + // Final response for disallowed IPs + if isAllowed { + logger.Error("%s: IP address not allowed", clientIP) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + } else { + next.ServeHTTP(w, r) + } + }) +} + +// isIPAllowed checks if a client IP matches an entry (range or single IP). +func isIPAllowed(clientIP, entry string) bool { + if strings.Contains(entry, "-") { + // Handle IP range + startIP, endIP, err := parseIPRange(entry) + return err == nil && ipInRange(clientIP, startIP, endIP) + } + // Handle single IP + return clientIP == entry } // / Parse a range string into start and end IPs From 89a6f3fffd1dacac58104d8666aec568250227d3 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Mon, 9 Dec 2024 18:33:44 +0100 Subject: [PATCH 6/6] feat: add access policy middleware support cidr block --- docs/middleware/access-policy.md | 5 +++-- internal/config.go | 4 ---- .../middlewares/access_policy_middleware.go | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/middleware/access-policy.md b/docs/middleware/access-policy.md index e7d255b..1f0df7d 100644 --- a/docs/middleware/access-policy.md +++ b/docs/middleware/access-policy.md @@ -13,7 +13,7 @@ It supports two actions: `ALLOW` and `DENY`. ### How It Works 1. **Define an action:** Specify whether the middleware should `ALLOW` or `DENY` access. -2. **Set sourceRanges:** Provide a list of IP addresses or IP ranges to which the policy applies. +2. **Set sourceRanges:** Provide a list of IP addresses, IP ranges or a CIDR block to which the policy applies. Requests originating from these sources will be evaluated according to the specified action. @@ -28,5 +28,6 @@ middlewares: action: DENY # Specify either DENY or ALLOW sourceRanges: - 192.168.1.1 # Single IP address - - 172.18.0.0-172.18.0.10 # IP range + - 10.42.1.1-10.42.1.100 # IP range + - 10.42.1.1/16 # CIDR block ``` diff --git a/internal/config.go b/internal/config.go index c143022..8b98a43 100644 --- a/internal/config.go +++ b/internal/config.go @@ -307,10 +307,6 @@ func getAccessPoliciesMiddleware(input interface{}) (AccessPolicyRuleMiddleware, if !validateCIDR(ip) { return AccessPolicyRuleMiddleware{}, fmt.Errorf("invalid cidr address") } - if validateCIDR(ip) { - return AccessPolicyRuleMiddleware{}, fmt.Errorf("cidr is not yet supported") - - } } } diff --git a/internal/middlewares/access_policy_middleware.go b/internal/middlewares/access_policy_middleware.go index 7858b97..b60d403 100644 --- a/internal/middlewares/access_policy_middleware.go +++ b/internal/middlewares/access_policy_middleware.go @@ -63,13 +63,18 @@ func (access AccessPolicy) AccessPolicyMiddleware(next http.Handler) http.Handle }) } -// isIPAllowed checks if a client IP matches an entry (range or single IP). +// isIPAllowed checks if a client IP matches an entry (range, single IP or CIDR block). func isIPAllowed(clientIP, entry string) bool { + // Handle IP range if strings.Contains(entry, "-") { // Handle IP range startIP, endIP, err := parseIPRange(entry) return err == nil && ipInRange(clientIP, startIP, endIP) } + // Handle CIDR + if strings.Contains(entry, "/") { + return ipInCIDR(clientIP, entry) + } // Handle single IP return clientIP == entry } @@ -116,3 +121,13 @@ func ipInRange(ipStr, startIP, endIP string) bool { } return true } + +// Check if an IP is within a CIDR block +func ipInCIDR(ipStr, cidr string) bool { + ip := net.ParseIP(ipStr) + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + return ipNet.Contains(ip) +}