Traffic Connector for Nginx¶
API Traffic Connectors enable monitoring of API traffic for observability and security insights.
This guide provides a step-by-step process to integrate SentryFlow with Nginx Inc. Ingress Controller, aimed at enhancing API observability. It includes detailed commands for each step along with their explanations.
SentryFlow make use of following to provide visibility into API calls:
- Nginx njs module.
- Njs filter.
Prerequisites¶
- Nginx Inc. Ingress Controller. Follow this to deploy it.
How To¶
To Observe API calls of your workloads served by Nginx inc. ingress controller in Kubernetes environment, follow the below steps:
- Create the following configmap in the same namespace as ingress controller.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: sentryflow-nginx-inc # Do not change this
namespace: default
data:
sentryflow.js: |
const MAX_BODY_SIZE = 1_000_000; // 1 MB
function getVar(r, name) {
try {
const v = r.variables[name];
return (v === undefined || v === null) ? "" : v;
} catch (err) {
r.error(`getVar failed for ${name}: ${err}`);
return "";
}
}
function getIntSafely(r, name) {
try {
const v = r.variables[name];
return (v === undefined || v === null) ? 0 : v;
} catch (err) {
r.error(`getIntSafely failed for ${name}: ${err}`);
return 0;
}
}
function safeGet(value) {
return (value === undefined || value === null) ? "" : value;
}
function captureRequestBody(r) {
try {
let body = r.variables.request_body || "";
if (body.length > MAX_BODY_SIZE) {
r.log(`REQUEST BODY OVER 1 MB LIMIT, truncating`);
body = body.slice(0, MAX_BODY_SIZE);
}
return body;
} catch (err) {
r.error(`Failed to get request body: ${err}`);
return "";
}
}
function responseHandler(r, data, flags) {
try {
r.sendBuffer(data, flags);
if (!r._respInit) {
r._respChunks = [];
r._respBytes = 0;
r._tooLarge = false;
r._respInit = true;
}
if (data && !r._tooLarge) {
const newSize = r._respBytes + data.length;
if (newSize > MAX_BODY_SIZE) {
r.log(`RESPONSE BODY OVER 1 MB LIMIT, NOT CAPTURING RESPONSE BODY`);
r._tooLarge = true;
r._respChunks = null;
r._respBytes = 0;
} else {
r._respChunks.push(data);
r._respBytes = newSize;
}
}
if (!flags.last) return;
let responseBody = ""
if (r._respBytes > 0) {
try {
const merged = Buffer.concat(r._respChunks);
responseBody = new TextDecoder("utf-8").decode(merged);
} catch (err) {
r.error(`Failed to decode response body: ${err}`);
responseBody = "";
}
}
// Safety check: final string shouldn't exceed max
//
if (responseBody.length > MAX_BODY_SIZE) {
r.log(`FINAL STRING TOO LARGE: length=${responseBody.length} → cleared`);
responseBody = "";
}
r._respChunks = null;
r._respBytes = 0;
let apiEvent = {
"metadata": {
"timestamp": Date.parse(r.variables.time_iso8601.split("+")[0]) / 1000,
"receiver_name": "nginx",
"receiver_version": ngx.version,
},
"source": {
"ip": safeGet(r.remoteAddress),
"port": getIntSafely(r, "remote_port"),
},
"destination": {
"ip": getVar(r, "server_addr"),
"port": getIntSafely(r, "server_port"),
},
"request": {
"headers": {},
"body": getVar(r, "body_text"),
},
"response": {
"headers": {},
"body": responseBody,
},
"protocol": getVar(r, "server_protocol"),
};
const headersIn = r.headersIn || {};
for (const header in headersIn) {
const value = headersIn[header];
apiEvent.request.headers[header] = Array.isArray(value) ? value.join(",") : value;
}
apiEvent.request.headers[":scheme"] = getVar(r, "scheme")
apiEvent.request.headers[":path"] = safeGet(r.uri)
apiEvent.request.headers[":method"] = getVar(r, "request_method")
apiEvent.request.headers["body_bytes_sent"] = getVar(r, "body_bytes_sent")
apiEvent.request.headers["request_length"] = getVar(r, "request_length")
apiEvent.request.headers["request_time"] = getVar(r, "request_time")
apiEvent.request.headers["query"] = getVar(r, "query_string")
const headersOut = r.headersOut || {};
for (const header in headersOut) {
const value = headersOut[header];
apiEvent.response.headers[header] = Array.isArray(value) ? value.join(",") : value;
}
apiEvent.response.headers[":status"] = getVar(r, "status")
r.subrequest("/sentryflow", {
method: "POST",
body: JSON.stringify(apiEvent),
detached: true,
});
} catch (err) {
r.error(`responseHandler failed: ${err}`);
// send minimal event for failure
if (flags.last) {
r.subrequest("/sentryflow", {
method: "POST",
body: JSON.stringify({ error: err.toString() }),
detached: true,
});
}
}
}
export default {responseHandler, captureRequestBody};
EOF
- Add the following volume and volume-mount in ingress controller deployment:
...
volumes:
- name: sentryflow-nginx-inc
configMap:
name: sentryflow-nginx-inc
...
...
volumeMounts:
- mountPath: /etc/nginx/njs/sentryflow.js
name: sentryflow-nginx-inc
subPath: sentryflow.js
- Update ingress controller configmap as follows:
...
data:
http-snippets: |
js_path "/etc/nginx/njs/";
subrequest_output_buffer_size 32k;
js_import main from sentryflow.js;
location-snippets: |
js_body_filter main.responseHandler buffer_type=buffer;
js_set $body_text main.captureRequestBody;
server-snippets: |
location /sentryflow {
internal;
# Update SentryFlow URL with path to ingest access logs if required.
proxy_pass http://sentryflow.sentryflow:8081/api/v1/events;
proxy_method POST;
proxy_set_header accept "application/json";
proxy_set_header Content-Type "application/json";
}
- Install Sentryflow
Update the nginx deployment name, config map name, and namespace name before running the command.
helm upgrade --install sentryflow \
oci://public.ecr.aws/k9v9d5v2/sentryflow-helm-charts \
--version v0.1.4 \
--namespace sentryflow \
--create-namespace \
--set config.receivers.nginxIngressController.enabled=true \
--set config.receivers.nginxIngressController.deploymentName=<nginx deployment name> \
--set config.receivers.nginxIngressController.configMapName=<nginx config map name> \
--set config.receivers.nginxIngressController.namespace=<namespace where ingress controller is deployed>
- Modify discovery engine config map and restart the discovery engine deployment
data:
app.yaml: |
...
summary-engine:
sentryflow:
cron-interval: 0h0m30s
decode-jwt: true
enabled: true
include-bodies: true
redact-sensitive-data: false
sensitive-rules-files-path:
- /var/lib/sumengine/sensitive-data-rules.yaml
threshold: 10000
watcher:
...
sentryflow:
enabled: true
event-type:
access-log: true
metric: false
service:
enabled: true
name: sentryflow
port: "8080"
url: "sentryflow.sentryflow"
-
Trigger API calls to generate traffic.
-
Use SentryFlow log client to see the API Events.
Next Steps
Proceed to the API Security Use Case to learn how to view your API inventory, create collections, upload OpenAPI specifications, and scan for security findings.