# Using Istio & OpenID Connect / OAUTH2 To Authorise

- Link: https://www.agilicus.com/using-istio-openid-connect-oauth2-to-authorise/
- Published: 2020-03-14T19:42:41+00:00
- Author: Don Bowman

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](https://istio.io/) 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](https://www.agilicus.com/using-single-sign-on-oauth2-across-many-sites-in-kubernetes/).

The key to the operation is a wee bit of [Lua](https://www.lua.org/) 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](https://www.agilicus.com/we-are-all-in-on-the-tls-the-hsts-preload/)
our domains to the preload list, guaranteeing that nobody forgets the encrypted-
only memo.

    ```line-numbers
    ---
    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
    ```

    ```language-yaml
    ---
    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
    ```
