feat: Add wildcard auth middleware paths (#24)

* chore: add concurrent route health check requests
* feat: Add wildcard auth middleware paths
* fix: bind privileged port permission denied on Kubernetes for nonroot user
This commit is contained in:
2024-11-02 11:55:37 +01:00
committed by GitHub
parent 778a098bdc
commit fe81ac7324
14 changed files with 243 additions and 17 deletions

View File

@@ -20,15 +20,13 @@ LABEL author="Jonas Kaninda"
LABEL version=${appVersion}
LABEL github="github.com/jkaninda/goma-gateway"
RUN apk --update add --no-cache tzdata ca-certificates curl
RUN mkdir -p ${WORKDIR} ${CERTSDIR} && \
chmod a+rw ${WORKDIR} ${CERTSDIR}
COPY --from=build /app/goma /usr/local/bin/goma
RUN chmod +x /usr/local/bin/goma && \
RUN chmod a+x /usr/local/bin/goma && \
ln -s /usr/local/bin/goma /usr/bin/goma
RUN addgroup -S ${user} && adduser -S ${user} -G ${user}
RUN apk --update add --no-cache tzdata ca-certificates curl libcap && setcap 'cap_net_bind_service=+ep' /usr/local/bin/goma
USER ${user}
WORKDIR $WORKDIR
ENTRYPOINT ["/usr/local/bin/goma"]

View File

@@ -170,6 +170,7 @@ gateway:
middlewares: []
#Defines proxy middlewares
# middleware name must be unique
middlewares:
# Enable Basic auth authorization based
- name: basic-auth
@@ -191,6 +192,7 @@ middlewares:
paths:
- /protected-access
- /example-of-jwt
#- /* or wildcard path
rule:
# This is an example URL
url: https://www.googleapis.com/auth/userinfo.email

14
examples/compose.yaml Normal file
View File

@@ -0,0 +1,14 @@
services:
goma-gateway:
image: jkaninda/goma-gateway
command: server
healthcheck:
test: curl -f http://localhost/healthz || exit 1
interval: 30s
retries: 5
start_period: 20s
timeout: 10s
ports:
- "80:80"
volumes:
- ./config:/config/

152
examples/configMap.yaml Normal file
View File

@@ -0,0 +1,152 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: goma-config
data:
goma.yml: |
gateway:
########## Global settings
listenAddr: 0.0.0.0:80
# Proxy write timeout
writeTimeout: 15
# Proxy read timeout
readTimeout: 15
# Proxy idle timeout
idleTimeout: 60
# Proxy rate limit, it's In-Memory IP based
# Distributed Rate Limiting for Token based across multiple instances is not yet integrated
rateLimiter: 0
accessLog: "/dev/Stdout"
errorLog: "/dev/stderr"
## Returns backend route healthcheck errors
disableRouteHealthCheckError: false
# Disable display routes on start
disableDisplayRouteOnStart: false
# disableKeepAlive allows enabling and disabling KeepALive server
disableKeepAlive: false
# interceptErrors intercepts backend errors based on defined the status codes
interceptErrors:
- 405
- 500
# - 400
# Proxy Global HTTP Cors
cors:
# Global routes cors for all routes
origins:
- http://localhost:8080
- https://example.com
# Global routes cors headers for all routes
headers:
Access-Control-Allow-Headers: 'Origin, Authorization, Accept, Content-Type, Access-Control-Allow-Headers, X-Client-Id, X-Session-Id'
Access-Control-Allow-Credentials: 'true'
Access-Control-Max-Age: 1728000
##### Define routes
routes:
# Example of a route | 1
- name: Public
# host Domain/host based request routing
host: "" # Host is optional
path: /public
## Rewrite a request path
# e.g rewrite: /store to /
rewrite: /healthz
destination: https://example.com
#DisableHeaderXForward Disable X-forwarded header.
# [X-Forwarded-Host, X-Forwarded-For, Host, Scheme ]
# It will not match the backend route, by default, it's disabled
disableHeaderXForward: false
# Internal health check
healthCheck: '' #/internal/health/ready
# Route Cors, global cors will be overridden by route
cors:
# Route Origins Cors, global cors will be overridden by route
origins:
- https://dev.example.com
- http://localhost:3000
- https://example.com
# Route Cors headers, route will override global cors
headers:
Access-Control-Allow-Methods: 'GET'
Access-Control-Allow-Headers: 'Origin, Authorization, Accept, Content-Type, Access-Control-Allow-Headers, X-Client-Id, X-Session-Id'
Access-Control-Allow-Credentials: 'true'
Access-Control-Max-Age: 1728000
##### Define route middlewares from middlewares names
## The name must be unique
## List of middleware name
middlewares:
- api-forbidden-paths
- basic-auth
# Example of a route | 2
- name: Authentication service
path: /auth
rewrite: /
destination: 'http://security-service:8080'
healthCheck: /internal/health/ready
cors: {}
middlewares:
- api-forbidden-paths
# Example of a route | 3
- name: Basic auth
path: /protected
rewrite: /
destination: 'http://notification-service:8080'
healthCheck:
cors: {}
middlewares: []
#Defines proxy middlewares
# middleware name must be unique
middlewares:
# Enable Basic auth authorization based
- name: basic-auth
# Authentication types | jwt, basic, OAuth
type: basic
paths:
- /user
- /admin
- /account
rule:
username: admin
password: admin
#Enables JWT authorization based on the result of a request and continues the request.
- name: google-auth
# Authentication types | jwt, basic, OAuth
# jwt authorization based on the result of backend's response and continue the request when the client is authorized
type: jwt
# Paths to protect
paths:
- /protected-access
- /example-of-jwt
#- /* or wildcard path
rule:
# This is an example URL
url: https://www.googleapis.com/auth/userinfo.email
# Required headers, if not present in the request, the proxy will return 403
requiredHeaders:
- Authorization
#Sets the request variable to the given value after the authorization request completes.
#
# Add header to the next request from AuthRequest header, depending on your requirements
# Key is AuthRequest's response header Key, and value is Request's header Key
# In case you want to get headers from the Authentication service and inject them into the next request's headers
#Sets the request variable to the given value after the authorization request completes.
#
# Add header to the next request from AuthRequest header, depending on your requirements
# Key is AuthRequest's response header Key, and value is Request's header Key
# In case you want to get headers from the Authentication service and inject them into the next request's headers
headers:
userId: X-Auth-UserId
userCountryId: X-Auth-UserCountryId
# In case you want to get headers from the Authentication service and inject them to the next request's params
params:
userCountryId: countryId
# The server will return 404
- name: api-forbidden-paths
type: access
## Forbidden paths
paths:
- /swagger-ui/*
- /v2/swagger-ui/*
- /api-docs/*
- /internal/*
- /actuator/*

44
examples/kubernetes.yaml Normal file
View File

@@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: goma-gateway
spec:
selector:
matchLabels:
app: goma-gateway
template:
metadata:
labels:
app: goma-gateway
spec:
containers:
- name: goma-gateway
image: jkaninda/goma-gateway
command: ["goma","server"]
resources:
limits:
memory: "128Mi"
cpu: "200m"
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 80
initialDelaySeconds: 15
periodSeconds: 40
timeoutSeconds: 10
volumeMounts:
- name: config
mountPath: /config2/
volumes:
- name: config
configMap:
name: goma-config

View File

@@ -113,6 +113,7 @@ middlewares:
paths:
- /protected-access
- /example-of-jwt
#- /* or wildcard path
rule:
# This is an example URL
url: https://www.googleapis.com/auth/userinfo.email

View File

@@ -30,13 +30,13 @@ func (blockList AccessListMiddleware) AccessMiddleware(next http.Handler) http.H
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, block := range blockList.List {
if isPathBlocked(r.URL.Path, util.ParseURLPath(blockList.Path+block)) {
logger.Debug("%s: %s access forbidden", getRealIP(r), r.URL.Path)
logger.Error("%s: %s access forbidden", getRealIP(r), r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.WriteHeader(http.StatusForbidden)
err := json.NewEncoder(w).Encode(ProxyResponseError{
Success: false,
Code: http.StatusNotFound,
Message: fmt.Sprintf("Not found: %s", r.URL.Path),
Code: http.StatusForbidden,
Message: fmt.Sprintf("You do not have permission to access this resource"),
})
if err != nil {
return

View File

@@ -57,11 +57,9 @@ func (intercept InterceptErrors) ErrorInterceptor(next http.Handler) http.Handle
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := newResponseRecorder(w)
next.ServeHTTP(rec, r)
//Set Server name
w.Header().Set("Server", "Goma")
if canIntercept(rec.statusCode, intercept.Errors) {
logger.Debug("Backend error intercepted")
logger.Debug("An error occurred in the backend, %d", rec.statusCode)
logger.Error("Backend error")
logger.Error("An error occurred in the backend, %d", rec.statusCode)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(rec.statusCode)
err := json.NewEncoder(w).Encode(ProxyResponseError{

View File

@@ -104,10 +104,10 @@ func (jwtAuth JwtAuth) AuthMiddleware(next http.Handler) http.Handler {
if r.Header.Get(header) == "" {
logger.Error("Proxy error, missing %s header", header)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.WriteHeader(http.StatusUnauthorized)
err := json.NewEncoder(w).Encode(ProxyResponseError{
Message: "Missing Authorization header",
Code: http.StatusForbidden,
Message: http.StatusText(http.StatusUnauthorized),
Code: http.StatusUnauthorized,
Success: false,
})
if err != nil {

View File

@@ -66,7 +66,7 @@ func (rl *RateLimiter) RateLimitMiddleware() mux.MiddlewareFunc {
rl.mu.Unlock()
if client.RequestCount > rl.Requests {
logger.Debug("Too many request from IP: %s %s %s", clientID, r.URL, r.UserAgent())
logger.Error("Too many request from IP: %s %s %s", clientID, r.URL, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(ProxyResponseError{

View File

@@ -88,6 +88,8 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc {
r.URL.Path = strings.Replace(r.URL.Path, fmt.Sprintf("%s/", proxyRoute.path), proxyRoute.rewrite, 1)
}
}
w.Header().Set("Proxied-By", gatewayName) //Set Server name
w.Header().Set("Server", serverName)
// Custom error handler for proxy errors
proxy.ErrorHandler = ProxyErrorHandler
proxy.ServeHTTP(w, r)

View File

@@ -76,7 +76,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
disableXForward: route.DisableHeaderXForward,
cors: route.Cors,
}
secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + midPath)).Subrouter()
secureRouter := r.PathPrefix(util.ParseRoutePath(route.Path, midPath)).Subrouter()
//Check Authentication middleware
switch rMiddleware.Type {
case BasicAuth:

View File

@@ -3,6 +3,7 @@ package pkg
const ConfigFile = "/config/goma.yml" // Default configuration file
const accessControlAllowOrigin = "Access-Control-Allow-Origin" // Cors
const serverName = "Goma"
const gatewayName = "Goma Gateway"
const AccessMiddleware = "access" // access middleware
const BasicAuth = "basic" // basic authentication middleware
const JWTAuth = "jwt" // JWT authentication middleware

View File

@@ -82,3 +82,17 @@ func ParseURLPath(urlPath string) string {
}
return urlPath
}
func ParseRoutePath(path, blockedPath string) string {
basePath := ParseURLPath(path)
switch {
case blockedPath == "":
return basePath
case strings.HasSuffix(blockedPath, "/*"):
return basePath + blockedPath[:len(blockedPath)-2]
case strings.HasSuffix(blockedPath, "*"):
return basePath + blockedPath[:len(blockedPath)-1]
default:
return basePath + blockedPath
}
}