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 version=${appVersion}
|
||||||
LABEL github="github.com/jkaninda/goma-gateway"
|
LABEL github="github.com/jkaninda/goma-gateway"
|
||||||
|
|
||||||
|
|
||||||
RUN apk --update add --no-cache tzdata ca-certificates curl
|
|
||||||
RUN mkdir -p ${WORKDIR} ${CERTSDIR} && \
|
RUN mkdir -p ${WORKDIR} ${CERTSDIR} && \
|
||||||
chmod a+rw ${WORKDIR} ${CERTSDIR}
|
chmod a+rw ${WORKDIR} ${CERTSDIR}
|
||||||
COPY --from=build /app/goma /usr/local/bin/goma
|
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
|
ln -s /usr/local/bin/goma /usr/bin/goma
|
||||||
RUN addgroup -S ${user} && adduser -S ${user} -G ${user}
|
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}
|
USER ${user}
|
||||||
WORKDIR $WORKDIR
|
WORKDIR $WORKDIR
|
||||||
ENTRYPOINT ["/usr/local/bin/goma"]
|
ENTRYPOINT ["/usr/local/bin/goma"]
|
||||||
@@ -170,6 +170,7 @@ gateway:
|
|||||||
middlewares: []
|
middlewares: []
|
||||||
|
|
||||||
#Defines proxy middlewares
|
#Defines proxy middlewares
|
||||||
|
# middleware name must be unique
|
||||||
middlewares:
|
middlewares:
|
||||||
# Enable Basic auth authorization based
|
# Enable Basic auth authorization based
|
||||||
- name: basic-auth
|
- name: basic-auth
|
||||||
@@ -191,6 +192,7 @@ middlewares:
|
|||||||
paths:
|
paths:
|
||||||
- /protected-access
|
- /protected-access
|
||||||
- /example-of-jwt
|
- /example-of-jwt
|
||||||
|
#- /* or wildcard path
|
||||||
rule:
|
rule:
|
||||||
# This is an example URL
|
# This is an example URL
|
||||||
url: https://www.googleapis.com/auth/userinfo.email
|
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:
|
paths:
|
||||||
- /protected-access
|
- /protected-access
|
||||||
- /example-of-jwt
|
- /example-of-jwt
|
||||||
|
#- /* or wildcard path
|
||||||
rule:
|
rule:
|
||||||
# This is an example URL
|
# This is an example URL
|
||||||
url: https://www.googleapis.com/auth/userinfo.email
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, block := range blockList.List {
|
for _, block := range blockList.List {
|
||||||
if isPathBlocked(r.URL.Path, util.ParseURLPath(blockList.Path+block)) {
|
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.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
||||||
Success: false,
|
Success: false,
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusForbidden,
|
||||||
Message: fmt.Sprintf("Not found: %s", r.URL.Path),
|
Message: fmt.Sprintf("You do not have permission to access this resource"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -57,11 +57,9 @@ func (intercept InterceptErrors) ErrorInterceptor(next http.Handler) http.Handle
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
rec := newResponseRecorder(w)
|
rec := newResponseRecorder(w)
|
||||||
next.ServeHTTP(rec, r)
|
next.ServeHTTP(rec, r)
|
||||||
//Set Server name
|
|
||||||
w.Header().Set("Server", "Goma")
|
|
||||||
if canIntercept(rec.statusCode, intercept.Errors) {
|
if canIntercept(rec.statusCode, intercept.Errors) {
|
||||||
logger.Debug("Backend error intercepted")
|
logger.Error("Backend error")
|
||||||
logger.Debug("An error occurred in the backend, %d", rec.statusCode)
|
logger.Error("An error occurred in the backend, %d", rec.statusCode)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(rec.statusCode)
|
w.WriteHeader(rec.statusCode)
|
||||||
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ func (jwtAuth JwtAuth) AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
if r.Header.Get(header) == "" {
|
if r.Header.Get(header) == "" {
|
||||||
logger.Error("Proxy error, missing %s header", header)
|
logger.Error("Proxy error, missing %s header", header)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
||||||
Message: "Missing Authorization header",
|
Message: http.StatusText(http.StatusUnauthorized),
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusUnauthorized,
|
||||||
Success: false,
|
Success: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (rl *RateLimiter) RateLimitMiddleware() mux.MiddlewareFunc {
|
|||||||
rl.mu.Unlock()
|
rl.mu.Unlock()
|
||||||
|
|
||||||
if client.RequestCount > rl.Requests {
|
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.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
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)
|
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
|
// Custom error handler for proxy errors
|
||||||
proxy.ErrorHandler = ProxyErrorHandler
|
proxy.ErrorHandler = ProxyErrorHandler
|
||||||
proxy.ServeHTTP(w, r)
|
proxy.ServeHTTP(w, r)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
|||||||
disableXForward: route.DisableHeaderXForward,
|
disableXForward: route.DisableHeaderXForward,
|
||||||
cors: route.Cors,
|
cors: route.Cors,
|
||||||
}
|
}
|
||||||
secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + midPath)).Subrouter()
|
secureRouter := r.PathPrefix(util.ParseRoutePath(route.Path, midPath)).Subrouter()
|
||||||
//Check Authentication middleware
|
//Check Authentication middleware
|
||||||
switch rMiddleware.Type {
|
switch rMiddleware.Type {
|
||||||
case BasicAuth:
|
case BasicAuth:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package pkg
|
|||||||
const ConfigFile = "/config/goma.yml" // Default configuration file
|
const ConfigFile = "/config/goma.yml" // Default configuration file
|
||||||
const accessControlAllowOrigin = "Access-Control-Allow-Origin" // Cors
|
const accessControlAllowOrigin = "Access-Control-Allow-Origin" // Cors
|
||||||
const serverName = "Goma"
|
const serverName = "Goma"
|
||||||
|
const gatewayName = "Goma Gateway"
|
||||||
const AccessMiddleware = "access" // access middleware
|
const AccessMiddleware = "access" // access middleware
|
||||||
const BasicAuth = "basic" // basic authentication middleware
|
const BasicAuth = "basic" // basic authentication middleware
|
||||||
const JWTAuth = "jwt" // JWT authentication middleware
|
const JWTAuth = "jwt" // JWT authentication middleware
|
||||||
|
|||||||
@@ -82,3 +82,17 @@ func ParseURLPath(urlPath string) string {
|
|||||||
}
|
}
|
||||||
return urlPath
|
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