openid istio

Using Istio & OpenID Connect / OAUTH2 To Authorise


We have a large number of management only services (kibana, grafana, prometheus, alertmanager, etc.). I want to make it very easy for developers to light up new ones, but also very secure. More specifically, I want to make it easier to be secure than to be insecure. Many breaches happen because a development-only thing is forgotten online. If you have a system where TLS + Authentication + Authorization is easy to do, and on-by-default, then you don’t have to worry (as much).

The method we have settled on here at Agilicus is to have *.admin.DOMAIN be universally managed by OpenID Connect-based (OAUTH2) login. This means that all services XXX.admin get an automatic TLS certificate, an automatic authentication. Without any integration. Without any effort.

How did we do this? The magic of Istio and Service Mesh. I’ll share the YAML below. But, in a nutshell, we run an oauth2_proxy for each domain we run (.ca, .com, .dev). This is integrated with our G Suite login. I talked more about it here.

The key to the operation is a wee bit of Lua code in the filter. You will see we instantiate this for 3 entries (.ca/.com/.dev). All development occurs in .dev (which further guarantees it will be secured & encrypted since .dev is in the HSTS preload list entirely). Even without that, we added our domains to the preload list, guaranteeing that nobody forgets the encrypted-only memo.

---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: authn-filter-8443
spec:
  workloadSelector:
    labels:
      app: istio-ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          portNumber: 8443
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.lua
          typed_config:
            '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
            inlineCode: |
              base_host = string.upper("admin.__ROOT_DOMAIN__")
              a, oauth_host = string.match("admin.__ROOT_DOMAIN__", '(admin.*%.)(.*%..*)')
              oauth_host = "oauth2." .. oauth_host

              function starts_with(str, start)
                return str:sub(1, #start) == start
              end

              function ignore_request (request_handle)
                local host = string.upper(request_handle:headers():get(":authority"))
                local path = request_handle:headers():get(":path")
                -- we skip un-protected hosts or 'well-known' paths (used for e.g. acme)
                i, j = string.find(host, base_host, 0, true)
                if i == nil or i == 1 or starts_with(path, '/.well-known/') then
                  -- if no match, or its just admin.__ROOT_DOMAIN__ (e.g. not X.admin) or its
                  -- /.well-known for acme
                  return true
                end

                -- request_handle:logWarn("Host protected")
                return false
              end

              function login (request_handle)
                local request_url = "https://"..request_handle:headers():get(":authority")..request_handle:headers():get(":path")
                local redirect_url = "https://"..oauth_host.."/oauth2/start?rd="..request_url
                headers = {
                    [":status"] = 302,
                    ["location"] = redirect_url,
                    ["content-type"] = "text/html"
                }
                request_handle:headers():add("content-type", "text/html")
                request_handle:respond(headers, "")
              end

              function is_snippet (request_handle)
                local ua = request_handle:headers():get(":user-agent")
                if ua ~= nil and ua:match("snippet") ~= nil then
                  headers = {
                    [":status"] = 200
                  }
                  request_handle:respond(headers, '')
                  return true
                end
                return false
              end

              function envoy_on_request(request_handle)
                if ignore_request(request_handle) then
                  return
                end
                if is_snippet(request_handle) then
                  return
                end

                cookie = request_handle:headers():get("Cookie")
                if cookie == nil then
                  -- request_handle:logWarn("login")
                  login(request_handle)
                  return
                end
                -- request_handle:logWarn("validating token against /ouath2/auth")
                local headers, body = request_handle:httpCall(
                    "outbound|443||"..oauth_host,
                    {
                      [":method"] = "GET",
                      [":path"] = "/oauth2/auth",
                      [":authority"] = oauth_host,
                      ["Cookie"] = cookie
                    },
                    nil,
                    5000)
                local status
                for header, value in pairs(headers) do
                  if header == ":status" then
                      status = value
                  end
                end

                -- request_handle:logWarn("token validation status:"..status)
                if status ~= "202" then
                  -- request_handle:logWarn("Not validated")
                  login(request_handle)
                  return
                end
              end
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: oauth2
spec:
  hosts:
    - oauth2.MYDOMAIN.ca
    - oauth2.MYDOMAIN.com
    - oauth2.MYDOMAIN.dev
  ports:
    - number: 443
      name: https-for-tls
      protocol: HTTPS
  resolution: DNS
  location: MESH_EXTERNAL
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: oauth2-mydomain-ca
spec:
  host: oauth2.MYDOMAIN.ca
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
      - port:
          number: 443
        tls:
          mode: SIMPLE 
          sni: oauth2.MYDOMAIN.ca
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: oauth2-mydomain-com
spec:
  host: oauth2.MYDOMAIN.com
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
      - port:
          number: 443
        tls:
          mode: SIMPLE  
          sni: oauth2.MYDOMAIN.com
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: oauth2-MYDOMAIN-dev
spec:
  host: oauth2.MYDOMAIN.dev
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
      - port:
          number: 443
        tls:
          mode: SIMPLE  
          sni: oauth2.MYDOMAIN.dev