From 1b798567cb17361bed22475bd8ca5e034b5ffde1 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Thu, 5 Aug 2021 23:10:28 +0200 Subject: [PATCH] Add option to get user information from a request This commit adds a new authentication middleware for the kobs API. This middleware is used to get user information (e.g. the email address of an user) from a HTTP header. The header can be set via the "--api.auth-header" flag. If this header is present kobs will log all requests including a user field. We also added an example how this header can be passed to kobs, when kobs is protected using the OAuth2 Proxy. In the future we will use this feature to extend some plugins like the Opsgenie plugin, where we can use this user for additional actions like acknowledge or snooze alerts. --- CHANGELOG.md | 2 + deploy/helm/kobs/Chart.yaml | 2 +- deploy/helm/kobs/templates/ingress.yaml | 4 +- docs/configuration/authentication.md | 266 ++++++++++++++++++++++++ docs/configuration/getting-started.md | 1 + mkdocs.yml | 1 + pkg/api/api.go | 2 + pkg/api/middleware/auth/auth.go | 54 +++++ pkg/api/middleware/httplog/httplog.go | 6 + 9 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 docs/configuration/authentication.md create mode 100644 pkg/api/middleware/auth/auth.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2faa08917..7e1ca938c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan ### Added +- [#103](https://github.com/kobsio/kobs/pull/103): Add option to get user information from a request. + ### Fixed - [#102](https://github.com/kobsio/kobs/pull/102): Fix GitHub Action for creating a new Helm release. diff --git a/deploy/helm/kobs/Chart.yaml b/deploy/helm/kobs/Chart.yaml index 3fa0640de..beeae6785 100644 --- a/deploy/helm/kobs/Chart.yaml +++ b/deploy/helm/kobs/Chart.yaml @@ -4,5 +4,5 @@ description: Kubernetes Observability Platform type: application home: https://kobs.io icon: https://kobs.io/assets/images/logo.svg -version: 0.5.2 +version: 0.5.3 appVersion: v0.5.0 diff --git a/deploy/helm/kobs/templates/ingress.yaml b/deploy/helm/kobs/templates/ingress.yaml index e8f87848d..e58526862 100644 --- a/deploy/helm/kobs/templates/ingress.yaml +++ b/deploy/helm/kobs/templates/ingress.yaml @@ -34,10 +34,10 @@ spec: pathType: Prefix backend: serviceName: {{ $fullName }} - servicePort: 15220 + servicePort: http-api - path: / backend: serviceName: {{ $fullName }} - servicePort: 15219 + servicePort: http-web {{- end }} {{- end }} diff --git a/docs/configuration/authentication.md b/docs/configuration/authentication.md new file mode 100644 index 000000000..665bfce05 --- /dev/null +++ b/docs/configuration/authentication.md @@ -0,0 +1,266 @@ +# Authentication + +kobs hasn't any built in authentication mechanism. We recommend to run kobs behind a service like [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/), which should handle the authentication of users. + +## Examples + +The following two examples show how you can setup kobs with an OAuth2 Proxy infront using the [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/) or [Istio](https://istio.io). Before you are looking into the examples, make sure you have setup your prefered [OAuth Provider](https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider). We will use Google as our OAuth Provider in the following, which requires a Client ID and a Client Secret. + +We are installing kobs into a namespace named `kobs` using the provided [Helm Chart](../installation/helm.md). It will be available at [demo.kobs.io](https://demo.kobs.io), so keep in mind that you have to adjust the domain for your setup. + +### NGINX Ingress Controller + +In the first step we have to create a Deployment, Service and Ingress for the OAuth2 Proxy. With the example wich can be found in the following we are exposing the OAuth2 Proxy via the [oauth2-proxy.kobs.io](https://oauth2-proxy.kobs.io) domain. We are also setting all flags and secrets to use Google as our OAuth Provider and the `--set-authorization-header` and `--set-xauthrequest` flags to pass the email address of the authenticated user to kobs. + +??? note "OAuth2 Proxy" + + ```yaml + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: oauth2-proxy + spec: + replicas: 1 + selector: + matchLabels: + app: oauth2-proxy + template: + metadata: + labels: + app: oauth2-proxy + spec: + containers: + - name: kobs + image: quay.io/oauth2-proxy/oauth2-proxy:v7.1.2 + args: + - --provider=google + - --skip-provider-button=true + - --oidc-issuer-url=https://accounts.google.com + - --upstream=static://200 + - --http-address=0.0.0.0:4180 + - --email-domain=kobs.io + - --cookie-domain=.kobs.io + - --whitelist-domain=.kobs.io + - --set-authorization-header=true + - --set-xauthrequest=true + env: + # For the sake of simplicity we directly setting the Client ID and Client Secret as value. In a production + # environment you should set the values for these environment variables from a secret. + - name: OAUTH2_PROXY_CLIENT_ID + value: + - name: OAUTH2_PROXY_CLIENT_SECRET + value: + # The Cookie Secret can be generated using the following command: + # python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())' + - name: OAUTH2_PROXY_COOKIE_SECRET + value: 7jctnRZlsQRSFaX76LK53w== + - name: OAUTH2_PROXY_COOKIE_NAME + value: kobs-demo + ports: + - containerPort: 4180 + name: http + protocol: TCP + + --- + apiVersion: v1 + kind: Service + metadata: + name: oauth2-proxy + spec: + type: ClusterIP + selector: + app: oauth2-proxy + ports: + - name: http + port: 4180 + protocol: TCP + targetPort: http + + --- + apiVersion: extensions/v1beta1 + kind: Ingress + metadata: + name: oauth2-proxy + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + spec: + rules: + - host: oauth2-proxy.kobs.io + http: + paths: + - backend: + serviceName: oauth2-proxy + servicePort: http + path: / + tls: + - hosts: + - oauth2-proxy.kobs.io + secretName: oauth2-proxy-cert + ``` + +When the OAuth2 Proxy is running we can install kobs with the following values file. It will use all the default values from the Helm chart and just enables and configures the Ingress for kobs: + +```yaml +ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.kobs.io/oauth2/auth + nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.kobs.io/oauth2/start?rd=https://demo.kobs.io + nginx.ingress.kubernetes.io/auth-response-headers: 'X-Auth-Request-Email' + nginx.ingress.kubernetes.io/configuration-snippet: | + auth_request_set $email $upstream_http_x_auth_request_email; + add_header X-Auth-Request-Email $email; + hosts: + - demo.kobs.io + tls: + - secretName: kobs-cert + hosts: + - demo.kobs.io +``` + +When you save the values from above in a file called `values.yaml`, you can run the following command to install kobs: + +```sh +helm upgrade --install kobs kobs/kobs -f values.yaml +``` + +### Istio + +In the first step we have to create a Deployment and Service the OAuth2 Proxy. + +??? note "OAuth2 Proxy" + + ```yaml + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: oauth2-proxy + spec: + replicas: 1 + selector: + matchLabels: + app: oauth2-proxy + template: + metadata: + labels: + app: oauth2-proxy + spec: + containers: + - name: kobs + image: quay.io/oauth2-proxy/oauth2-proxy:v7.1.2 + args: + - --provider=google + - --skip-provider-button=true + - --oidc-issuer-url=https://accounts.google.com + - --upstream=static://200 + - --http-address=0.0.0.0:4180 + - --email-domain=kobs.io + - --cookie-domain=.kobs.io + - --whitelist-domain=.kobs.io + - --set-authorization-header=true + - --set-xauthrequest=true + env: + # For the sake of simplicity we directly setting the Client ID and Client Secret as value. In a production + # environment you should set the values for these environment variables from a secret. + - name: OAUTH2_PROXY_CLIENT_ID + value: + - name: OAUTH2_PROXY_CLIENT_SECRET + value: + # The Cookie Secret can be generated using the following command: + # python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(16)).decode())' + - name: OAUTH2_PROXY_COOKIE_SECRET + value: 7jctnRZlsQRSFaX76LK53w== + - name: OAUTH2_PROXY_COOKIE_NAME + value: kobs-demo + ports: + - containerPort: 4180 + name: http + protocol: TCP + + --- + apiVersion: v1 + kind: Service + metadata: + name: oauth2-proxy + spec: + type: ClusterIP + selector: + app: oauth2-proxy + ports: + - name: http + port: 4180 + protocol: TCP + targetPort: http + ``` + +When the OAuth2 Proxy is running we have to define the external authorizer that is allowed to be used in the mesh config. This is currently defined in the [extension provider](https://github.com/istio/api/blob/a205c627e4b955302bbb77dd837c8548e89e6e64/mesh/v1alpha1/config.proto#L534) in the mesh config. + +```yaml +meshConfig: + extensionProviders: + - name: oauth2-proxy + envoyExtAuthzHttp: + service: oauth2-proxy.kobs.svc.cluster.local + port: "4180" + includeHeadersInCheck: ["authorization", "cookie"] + headersToUpstreamOnAllow: ["authorization", "x-auth-request-email"] +``` + +The external authorizer is now ready to be used by the authorization policy. + +```yaml +--- +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: kobs +spec: + selector: + matchLabels: + app.kubernetes.io/instance: kobs + app.kubernetes.io/name: kobs + action: CUSTOM + provider: + name: oauth2-proxy + rules: + - to: + - operation: + hosts: + - "*.kobs.io" + notPaths: + - "/oauth2*" +``` + +Now we have to adjust the `istio` section in the kobs Helm chart. In contrast to the NGINX Ingress Controller example we do not create an additional Ingress / VirtualService for the OAuth2 Proxy. Instead the OAuth2 Proxy is exposed on the same domain as kobs via an additiona route: + +```yaml +istio: + virtualService: + create: true + gateways: + - istio-system/istio-default-gateway + hosts: + - demo.kobs.io + timeout: 3600s + additionalRoutes: + - match: + - uri: + prefix: /oauth2 + route: + - destination: + host: oauth2-proxy.kobs.svc.cluster.local + port: + number: 4180 + timeout: 60s +``` + +When you save the values from above in a file called `values.yaml`, you can run the following command to install kobs: + +```sh +helm upgrade --install kobs kobs/kobs -f values.yaml +``` diff --git a/docs/configuration/getting-started.md b/docs/configuration/getting-started.md index 2fe7ede73..240faf8b7 100644 --- a/docs/configuration/getting-started.md +++ b/docs/configuration/getting-started.md @@ -9,6 +9,7 @@ The following command-line arguments and environment variables are available. | Command-line Argument | Environment Variable | Description | Default | | --------------------- | -------------------- | ----------- | ------- | | `--api.address` | `KOBS_API_ADDRESS` | The address, where the API server is listen on. | `:15220` | +| `--api.auth-header` | `KOBS_API_AUTH_HEADER` | The header, which contains the details about the authenticated user. More information can be found in the [Authentication](authentication.md) section. | `X-Auth-Request-Email` | | `--app.address` | `KOBS_APP_ADDRESS` | The address, where the Application server is listen on. | `:15219` | | `--app.assets` | `KOBS_APP_ASSETS` | The location of the assets directory. | `app/build` | | `--clusters.cache-duration.namespaces` | `KOBS_CLUSTERS_CACHE_DURATION_NAMESPACES` | The duration, for how long requests to get the list of namespaces should be cached. | `5m` | diff --git a/mkdocs.yml b/mkdocs.yml index 75371dfc6..b8860d834 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - Getting Started: configuration/getting-started.md - Clusters: configuration/clusters.md - Plugins: configuration/plugins.md + - Authentication: configuration/authentication.md - Resources: - Resources: resources/resources.md - Applications: resources/applications.md diff --git a/pkg/api/api.go b/pkg/api/api.go index 74b830616..d7b4e6ca3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,6 +7,7 @@ import ( "time" "github.com/kobsio/kobs/pkg/api/clusters" + "github.com/kobsio/kobs/pkg/api/middleware/auth" "github.com/kobsio/kobs/pkg/api/middleware/httplog" "github.com/go-chi/chi/v5" @@ -71,6 +72,7 @@ func New(loadedClusters *clusters.Clusters, pluginsRouter chi.Router, isDevelopm router.Use(middleware.RequestID) router.Use(middleware.Recoverer) router.Use(middleware.URLFormat) + router.Use(auth.Auth) router.Use(httplog.NewStructuredLogger(log.Logger)) router.Use(render.SetContentType(render.ContentTypeJSON)) diff --git a/pkg/api/middleware/auth/auth.go b/pkg/api/middleware/auth/auth.go new file mode 100644 index 000000000..1675f7ddb --- /dev/null +++ b/pkg/api/middleware/auth/auth.go @@ -0,0 +1,54 @@ +package auth + +import ( + "context" + "net/http" + "os" + + flag "github.com/spf13/pflag" +) + +// Key to use when setting the user. +type ctxKeyUser int + +// userKey is the key that holds the user in a request context. +const userKey ctxKeyUser = 0 + +// UserHeader is the name of the HTTP Header which contains the user information. +var ( + userHeader string +) + +func init() { + defaultHeader := "X-Auth-Request-Email" + if os.Getenv("KOBS_API_AUTH_HEADER") != "" { + defaultHeader = os.Getenv("KOBS_API_AUTH_HEADER") + } + + flag.StringVar(&userHeader, "api.auth-header", defaultHeader, "The header, which contains the details about the authenticated user.") +} + +// Auth is a middleware that injects the user information from a configured request header. +func Auth(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + user := r.Header.Get(userHeader) + ctx = context.WithValue(ctx, userKey, user) + next.ServeHTTP(w, r.WithContext(ctx)) + } + + return http.HandlerFunc(fn) +} + +// GetUser returns a user from the given context if one is present. Returns the empty string if a user can not be found. +func GetUser(ctx context.Context) string { + if ctx == nil { + return "" + } + + if reqID, ok := ctx.Value(userKey).(string); ok { + return reqID + } + + return "" +} diff --git a/pkg/api/middleware/httplog/httplog.go b/pkg/api/middleware/httplog/httplog.go index 8ae9eccec..0369f8d27 100644 --- a/pkg/api/middleware/httplog/httplog.go +++ b/pkg/api/middleware/httplog/httplog.go @@ -7,6 +7,8 @@ import ( "net/http" "time" + "github.com/kobsio/kobs/pkg/api/middleware/auth" + "github.com/go-chi/chi/v5/middleware" "github.com/sirupsen/logrus" ) @@ -34,6 +36,10 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { logFields["req_id"] = reqID } + if user := auth.GetUser(r.Context()); user != "" { + logFields["user"] = user + } + scheme := "http" if r.TLS != nil { scheme = "https"