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:
@@ -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"]
|
||||
@@ -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
14
examples/compose.yaml
Normal 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
152
examples/configMap.yaml
Normal 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
44
examples/kubernetes.yaml
Normal 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
|
||||
1
goma.yml
1
goma.yml
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user