From fe335425c12065ecfc5ab5a4b1c7fbe94ba40a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Mass=C3=A9?= Date: Wed, 7 Feb 2018 16:24:52 +0100 Subject: [PATCH] certificate registration --- .s2i/bin/run | 9 +- nginx.env.conf | 317 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 300 insertions(+), 26 deletions(-) diff --git a/.s2i/bin/run b/.s2i/bin/run index 4d1b489..a813d61 100755 --- a/.s2i/bin/run +++ b/.s2i/bin/run @@ -9,11 +9,14 @@ set -e # Default values are set here export "LOG_LEVEL=${LOG_LEVEL:=info}" export "NGINX_CONF=${NGINX_CONF:=/opt/app-root/etc/}" +export "BACKEND_ENDPOINT_OVERRIDE=${BACKEND_ENDPOINT_OVERRIDE:=https://su1.3scale.net}" +export "SSO_REALMS=${SSO_REALMS:=3scale}" +export "SSO_CLIENT_ID=${SSO_CLIENT_ID:=admin-cli}" # Process the environment variables in the nginx configuration file if [ -f "$NGINX_CONF/nginx.env.conf" ]; then - envsubst '$LOG_LEVEL:$RESOLVER:$APP_ROOT:$SSO_SERVICE_HOSTNAME:$PROXY_ROUTE_HOSTNAME' < "$NGINX_CONF/nginx.env.conf" > "$NGINX_CONF/nginx.conf" + envsubst '$LOG_LEVEL:$RESOLVER:$APP_ROOT:$SSO_SERVICE_HOSTNAME:$PROXY_ROUTE_HOSTNAME:$PROXY_ROUTE_HOSTNAME:$BACKEND_ENDPOINT_OVERRIDE:$THREESCALE_PORTAL_ENDPOINT:$SSO_REALMS:$THREESCALE_SERVICE_TOKEN:$THREESCALE_SERVICE_ID:$THREESCALE_ACCESS_TOKEN:$SSO_SERVICE_USERNAME:$SSO_SERVICE_PASSWORD:$SSO_CLIENT_ID' < "$NGINX_CONF/nginx.env.conf" > "$NGINX_CONF/nginx.conf" fi -# Run nginx with our custom config file -exec nginx -g "daemon off;" -c "$NGINX_CONF/nginx.conf" +# Run openresty with our custom config file +exec openresty -g "daemon off;" -c "$NGINX_CONF/nginx.conf" diff --git a/nginx.env.conf b/nginx.env.conf index 1761c8f..2d7f26c 100644 --- a/nginx.env.conf +++ b/nginx.env.conf @@ -1,33 +1,304 @@ error_log stderr ${LOG_LEVEL}; -worker_processes 1; +worker_processes 1; events { - worker_connections 1024; + worker_connections 1024; } http { - default_type text/plain; - sendfile on; - keepalive_timeout 65; - resolver ${RESOLVER} ipv6=off; - - server { - listen 8443 ssl; - server_name ${PROXY_ROUTE_HOSTNAME}; - - ssl on; - ssl_certificate ${APP_ROOT}/etc/serving-cert/tls.crt; - ssl_certificate_key ${APP_ROOT}/etc/serving-cert/tls.key; - - location / { - proxy_pass http://${SSO_SERVICE_HOSTNAME}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } + default_type text/plain; + sendfile on; + keepalive_timeout 65; + resolver ${RESOLVER} ipv6=off; + + server { + listen 8443 ssl; + server_name ${PROXY_ROUTE_HOSTNAME}; + + ssl on; + ssl_certificate ${APP_ROOT}/etc/serving-cert/tls.crt; + ssl_certificate_key ${APP_ROOT}/etc/serving-cert/tls.key; + + # Enable SSL/TLS Client Certificates Authentication + ssl_verify_client optional; + ssl_client_certificate ${APP_ROOT}/etc/ca-certs/ca-bundle.pem; + ssl_crl ${APP_ROOT}/etc/ca-certs/crl.pem; + + location ~ ^/auth/realms/(${SSO_REALMS})/register$ { + access_by_lua_block { + ngx.log(ngx.INFO, "VERIFY: ", ngx.var.ssl_client_verify) + if ngx.var.ssl_client_verify ~= "SUCCESS" then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"You need to authenticate using an SSL/TLS Client Certificate."}') + ngx.exit(ngx.HTTP_OK) + end + ngx.log(ngx.INFO, "Authenticated Client : ", ngx.var.ssl_client_s_dn) + } + + content_by_lua_block { + local realm = ngx.var[1]; -- captured from the location regex + ngx.log(ngx.INFO, "Received a registration request for realm " .. realm) + if ngx.req.get_method() ~= "POST" then + ngx.status = ngx.HTTP_NOT_ALLOWED + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"Only POST requests are accepted."}') + ngx.exit(ngx.HTTP_OK) + end + + if ngx.req.get_headers()["Content-Type"] ~= "application/x-www-form-urlencoded" then + ngx.status = 415 -- Unsupported Media + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"Wrong content-type. Must be application/x-www-form-urlencoded."}') + ngx.exit(ngx.HTTP_OK) + end + + ngx.req.read_body() + local args, err = ngx.req.get_post_args() + if not args then + ngx.status = ngx.HTTP_BAD_REQUEST + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"No post parameters found."}') + ngx.exit(ngx.HTTP_OK) + end + + if not args['apikey'] or args['apikey'] == '' then + ngx.status = ngx.HTTP_BAD_REQUEST + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"No apikey parameter found in the request."}') + ngx.exit(ngx.HTTP_OK) + end + + if not args['client_id'] or args['client_id'] == '' then + ngx.status = ngx.HTTP_BAD_REQUEST + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"No client_id parameter found in the request."}') + ngx.exit(ngx.HTTP_OK) + end + + local apikey = args['apikey'] + local client_id = args['client_id'] + + local http = require "resty.http" + local httpc = http.new() + + local res, err = httpc:request_uri("${BACKEND_ENDPOINT_OVERRIDE}/transactions/authrep.xml", { + query = { + service_token = "${THREESCALE_SERVICE_TOKEN}", + service_id = "${THREESCALE_SERVICE_ID}", + user_key = apikey + }, + method = "GET", + ssl_verify = false + }) + + if not res then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not get a response from the 3scale backend."}') + ngx.exit(ngx.HTTP_OK) + end + + if res.status ~= ngx.HTTP_OK then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Denied by the 3scale backend."}') + ngx.exit(ngx.HTTP_OK) + end + + res, err = httpc:request_uri("${THREESCALE_PORTAL_ENDPOINT}/admin/api/applications/find.json", { + query = { + access_token = "${THREESCALE_ACCESS_TOKEN}", + user_key = apikey + }, + method = "GET", + ssl_verify = false + }) + + if not res then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not get a response from the 3scale API."}') + ngx.exit(ngx.HTTP_OK) + end + + local cjson = require 'cjson' + local json_body = cjson.decode(res.body) + local apikey_account_id = json_body['application'] and json_body['application']['account_id'] + + res, err = httpc:request_uri("${THREESCALE_PORTAL_ENDPOINT}/admin/api/applications/find.json", { + query = { + access_token = "${THREESCALE_ACCESS_TOKEN}", + app_id = client_id + }, + method = "GET", + ssl_verify = false + }) + + if not res then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not get a response from the 3scale API."}') + ngx.exit(ngx.HTTP_OK) + end + + json_body = cjson.decode(res.body) + local client_account_id = json_body['application'] and json_body['application']['account_id'] + + if client_account_id ~= apikey_account_id then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"The apikey and client_id do not belong to the same account."}') + ngx.exit(ngx.HTTP_OK) + end + + res, err = httpc:request_uri("http://${SSO_SERVICE_HOSTNAME}/auth/realms/"..realm.."/protocol/openid-connect/token", { + body = ngx.encode_args({ + client_id = "${SSO_CLIENT_ID}", + username = "${SSO_SERVICE_USERNAME}", + password = "${SSO_SERVICE_PASSWORD}", + grant_type = "password" + }), + headers = { + ["Content-Type"] = "application/x-www-form-urlencoded", + }, + method = "POST", + ssl_verify = false + }) + + if not res or res.status ~= ngx.HTTP_OK then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not get an access token with admin privileges on RH-SSO."}') + ngx.exit(ngx.HTTP_OK) + end + + json_body = cjson.decode(res.body) + local access_token = json_body['access_token'] + + res, err = httpc:request_uri("http://${SSO_SERVICE_HOSTNAME}/auth/admin/realms/"..realm.."/clients", { + query = { + clientId = client_id + }, + headers = { + ["Authorization"] = "Bearer " .. access_token + }, + method = "GET", + ssl_verify = false + }) + + if not res or res.status ~= ngx.HTTP_OK then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not retrieve the list of clients in RH-SSO."}') + ngx.exit(ngx.HTTP_OK) + end + + json_body = cjson.decode(res.body) + local client_id_rhssoid = json_body[1] and json_body[1]['id'] + + if not client_id_rhssoid then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not find the client in RH-SSO."}') + ngx.exit(ngx.HTTP_OK) + end + + res, err = httpc:request_uri("http://${SSO_SERVICE_HOSTNAME}/auth/admin/realms/"..realm.."/clients/" .. client_id_rhssoid .. "/certificates/jwt.credential", { + method = "GET", + headers = { + ["Authorization"] = "Bearer " .. access_token + }, + ssl_verify = false + }) + + if not res or res.status ~= ngx.HTTP_OK then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not check client certificates in RH-SSO."}') + ngx.exit(ngx.HTTP_OK) + end + + json_body = cjson.decode(res.body) + local existing_certificate = json_body['certificate'] + + if existing_certificate then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"Already registered."}') + ngx.exit(ngx.HTTP_OK) + end + + local certificate = ngx.var.ssl_client_raw_cert + local start, len = string.find(certificate, "[-]+BEGIN CERTIFICATE[-]+") + start = start + len + local finish = string.find(certificate, "[-]+END CERTIFICATE[-]+") + certificate = string.sub(certificate, start, finish-1) + + local raw_certificate = "" + for i in string.gmatch(certificate, "[a-zA-Z0-9+/=]+") do + raw_certificate = raw_certificate .. i + end + + local boundary = string.format("--------------------------%s", ngx.var.request_id) + local content_type = "multipart/form-data; boundary=" .. boundary + + body = string.format('--%s\r\nContent-Disposition: form-data; name="file"\r\nContent-Type: application/octet-stream\r\n\r\n%s\r\n--%s\r\nContent-Disposition: form-data; name="keystoreFormat"\r\n\r\n%s\r\n--%s--\r\n', boundary, raw_certificate, boundary, "Certificate PEM", boundary) + ngx.say(body) + res, err = httpc:request_uri("http://${SSO_SERVICE_HOSTNAME}/auth/admin/realms/"..realm.."/clients/" .. client_id_rhssoid .. "/certificates/jwt.credential/upload", { + method = "POST", + body = body, + headers = { + ["Authorization"] = "Bearer " .. access_token, + ["Content-Type"] = content_type + }, + ssl_verify = false + }) + + if not res or res.status ~= ngx.HTTP_OK then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"server_error","error_description":"Could not register certificate in RH-SSO."}') + ngx.exit(ngx.HTTP_OK) + end + + ngx.status = ngx.HTTP_OK + ngx.header['Content-Type'] = 'application/json' + ngx.say('{ "status": "registered" }') + + ngx.exit(ngx.HTTP_OK) + } } -} + location ~ ^/auth/realms/(${SSO_REALMS})/protocol/openid-connect/token$ { + access_by_lua_block { + ngx.log(ngx.INFO, "VERIFY: ", ngx.var.ssl_client_verify) + if ngx.var.ssl_client_verify ~= "SUCCESS" then + ngx.status = ngx.HTTP_FORBIDDEN + ngx.header['Content-Type'] = 'application/json' + ngx.say('{"error":"invalid_request","error_description":"You need to authenticate using an SSL/TLS Client Certificate."}') + ngx.exit(ngx.HTTP_OK) + end + ngx.log(ngx.ERR, "Authenticated Client : ", ngx.var.ssl_client_s_dn) + } + # Beware: no slash at the end of the proxy_pass directive since we don't want to rewrite URLs + proxy_pass http://${SSO_SERVICE_HOSTNAME}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/.* { + # Beware: no slash at the end of the proxy_pass directive since we don't want to rewrite URLs + proxy_pass http://${SSO_SERVICE_HOSTNAME}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + } +}