From 61bc6c72bc86ceeda5416e12d8bec7bcba8c72e5 Mon Sep 17 00:00:00 2001 From: Sergey Zabolotny Date: Fri, 1 Nov 2019 11:29:10 +0200 Subject: [PATCH 1/5] feature/oauth: Install oauth dependencies --- Dockerfile | 13 +++++++++++-- conf/nginx/nginx.conf.tmpl | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 062678f..4cc247c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,10 +43,11 @@ RUN set -xe; \ # Also symlink nginx binary to a location in PATH ln -s /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx -# Certs +# Certs and OAuth RUN set -xe; \ apk add --update --no-cache \ openssl \ + git \ ; \ # Create a folder for custom vhost certs (mount custom certs here) mkdir -p /etc/certs/custom; \ @@ -61,7 +62,15 @@ RUN set -xe; \ -days 3650 \ -out /etc/certs/server.crt \ -keyout /etc/certs/server.key; \ - apk del openssl && rm -rf /var/cache/apk/*; + # Install OAuth dependencies + git clone -c transfer.fsckobjects=true https://github.com/pintsized/lua-resty-http.git /tmp/lua-resty-http; \ + cd /tmp/lua-resty-http; \ + # https://github.com/pintsized/lua-resty-http/releases/tag/v0.07 v0.07 + git checkout 69695416d408f9cfdaae1ca47650ee4523667c3d; \ + mkdir -p /etc/nginx/lua; \ + cp -aR /tmp/lua-resty-http/lib/resty /etc/nginx/lua/resty; \ + rm -rf /tmp/lua-resty-http; \ + apk del openssl git && rm -rf /var/cache/apk/*; COPY conf/nginx/ /etc/nginx/ COPY conf/sudoers /etc/sudoers diff --git a/conf/nginx/nginx.conf.tmpl b/conf/nginx/nginx.conf.tmpl index 357b5ba..f15262f 100644 --- a/conf/nginx/nginx.conf.tmpl +++ b/conf/nginx/nginx.conf.tmpl @@ -13,6 +13,9 @@ events { } http { + # Set lua packages path to /etc/nginx/lua/ + lua_package_path '/etc/nginx/lua/?.lua;'; + server_names_hash_bucket_size 128; include /etc/nginx/mime.types; default_type application/octet-stream; From b87ff68c489ab8ac6afc15053cacd8fe5cadc2d3 Mon Sep 17 00:00:00 2001 From: Sergey Zabolotny Date: Fri, 1 Nov 2019 13:02:33 +0200 Subject: [PATCH 2/5] feature/oauth: Add resolver and allow using trusted ssl certificates --- conf/nginx/nginx.conf.tmpl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conf/nginx/nginx.conf.tmpl b/conf/nginx/nginx.conf.tmpl index f15262f..41e8e6f 100644 --- a/conf/nginx/nginx.conf.tmpl +++ b/conf/nginx/nginx.conf.tmpl @@ -16,6 +16,11 @@ http { # Set lua packages path to /etc/nginx/lua/ lua_package_path '/etc/nginx/lua/?.lua;'; + resolver 8.8.8.8 ipv6=off; + + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + lua_ssl_verify_depth 5; + server_names_hash_bucket_size 128; include /etc/nginx/mime.types; default_type application/octet-stream; From 49f34a478550beac2824f82e0107e9011ab9114b Mon Sep 17 00:00:00 2001 From: Sergey Zabolotny Date: Wed, 6 Nov 2019 17:26:42 +0200 Subject: [PATCH 3/5] Add session functionality --- conf/nginx/lua/access.lua | 254 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 conf/nginx/lua/access.lua diff --git a/conf/nginx/lua/access.lua b/conf/nginx/lua/access.lua new file mode 100644 index 0000000..50b5674 --- /dev/null +++ b/conf/nginx/lua/access.lua @@ -0,0 +1,254 @@ +-- Copyright 2015-2016 CloudFlare +-- Copyright 2014-2015 Aaron Westendorf + +local json = require("cjson") +local http = require("resty.http") + +local uri = ngx.var.uri +local uri_args = ngx.req.get_uri_args() +local scheme = ngx.var.scheme + +local client_id = ngx.var.ngo_client_id +local client_secret = ngx.var.ngo_client_secret +local token_secret = ngx.var.ngo_token_secret +local domain = ngx.var.ngo_domain +local cb_scheme = ngx.var.ngo_callback_scheme or scheme +local cb_server_name = ngx.var.ngo_callback_host or ngx.var.server_name +local cb_uri = ngx.var.ngo_callback_uri or "/_oauth" +local cb_url = cb_scheme .. "://" .. cb_server_name .. cb_uri +local redirect_url = cb_scheme .. "://" .. cb_server_name .. ngx.var.request_uri +local extra_validity = tonumber(ngx.var.ngo_extra_validity or "0") +local whitelist = ngx.var.ngo_whitelist or "" +local blacklist = ngx.var.ngo_blacklist or "" +local secure_cookies = ngx.var.ngo_secure_cookies == "true" or false +local http_only_cookies = ngx.var.ngo_http_only_cookies == "true" or false +local set_user = ngx.var.ngo_user or false +local email_as_user = ngx.var.ngo_email_as_user == "true" or false +local session_id = uri_args["state"] or ngx.var.cookie_session +local session = ngx.shared.session; + +if whitelist:len() == 0 then + whitelist = nil +end + +if blacklist:len() == 0 then + blacklist = nil +end + +local function create_uuid (length) + local index, pw, rnd = 0, "" + local chars = { + "abcdefghijklmnopqrstuvwxyz", + "0123456789" + } + math.randomseed(os.clock()) + repeat + index = index + 1 + rnd = math.random(chars[index]:len()) + if math.random(2) == 1 then + pw = pw .. chars[index]:sub(rnd, rnd) + else + pw = chars[index]:sub(rnd, rnd) .. pw + end + index = index % #chars + until pw:len() >= length + return pw +end + +local function check_domain(email, whitelist_failed) + local oauth_domain = email:match("[^@]+@(.+)") + -- if domain is configured, check it, if it isn't, permit request + if domain:len() ~= 0 then + if not string.find(" " .. domain .. " ", " " .. oauth_domain .. " ", 1, true) then + if whitelist_failed then + ngx.log(ngx.ERR, email .. " is not on " .. domain .. " nor in the whitelist") + else + ngx.log(ngx.ERR, email .. " is not on " .. domain) + end + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + end +end + +local function on_auth(email, token, expires) + if blacklist then + -- blacklisted user is always rejected + if string.find(" " .. blacklist .. " ", " " .. email .. " ", 1, true) then + ngx.log(ngx.ERR, email .. " is in blacklist") + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + end + + if whitelist then + -- if whitelisted, no need check the if it's a valid domain + if not string.find(" " .. whitelist .. " ", " " .. email .. " ", 1, true) then + check_domain(email, true) + end + else + -- empty whitelist, lets check if it's a valid domain + check_domain(email, false) + end + + if set_user then + if email_as_user then + ngx.var.ngo_user = email + else + ngx.var.ngo_user = email:match("([^@]+)@.+") + end + end +end + +local function request_access_token(code) + local request = http.new() + + request:set_timeout(7000) + + local res, err = request:request_uri("https://accounts.google.com/o/oauth2/token", { + method = "POST", + body = ngx.encode_args({ + code = code, + client_id = client_id, + client_secret = client_secret, + redirect_uri = cb_url, + grant_type = "authorization_code", + }), + headers = { + ["Content-type"] = "application/x-www-form-urlencoded" + }, + ssl_verify = true, + }) + if not res then + return nil, (err or "auth token request failed: " .. (err or "unknown reason")) + end + + if res.status ~= 200 then + return nil, "received " .. res.status .. " from https://accounts.google.com/o/oauth2/token: " .. res.body + end + + return json.decode(res.body) +end + +local function request_profile(token) + local request = http.new() + + request:set_timeout(7000) + + local res, err = request:request_uri("https://www.googleapis.com/oauth2/v2/userinfo", { + headers = { + ["Authorization"] = "Bearer " .. token, + }, + ssl_verify = true, + }) + if not res then + return nil, "auth info request failed: " .. (err or "unknown reason") + end + + if res.status ~= 200 then + return nil, "received " .. res.status .. " from https://www.googleapis.com/oauth2/v2/userinfo" + end + + return json.decode(res.body) +end + +local function is_authorized() + local session_data = json.decode(session:get(session_id)) + local expires = session_data.expires + local email = session_data.email + local token = session_data.token + + local expected_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires)) + + if token == expected_token and expires and expires > ngx.time() - extra_validity then + session:set(session_id, json.encode(session_data), 3600) + return true + else + return false + end +end + +local function redirect_to_auth() + ngx.header["Set-Cookie"] = {"session=" .. session_id} + + -- google seems to accept space separated domain list in the login_hint, so use this undocumented feature. + return ngx.redirect("https://accounts.google.com/o/oauth2/auth?" .. ngx.encode_args({ + client_id = client_id, + scope = "email", + response_type = "code", + redirect_uri = cb_url, + state = session_id, + login_hint = domain, + })) +end + +local function authorize() + if uri ~= cb_uri then + return redirect_to_auth() + end + + if uri_args["error"] then + ngx.log(ngx.ERR, "received " .. uri_args["error"] .. " from https://accounts.google.com/o/oauth2/auth") + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + + local token, token_err = request_access_token(uri_args["code"]) + if not token then + ngx.log(ngx.ERR, "got error during access token request: " .. token_err) + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + + local profile, profile_err = request_profile(token["access_token"]) + if not profile then + ngx.log(ngx.ERR, "got error during profile request: " .. profile_err) + return ngx.exit(ngx.HTTP_FORBIDDEN) + end + + local expires = ngx.time() + token["expires_in"] + local cookie_tail = ";version=1;path=/;Max-Age=" .. extra_validity + token["expires_in"] + if secure_cookies then + cookie_tail = cookie_tail .. ";secure" + end + if http_only_cookies then + cookie_tail = cookie_tail .. ";httponly" + end + + local email = profile["email"] + local user_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires)) + + -- Update session with data from auth response + local session_data = json.decode(session:get(session_id)) + session_data.expires = expires + session_data.email = email + session_data.token = user_token + session:set(session_id, json.encode(session_data), 3600) + return ngx.redirect(session_data.original_url) +end + +-------------------------------- +-- Main code +-------------------------------- +-- Flush expired sessions +ngx.shared.session:flush_expired() + +-- Create new session if session_id is empty or session with session_id does not exists +if session_id == nil or session:get(session_id) == nil then + session_id = create_uuid(32) + local session_data = { + ["id"] = session_id, + ["original_url"] = cb_scheme .. "://" .. ngx.var.host .. ngx.var.request_uri, + ["expires"] = 0, + ["email"] = "", + ["token"] = "", + } + session:set(session_id, json.encode(session_data), 300) +else + session_data = json.decode(session:get(session_id)) + session_id = session_data.id +end + +if not is_authorized() then + authorize() +end + +if uri == "/_oauth" then + return ngx.redirect(session_data.original_url) +end From ac3431d3cebcfbea1b125c0eae41aa8e814f2606 Mon Sep 17 00:00:00 2001 From: Sergey Zabolotny Date: Fri, 8 Nov 2019 19:40:41 +0200 Subject: [PATCH 4/5] oauth configuration through env variables --- bin/docker-entrypoint.sh | 1 + conf/nginx/conf.d/auth.conf.tmpl | 41 +++++ conf/nginx/conf.d/vhosts.conf.tmpl | 276 ++++++++++++++++------------- conf/nginx/lua/access.lua | 1 + conf/nginx/nginx.conf.tmpl | 3 +- 5 files changed, 198 insertions(+), 124 deletions(-) create mode 100644 conf/nginx/conf.d/auth.conf.tmpl diff --git a/bin/docker-entrypoint.sh b/bin/docker-entrypoint.sh index 419ed87..6747ce1 100755 --- a/bin/docker-entrypoint.sh +++ b/bin/docker-entrypoint.sh @@ -22,6 +22,7 @@ fi if [[ "$1" == "supervisord" ]]; then # Generate config files from templates gomplate --file /etc/nginx/nginx.conf.tmpl --out /etc/nginx/nginx.conf + gomplate --file /etc/nginx/conf.d/auth.conf.tmpl --out /etc/nginx/conf.d/auth.conf exec supervisord -c /etc/supervisord.conf # Command mode diff --git a/conf/nginx/conf.d/auth.conf.tmpl b/conf/nginx/conf.d/auth.conf.tmpl new file mode 100644 index 0000000..9bd5776 --- /dev/null +++ b/conf/nginx/conf.d/auth.conf.tmpl @@ -0,0 +1,41 @@ +{{ if and (getenv "NGO_CALLBACK_HOST") (getenv "NGO_CLIENT_ID") (getenv "NGO_CLIENT_SECRET") (getenv "NGO_TOKEN_SECRET") }} +# Enable oauth + +server { + listen 80; + listen 443 ssl http2; + server_name auth; + server_name auth.*; + ssl_certificate /etc/certs/server.crt; + ssl_certificate_key /etc/certs/server.key; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + lua_ssl_verify_depth 5; + + set $ngo_callback_uri '/_oauth'; + set $ngo_callback_host '{{ getenv "NGO_CALLBACK_HOST" }}'; + set $ngo_client_id '{{ getenv "NGO_CLIENT_ID" }}'; + set $ngo_client_secret '{{ getenv "NGO_CLIENT_SECRET" }}'; + set $ngo_token_secret '{{ getenv "NGO_TOKEN_SECRET" }}'; + set $ngo_user 'unknown'; + set $ngo_email_as_user 'true'; + set $ngo_extra_validity '0'; + set_by_lua $ngo_domain 'return os.getenv("NGO_DOMAIN")'; + set_by_lua $ngo_whitelist 'return os.getenv("NGO_WHITELIST")'; + set_by_lua $ngo_blacklist 'return os.getenv("NGO_BLACKLIST")'; + + expires 0; + + add_header Google-User $ngo_user; + + location / { + # See https://github.com/openresty/lua-nginx-module#ngxeof + proxy_ignore_client_abort on; + access_by_lua_file "/etc/nginx/lua/access.lua"; + } +} +{{ end }} diff --git a/conf/nginx/conf.d/vhosts.conf.tmpl b/conf/nginx/conf.d/vhosts.conf.tmpl index add99ce..e0b6fa5 100644 --- a/conf/nginx/conf.d/vhosts.conf.tmpl +++ b/conf/nginx/conf.d/vhosts.conf.tmpl @@ -2,68 +2,98 @@ {{/* Upstream template */}} {{ define "upstream" }} - {{ if .Address }} - {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} - {{ if and .Container.Node.ID .Address.HostPort }} + {{ if .Address }} + {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} + {{ if and .Container.Node.ID .Address.HostPort }} - # {{ .Container.Node.Name }}/{{ .Container.Name }} - server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; + # {{ .Container.Node.Name }}/{{ .Container.Name }} + server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; - {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} - {{ else if .Network }} - - # {{ .Container.Name }} - server {{ .Network.IP }}:{{ .Address.Port }}; - - {{ end }} + {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} {{ else if .Network }} - # {{ .Container.Name }} - server {{ .Network.IP }} down; - + # {{ .Container.Name }} + server {{ .Network.IP }}:{{ .Address.Port }}; {{ end }} + {{ else if .Network }} + + # {{ .Container.Name }} + server {{ .Network.IP }} down; + + {{ end }} {{ end }} {{/* HTTP/HTTPS server template */}} {{ define "server" }} - ## HTTPS - server { - - listen 80; - listen 443 ssl http2; - {{ range $host := split .Hosts "," }} + ## HTTPS + server { - server_name {{ $host }}; + listen 80; + listen 443 ssl http2; + {{ range $host := split .Hosts "," }} - {{ end }} + server_name {{ $host }}; - {{ $cert := (printf "/etc/certs/custom/%s.crt" .Cert) }} - {{ $certKey := (printf "/etc/certs/custom/%s.key" .Cert) }} + {{ end }} - {{/* Use custom cert if it exist */}} - {{/* E.g. /etc/certs/custom/example.com.crt and /etc/certs/custom/example.com.key */}} - {{ if (and (exists $cert) (exists $certKey)) }} + {{ $cert := (printf "/etc/certs/custom/%s.crt" .Cert) }} + {{ $certKey := (printf "/etc/certs/custom/%s.key" .Cert) }} - ssl_certificate {{ $cert }}; - ssl_certificate_key {{ $certKey }}; + {{/* Use custom cert if it exist */}} + {{/* E.g. /etc/certs/custom/example.com.crt and /etc/certs/custom/example.com.key */}} + {{ if (and (exists $cert) (exists $certKey)) }} - {{/* Use default self-signed cert otherwise */}} - {{ else }} + ssl_certificate {{ $cert }}; + ssl_certificate_key {{ $certKey }}; - ssl_certificate /etc/certs/server.crt; - ssl_certificate_key /etc/certs/server.key; + {{/* Use default self-signed cert otherwise */}} + {{ else }} - {{ end }} + ssl_certificate /etc/certs/server.crt; + ssl_certificate_key /etc/certs/server.key; - ssl_session_cache builtin:1000 shared:SSL:10m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; - ssl_prefer_server_ciphers on; + {{ end }} - location / { - proxy_pass http://{{ .Upstream }}; - } + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + {{ if and .Host.Env.NGO_CALLBACK_HOST .Host.Env.NGO_CLIENT_ID .Host.Env.NGO_CLIENT_SECRET .Host.Env.NGO_TOKEN_SECRET .Container.Env.OAUTH_ENABLED }} + {{ if eq .Container.Env.OAUTH_ENABLED "1" }} + # Enable oauth + resolver 8.8.8.8 ipv6=off; + + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + lua_ssl_verify_depth 5; + + set $ngo_callback_uri '/_oauth'; + set $ngo_callback_host '{{ .Host.Env.NGO_CALLBACK_HOST }}'; + set $ngo_client_id '{{ .Host.Env.NGO_CLIENT_ID }}'; + set $ngo_client_secret '{{ .Host.Env.NGO_CLIENT_SECRET }}'; + set $ngo_token_secret '{{ .Host.Env.NGO_TOKEN_SECRET }}'; + set $ngo_user 'unknown'; + set $ngo_email_as_user 'true'; + set $ngo_extra_validity '0'; + set_by_lua $ngo_domain 'return os.getenv("NGO_DOMAIN")'; + set_by_lua $ngo_whitelist 'return os.getenv("NGO_WHITELIST")'; + set_by_lua $ngo_blacklist 'return os.getenv("NGO_BLACKLIST")'; + + expires 0; + + add_header Google-User $ngo_user; + + location / { + proxy_ignore_client_abort on; + access_by_lua_file "/etc/nginx/lua/access.lua"; + proxy_pass http://{{ .Upstream }}; + {{ end }} + {{ else }} + location / { + proxy_pass http://{{ .Upstream }}; + {{ end }} } + } {{ end }} {{/* END: Variables */}} @@ -74,58 +104,58 @@ {{ range $hosts, $containers_vhost := groupByLabel $dc_containers "io.docksal.virtual-host" }} - {{ range $service, $containers := groupByLabel $containers_vhost "com.docker.compose.service" }} - {{ $pr_container := $containers | first }} - {{ $project := index $pr_container.Labels "com.docker.compose.project" }} - {{ $upstream := (print $project "-" $service) }} - - # -------------------------------------------------- # - # Reachable via "{{ $project }}_default" network - upstream {{ $upstream }} { - {{ range $container := $containers }} - {{ $addrLen := len $container.Addresses }} - - {{ range $knownNetwork := $CurrentContainer.Networks }} - {{ range $containerNetwork := $container.Networks }} - {{ if eq $knownNetwork.Name $containerNetwork.Name }} - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{/* Assume port 80 by default */}} - {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{ end }} - {{ end }} - {{ end }} + {{ range $service, $containers := groupByLabel $containers_vhost "com.docker.compose.service" }} + {{ $pr_container := $containers | first }} + {{ $project := index $pr_container.Labels "com.docker.compose.project" }} + {{ $upstream := (print $project "-" $service) }} + + # -------------------------------------------------- # + # Reachable via "{{ $project }}_default" network + upstream {{ $upstream }} { + {{ range $container := $containers }} + {{ $addrLen := len $container.Addresses }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{/* Assume port 80 by default */}} + {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} {{ end }} + {{ end }} {{ end }} - } + {{ end }} + {{ end }} + } - {{/* Get the cert name from io.docksal.cert-name container label */}} - {{ $certName := or (index $pr_container.Labels "io.docksal.cert-name") "none" }} - {{/* Unset certName if its value us "none" */}} - {{ $certName := when (ne $certName "none") $certName nil }} + {{/* Get the cert name from io.docksal.cert-name container label */}} + {{ $certName := or (index $pr_container.Labels "io.docksal.cert-name") "none" }} + {{/* Unset certName if its value us "none" */}} + {{ $certName := when (ne $certName "none") $certName nil }} - {{/* Get the best matching cert by name for the vhost. */}} - {{ $primaryHost := (index (split $hosts ",") 0) }} - {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} + {{/* Get the best matching cert by name for the vhost. */}} + {{ $primaryHost := (index (split $hosts ",") 0) }} + {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} - {{/* Trim file suffix - it will be added later */}} - {{ $vhostCert := trimSuffix ".crt" $vhostCert }} - {{ $vhostCert := trimSuffix ".key" $vhostCert }} + {{/* Trim file suffix - it will be added later */}} + {{ $vhostCert := trimSuffix ".crt" $vhostCert }} + {{ $vhostCert := trimSuffix ".key" $vhostCert }} - {{/* Use the cert specified on the container or fallback to the best vhost match */}} - {{ $cert := (coalesce $certName $vhostCert) }} + {{/* Use the cert specified on the container or fallback to the best vhost match */}} + {{ $cert := (coalesce $certName $vhostCert) }} - {{/* Generate HTTP/HTTPS server config */}} - {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert) }} + {{/* Generate HTTP/HTTPS server config */}} + {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $pr_container "Host" $CurrentContainer) }} - {{ end }} - # -------------------------------------------------- # + {{ end }} + # -------------------------------------------------- # {{ end }} @@ -137,49 +167,49 @@ {{ range $hosts, $containers := groupByLabel $d_containers "io.docksal.virtual-host" }} - {{ $container := $containers | first }} - {{ $upstream := $container.Name }} - - upstream {{ $upstream }} { - {{ $addrLen := len $container.Addresses }} - - {{ range $knownNetwork := $CurrentContainer.Networks }} - {{ range $containerNetwork := $container.Networks }} - {{ if eq $knownNetwork.Name $containerNetwork.Name }} - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{/* Assume port 80 by default */}} - {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{ end }} - {{ end }} - {{ end }} + {{ $container := $containers | first }} + {{ $upstream := $container.Name }} + + upstream {{ $upstream }} { + {{ $addrLen := len $container.Addresses }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{/* Assume port 80 by default */}} + {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{ end }} {{ end }} - } + {{ end }} + {{ end }} + } - {{/* Get the cert name from io.docksal.cert-name container label */}} - {{ $certName := or (index $container.Labels "io.docksal.cert-name") "none" }} - {{/* Unset certName if its value us "none" */}} - {{ $certName := when (ne $certName "none") $certName nil }} + {{/* Get the cert name from io.docksal.cert-name container label */}} + {{ $certName := or (index $container.Labels "io.docksal.cert-name") "none" }} + {{/* Unset certName if its value us "none" */}} + {{ $certName := when (ne $certName "none") $certName nil }} - {{/* Get the best matching cert by name for the vhost. */}} - {{ $primaryHost := (index (split $hosts ",") 0) }} - {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} + {{/* Get the best matching cert by name for the vhost. */}} + {{ $primaryHost := (index (split $hosts ",") 0) }} + {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} - {{/* Trim file suffix - it will be added later */}} - {{ $vhostCert := trimSuffix ".crt" $vhostCert }} - {{ $vhostCert := trimSuffix ".key" $vhostCert }} + {{/* Trim file suffix - it will be added later */}} + {{ $vhostCert := trimSuffix ".crt" $vhostCert }} + {{ $vhostCert := trimSuffix ".key" $vhostCert }} - {{/* Use the cert specified on the container or fallback to the best vhost match */}} - {{ $cert := (coalesce $certName $vhostCert) }} + {{/* Use the cert specified on the container or fallback to the best vhost match */}} + {{ $cert := (coalesce $certName $vhostCert) }} - {{/* Generate HTTP/HTTPS server config */}} - {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert) }} + {{/* Generate HTTP/HTTPS server config */}} + {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $container "Host" $CurrentContainer) }} {{ end }} diff --git a/conf/nginx/lua/access.lua b/conf/nginx/lua/access.lua index 50b5674..9e83dc9 100644 --- a/conf/nginx/lua/access.lua +++ b/conf/nginx/lua/access.lua @@ -89,6 +89,7 @@ local function on_auth(email, token, expires) check_domain(email, false) end + if set_user then if email_as_user then ngx.var.ngo_user = email diff --git a/conf/nginx/nginx.conf.tmpl b/conf/nginx/nginx.conf.tmpl index 41e8e6f..b904779 100644 --- a/conf/nginx/nginx.conf.tmpl +++ b/conf/nginx/nginx.conf.tmpl @@ -32,7 +32,8 @@ http { access_log off; {{ end }} - lua_shared_dict hosts 1m; + lua_shared_dict hosts 1m; + lua_shared_dict session 1m; # For Lua debugging only #lua_code_cache off; From 418bea649e3eb2e069ba01771c63cebd91f751ff Mon Sep 17 00:00:00 2001 From: Sergey Zabolotny Date: Fri, 15 Nov 2019 17:17:33 +0200 Subject: [PATCH 5/5] Fix tabs --- conf/nginx/conf.d/vhosts.conf.tmpl | 307 +++++++++++++++-------------- conf/nginx/nginx.conf.tmpl | 3 +- 2 files changed, 156 insertions(+), 154 deletions(-) diff --git a/conf/nginx/conf.d/vhosts.conf.tmpl b/conf/nginx/conf.d/vhosts.conf.tmpl index e0b6fa5..0f2dff5 100644 --- a/conf/nginx/conf.d/vhosts.conf.tmpl +++ b/conf/nginx/conf.d/vhosts.conf.tmpl @@ -2,98 +2,99 @@ {{/* Upstream template */}} {{ define "upstream" }} - {{ if .Address }} - {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} - {{ if and .Container.Node.ID .Address.HostPort }} + {{ if .Address }} + {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} + {{ if and .Container.Node.ID .Address.HostPort }} - # {{ .Container.Node.Name }}/{{ .Container.Name }} - server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; + # {{ .Container.Node.Name }}/{{ .Container.Name }} + server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; - {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} - {{ else if .Network }} + {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} + {{ else if .Network }} - # {{ .Container.Name }} - server {{ .Network.IP }}:{{ .Address.Port }}; - {{ end }} - {{ else if .Network }} + # {{ .Container.Name }} + server {{ .Network.IP }}:{{ .Address.Port }}; + + {{ end }} + {{ else if .Network }} - # {{ .Container.Name }} - server {{ .Network.IP }} down; + # {{ .Container.Name }} + server {{ .Network.IP }} down; - {{ end }} + {{ end }} {{ end }} {{/* HTTP/HTTPS server template */}} {{ define "server" }} - ## HTTPS - server { + ## HTTPS + server { - listen 80; - listen 443 ssl http2; - {{ range $host := split .Hosts "," }} + listen 80; + listen 443 ssl http2; + {{ range $host := split .Hosts "," }} - server_name {{ $host }}; + server_name {{ $host }}; - {{ end }} + {{ end }} - {{ $cert := (printf "/etc/certs/custom/%s.crt" .Cert) }} - {{ $certKey := (printf "/etc/certs/custom/%s.key" .Cert) }} + {{ $cert := (printf "/etc/certs/custom/%s.crt" .Cert) }} + {{ $certKey := (printf "/etc/certs/custom/%s.key" .Cert) }} - {{/* Use custom cert if it exist */}} - {{/* E.g. /etc/certs/custom/example.com.crt and /etc/certs/custom/example.com.key */}} - {{ if (and (exists $cert) (exists $certKey)) }} + {{/* Use custom cert if it exist */}} + {{/* E.g. /etc/certs/custom/example.com.crt and /etc/certs/custom/example.com.key */}} + {{ if (and (exists $cert) (exists $certKey)) }} - ssl_certificate {{ $cert }}; - ssl_certificate_key {{ $certKey }}; + ssl_certificate {{ $cert }}; + ssl_certificate_key {{ $certKey }}; - {{/* Use default self-signed cert otherwise */}} - {{ else }} + {{/* Use default self-signed cert otherwise */}} + {{ else }} - ssl_certificate /etc/certs/server.crt; - ssl_certificate_key /etc/certs/server.key; + ssl_certificate /etc/certs/server.crt; + ssl_certificate_key /etc/certs/server.key; - {{ end }} + {{ end }} - ssl_session_cache builtin:1000 shared:SSL:10m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; - ssl_prefer_server_ciphers on; - - {{ if and .Host.Env.NGO_CALLBACK_HOST .Host.Env.NGO_CLIENT_ID .Host.Env.NGO_CLIENT_SECRET .Host.Env.NGO_TOKEN_SECRET .Container.Env.OAUTH_ENABLED }} - {{ if eq .Container.Env.OAUTH_ENABLED "1" }} - # Enable oauth - resolver 8.8.8.8 ipv6=off; - - lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; - lua_ssl_verify_depth 5; - - set $ngo_callback_uri '/_oauth'; - set $ngo_callback_host '{{ .Host.Env.NGO_CALLBACK_HOST }}'; - set $ngo_client_id '{{ .Host.Env.NGO_CLIENT_ID }}'; - set $ngo_client_secret '{{ .Host.Env.NGO_CLIENT_SECRET }}'; - set $ngo_token_secret '{{ .Host.Env.NGO_TOKEN_SECRET }}'; - set $ngo_user 'unknown'; - set $ngo_email_as_user 'true'; - set $ngo_extra_validity '0'; - set_by_lua $ngo_domain 'return os.getenv("NGO_DOMAIN")'; - set_by_lua $ngo_whitelist 'return os.getenv("NGO_WHITELIST")'; - set_by_lua $ngo_blacklist 'return os.getenv("NGO_BLACKLIST")'; - - expires 0; - - add_header Google-User $ngo_user; - - location / { - proxy_ignore_client_abort on; - access_by_lua_file "/etc/nginx/lua/access.lua"; - proxy_pass http://{{ .Upstream }}; - {{ end }} - {{ else }} - location / { - proxy_pass http://{{ .Upstream }}; - {{ end }} + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + {{ if and .Host.Env.NGO_CALLBACK_HOST .Host.Env.NGO_CLIENT_ID .Host.Env.NGO_CLIENT_SECRET .Host.Env.NGO_TOKEN_SECRET .Container.Env.OAUTH_ENABLED }} + {{ if eq .Container.Env.OAUTH_ENABLED "1" }} + # Enable oauth + resolver 8.8.8.8 ipv6=off; + + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + lua_ssl_verify_depth 5; + + set $ngo_callback_uri '/_oauth'; + set $ngo_callback_host '{{ .Host.Env.NGO_CALLBACK_HOST }}'; + set $ngo_client_id '{{ .Host.Env.NGO_CLIENT_ID }}'; + set $ngo_client_secret '{{ .Host.Env.NGO_CLIENT_SECRET }}'; + set $ngo_token_secret '{{ .Host.Env.NGO_TOKEN_SECRET }}'; + set $ngo_user 'unknown'; + set $ngo_email_as_user 'true'; + set $ngo_extra_validity '0'; + set_by_lua $ngo_domain 'return os.getenv("NGO_DOMAIN")'; + set_by_lua $ngo_whitelist 'return os.getenv("NGO_WHITELIST")'; + set_by_lua $ngo_blacklist 'return os.getenv("NGO_BLACKLIST")'; + + expires 0; + + add_header Google-User $ngo_user; + + location / { + proxy_ignore_client_abort on; + access_by_lua_file "/etc/nginx/lua/access.lua"; + proxy_pass http://{{ .Upstream }}; + {{ end }} + {{ else }} + location / { + proxy_pass http://{{ .Upstream }}; + {{ end }} + } } - } {{ end }} {{/* END: Variables */}} @@ -104,58 +105,58 @@ {{ range $hosts, $containers_vhost := groupByLabel $dc_containers "io.docksal.virtual-host" }} - {{ range $service, $containers := groupByLabel $containers_vhost "com.docker.compose.service" }} - {{ $pr_container := $containers | first }} - {{ $project := index $pr_container.Labels "com.docker.compose.project" }} - {{ $upstream := (print $project "-" $service) }} - - # -------------------------------------------------- # - # Reachable via "{{ $project }}_default" network - upstream {{ $upstream }} { - {{ range $container := $containers }} - {{ $addrLen := len $container.Addresses }} - - {{ range $knownNetwork := $CurrentContainer.Networks }} - {{ range $containerNetwork := $container.Networks }} - {{ if eq $knownNetwork.Name $containerNetwork.Name }} - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{/* Assume port 80 by default */}} - {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{ range $service, $containers := groupByLabel $containers_vhost "com.docker.compose.service" }} + {{ $pr_container := $containers | first }} + {{ $project := index $pr_container.Labels "com.docker.compose.project" }} + {{ $upstream := (print $project "-" $service) }} + + # -------------------------------------------------- # + # Reachable via "{{ $project }}_default" network + upstream {{ $upstream }} { + {{ range $container := $containers }} + {{ $addrLen := len $container.Addresses }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{/* Assume port 80 by default */}} + {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{ end }} + {{ end }} + {{ end }} {{ end }} - {{ end }} {{ end }} - {{ end }} - {{ end }} - } + } - {{/* Get the cert name from io.docksal.cert-name container label */}} - {{ $certName := or (index $pr_container.Labels "io.docksal.cert-name") "none" }} - {{/* Unset certName if its value us "none" */}} - {{ $certName := when (ne $certName "none") $certName nil }} + {{/* Get the cert name from io.docksal.cert-name container label */}} + {{ $certName := or (index $pr_container.Labels "io.docksal.cert-name") "none" }} + {{/* Unset certName if its value us "none" */}} + {{ $certName := when (ne $certName "none") $certName nil }} - {{/* Get the best matching cert by name for the vhost. */}} - {{ $primaryHost := (index (split $hosts ",") 0) }} - {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} + {{/* Get the best matching cert by name for the vhost. */}} + {{ $primaryHost := (index (split $hosts ",") 0) }} + {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} - {{/* Trim file suffix - it will be added later */}} - {{ $vhostCert := trimSuffix ".crt" $vhostCert }} - {{ $vhostCert := trimSuffix ".key" $vhostCert }} + {{/* Trim file suffix - it will be added later */}} + {{ $vhostCert := trimSuffix ".crt" $vhostCert }} + {{ $vhostCert := trimSuffix ".key" $vhostCert }} - {{/* Use the cert specified on the container or fallback to the best vhost match */}} - {{ $cert := (coalesce $certName $vhostCert) }} + {{/* Use the cert specified on the container or fallback to the best vhost match */}} + {{ $cert := (coalesce $certName $vhostCert) }} - {{/* Generate HTTP/HTTPS server config */}} - {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $pr_container "Host" $CurrentContainer) }} + {{/* Generate HTTP/HTTPS server config */}} + {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $pr_container "Host" $CurrentContainer) }} - {{ end }} - # -------------------------------------------------- # + {{ end }} + # -------------------------------------------------- # {{ end }} @@ -167,49 +168,49 @@ {{ range $hosts, $containers := groupByLabel $d_containers "io.docksal.virtual-host" }} - {{ $container := $containers | first }} - {{ $upstream := $container.Name }} - - upstream {{ $upstream }} { - {{ $addrLen := len $container.Addresses }} - - {{ range $knownNetwork := $CurrentContainer.Networks }} - {{ range $containerNetwork := $container.Networks }} - {{ if eq $knownNetwork.Name $containerNetwork.Name }} - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{/* Assume port 80 by default */}} - {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} - {{ end }} + {{ $container := $containers | first }} + {{ $upstream := $container.Name }} + + upstream {{ $upstream }} { + {{ $addrLen := len $container.Addresses }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{/* Assume port 80 by default */}} + {{ $port := or (index $container.Labels "io.docksal.virtual-port") "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{ end }} + {{ end }} + {{ end }} {{ end }} - {{ end }} - {{ end }} - } + } - {{/* Get the cert name from io.docksal.cert-name container label */}} - {{ $certName := or (index $container.Labels "io.docksal.cert-name") "none" }} - {{/* Unset certName if its value us "none" */}} - {{ $certName := when (ne $certName "none") $certName nil }} + {{/* Get the cert name from io.docksal.cert-name container label */}} + {{ $certName := or (index $container.Labels "io.docksal.cert-name") "none" }} + {{/* Unset certName if its value us "none" */}} + {{ $certName := when (ne $certName "none") $certName nil }} - {{/* Get the best matching cert by name for the vhost. */}} - {{ $primaryHost := (index (split $hosts ",") 0) }} - {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} + {{/* Get the best matching cert by name for the vhost. */}} + {{ $primaryHost := (index (split $hosts ",") 0) }} + {{ $vhostCert := (closest (dir "/etc/certs/custom") (printf "%s.crt" $primaryHost)) }} - {{/* Trim file suffix - it will be added later */}} - {{ $vhostCert := trimSuffix ".crt" $vhostCert }} - {{ $vhostCert := trimSuffix ".key" $vhostCert }} + {{/* Trim file suffix - it will be added later */}} + {{ $vhostCert := trimSuffix ".crt" $vhostCert }} + {{ $vhostCert := trimSuffix ".key" $vhostCert }} - {{/* Use the cert specified on the container or fallback to the best vhost match */}} - {{ $cert := (coalesce $certName $vhostCert) }} + {{/* Use the cert specified on the container or fallback to the best vhost match */}} + {{ $cert := (coalesce $certName $vhostCert) }} - {{/* Generate HTTP/HTTPS server config */}} - {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $container "Host" $CurrentContainer) }} + {{/* Generate HTTP/HTTPS server config */}} + {{ template "server" (dict "Hosts" $hosts "Upstream" $upstream "Cert" $cert "Container" $container "Host" $CurrentContainer) }} {{ end }} diff --git a/conf/nginx/nginx.conf.tmpl b/conf/nginx/nginx.conf.tmpl index b904779..7888d09 100644 --- a/conf/nginx/nginx.conf.tmpl +++ b/conf/nginx/nginx.conf.tmpl @@ -18,6 +18,7 @@ http { resolver 8.8.8.8 ipv6=off; + # required for checking server certificates (google, microsoft, etc), to which the lua code initiates connections lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; lua_ssl_verify_depth 5; @@ -32,7 +33,7 @@ http { access_log off; {{ end }} - lua_shared_dict hosts 1m; + lua_shared_dict hosts 1m; lua_shared_dict session 1m; # For Lua debugging only #lua_code_cache off;