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