From 7a65a01e94d12afa37dbf86fd1716db19c2d8521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Mass=C3=A9?= Date: Mon, 28 Aug 2017 17:08:28 +0200 Subject: [PATCH] first commit --- README.md | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ resolver.conf | 11 ++++ syslog-ng.conf | 12 +++++ verbose.lua | 106 ++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 README.md create mode 100644 resolver.conf create mode 100644 syslog-ng.conf create mode 100644 verbose.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..329870e --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# An Apicast module that logs API requests and responses + +## Description + +This project is an [Apicast](https://github.com/3scale/apicast/) module +that logs API requests and responses for non-repudiation purposes. + +## How it works + +This Apicast module intercepts API requests and sends them to a syslog server. +The request, response, headers and additional information are serialized +as JSON and sent to a syslog server. + +## Pre-requisites + +This projects requires : +- an [Apicast](https://github.com/3scale/apicast/) gateway +- a syslog server (such as [syslog-ng](https://github.com/balabit/syslog-ng) or [rsyslog](https://github.com/rsyslog/rsyslog)) +- the [lua-resty-logger-socket](https://github.com/cloudflare/lua-resty-logger-socket) module + +## Installation + +If not already done, start your syslog server and configure it to listen +for TCP connections on port 601. An exemple is given below with `syslog-ng`: + +``` +oadm policy add-scc-to-user privileged -z default +oc new-app balabit/syslog-ng --name syslog-ng +oc volume dc/syslog-ng --add --name log --type emptyDir --mount-path /var/log/ +oc create configmap syslog-ng --from-file=syslog-ng.conf +oc volume dc/syslog-ng --add --name=conf --mount-path /etc/syslog-ng/conf.d/ --type=configmap --configmap-name=syslog-ng +``` + +Then, update your `apicast-{staging,production}` to embed the required code, module and environment variables. + +Put `resolver.conf` in `/opt/app-root/src/apicast.d/resolver.conf`: +``` +oc create configmap resolver --from-file=resolver.conf +oc volume dc/apicast-staging --add --name=resolver --mount-path /opt/app-root/src/apicast.d/ --type=configmap --configmap-name=resolver +``` + +Put the `lua-resty-logger-socket` module in `/opt/app-root/src/src/resty/logger/`: +``` +git clone https://github.com/cloudflare/lua-resty-logger-socket.git +oc create configmap lua-resty-logger-socket --from-file=lua-resty-logger-socket/lib/resty/logger/socket.lua +oc volume dc/apicast-staging --add --name=lua-resty-logger-socket --mount-path /opt/app-root/src/src/resty/logger/ --type=configmap --configmap-name=lua-resty-logger-socket +``` + +Put the `verbose.lua` module in `/opt/app-root/src/src/custom/`: +``` +oc create configmap apicast-logging --from-file=verbose.lua +oc volume dc/apicast-staging --add --name=apicast-logging --mount-path /opt/app-root/src/src/custom/ --type=configmap --configmap-name=apicast-logging +``` + +Set the configuration required by `verbose.lua` as environment variables and re-deploy apicast: +``` +oc env dc/apicast-staging APICAST_MODULE=custom/verbose +oc env dc/apicast-staging SYSLOG_PROTOCOL=tcp +oc env dc/apicast-staging SYSLOG_PORT=601 +oc env dc/apicast-staging SYSLOG_HOST=syslog-ng +oc rollout latest apicast-staging +``` + +## Message format + +The requests and responses are serialized as follow: + +``` +{ + "request": { + "request_id": "3b1b0d[...]", # The unique ID of the request + "raw": "R0VUIC8/dXN[...]", # The raw request (request line + headers), base64 encoded + "headers": { # The request headers as an object + "host": "echo-api.3scale.net", + "accept": "*/*", + "user-agent": "curl/7.54.0" + }, + "body": "3b1b0d587[...]", # The body of the request, base64 encoded + "method": "GET", # HTTP Method + "start_time": 1503929520.684, # The time at which the request has been received + "uri_args": { # The decoded querystring as an object + "foo": "bar" + }, + "http_version": 1.1 # The version of the HTTP protocol used to submit the request + }, + "response": { + "headers": { # The response headers as an object + "cache-control": "private", + "content-type": "application/json", + "x-content-type-options": "nosniff", + "connection": "keep-alive", + "content-length": "715", + "vary": "Origin" + }, + "body": "ewogICJtZXRob2Qi[...]", # The body of the response, base64 encoded + "status": 200 # The HTTP Status Code + }, + "upstream": { # See http://nginx.org/en/docs/http/ngx_http_upstream_module.html#variables + "response_length": "715", + "header_time": "0.352", + "addr": "107.21.49.219:443", + "response_time": "0.352", + "status": "200", + "connect_time": "0.261" + }, +} +``` + +## Development + +First of all, setup your development environment as explained [here](https://github.com/3scale/apicast/tree/master#development--testing). + +Then, issue the following commands : +``` +git clone TODO +git clone https://github.com/3scale/apicast.git +cd apicast +git checkout -b 3.0-stable +luarocks make apicast/*.rockspec --local +export THREESCALE_DEPLOYMENT_ENV=sandbox +export THREESCALE_PORTAL_ENDPOINT=https://@-admin.3scale.net +export SYSLOG_HOST=localhost +export SYSLOG_PORT=601 +export SYSLOG_PROTOCOL=tcp + +ln -s ../apicast-logger custom +export APICAST_MODULE=custom/verbose + +bin/apicast -vvvv -i 0 -m off +``` + +## Troubleshooting + +When troubleshooting, keep in mind that the underlying `lua-resty-logger-socket` +module is asynchronous. When the logs cannot be sent to the syslog server, +the error is caught **ONLY UPON THE NEXT REQUESTS**. So, you might have to send +a couple requests before seeing errors in the logs. + +If you need to troubleshoot DNS issue : +``` +dig syslog-ng.3scale.svc.cluster.local +dig -p5353 @127.0.0.1 syslog-ng.3scale.svc.cluster.local +``` diff --git a/resolver.conf b/resolver.conf new file mode 100644 index 0000000..d81b8eb --- /dev/null +++ b/resolver.conf @@ -0,0 +1,11 @@ +# Nginx Configuration - To be placed in the 'apicast.d' folder. +# +# We need to add a resolver stanza since the underlying 'lua-resty-logger-socket' +# module use the nginx resolver and not ours. +# +# See https://github.com/openresty/lua-nginx-module#tcpsockconnect +# +# On Apicast Docker images, a built-in dnsmasq resolver is embedded +# and available on localhost port 5353 +# +resolver 127.0.0.1:5353; diff --git a/syslog-ng.conf b/syslog-ng.conf new file mode 100644 index 0000000..11253fc --- /dev/null +++ b/syslog-ng.conf @@ -0,0 +1,12 @@ +source s_network { + tcp(port(601)); +}; + +destination d_apicast { + file("/var/log/apicast.log"); +}; + +log { + source(s_network); + destination(d_apicast); +}; diff --git a/verbose.lua b/verbose.lua new file mode 100644 index 0000000..182e204 --- /dev/null +++ b/verbose.lua @@ -0,0 +1,106 @@ +local apicast = require('apicast').new() +local cjson = require('cjson') +local logger = require("resty.logger.socket") + +local _M = { _VERSION = '0.0' } +local mt = { __index = setmetatable(_M, { __index = apicast }) } + +function _M.new() + return setmetatable({}, mt) +end + +function _M:init() + local host = os.getenv('SYSLOG_HOST') + local port = os.getenv('SYSLOG_PORT') + local proto = os.getenv('SYSLOG_PROTOCOL') or 'tcp' + local flush_limit = os.getenv('SYSLOG_FLUSH_LIMIT') or '0' + local drop_limit = os.getenv('SYSLOG_DROP_LIMIT') or '1048576' + + if (host == nil or host == "") then + ngx.log(ngx.ERR, "The environment SYSLOG_HOST is NOT defined !") + end + + if (port == nil or port == "") then + ngx.log(ngx.ERR, "The environment SYSLOG_PORT is NOT defined !") + end + + port = tonumber(port) + flush_limit = tonumber(flush_limit) + drop_limit = tonumber(drop_limit) + ngx.log(ngx.WARN, "Sending custom logs to " .. proto .. "://" .. host .. ":" .. port .. " with flush_limit = " .. flush_limit .. " and drop_limit = " .. drop_limit) + + if not logger.initted() then + local ok, err = logger.init{ + host = host, + port = port, + sock_type = proto, + flush_limit = flush_limit, + drop_limit = drop_limit, + } + if not ok then + ngx.log(ngx.ERR, "failed to initialize the logger: ", err) + end + end + + return apicast:init() +end + + +function do_log(payload) + -- construct the custom access log message in + -- the Lua variable "msg" + local bytes, err = logger.log(payload) + if err then + ngx.log(ngx.ERR, "failed to log message: ", err) + end +end + +-- This function is called for each chunk of response received from upstream server +-- when the last chunk is received, ngx.arg[2] is true. +function _M.body_filter() + ngx.ctx.buffered = (ngx.ctx.buffered or "") .. ngx.arg[1] + + if ngx.arg[2] then -- EOF + local dict = {} + + -- Gather information of the request + local request = {} + if ngx.var.request_body then + request["body"] = ngx.encode_base64(ngx.var.request_body) + end + request["headers"] = ngx.req.get_headers() + request["start_time"] = ngx.req.start_time() + request["http_version"] = ngx.req.http_version() + request["raw"] = ngx.encode_base64(ngx.req.raw_header()) + request["method"] = ngx.req.get_method() + request["uri_args"] = ngx.req.get_uri_args() + request["request_id"] = ngx.var.request_id + dict["request"] = request + + -- Gather information of the response + local response = {} + if ngx.ctx.buffered then + response["body"] = ngx.encode_base64(ngx.ctx.buffered) + end + response["headers"] = ngx.resp.get_headers() + response["status"] = ngx.status + dict["response"] = response + + -- timing stats + local upstream = {} + upstream["addr"] = ngx.var.upstream_addr + upstream["bytes_received"] = ngx.var.upstream_bytes_received + upstream["cache_status"] = ngx.var.upstream_cache_status + upstream["connect_time"] = ngx.var.upstream_connect_time + upstream["header_time"] = ngx.var.upstream_header_time + upstream["response_length"] = ngx.var.upstream_response_length + upstream["response_time"] = ngx.var.upstream_response_time + upstream["status"] = ngx.var.upstream_status + dict["upstream"] = upstream + + do_log(cjson.encode(dict)) + end + return apicast:body_filter() +end + +return _M