Configure Data Masking
Mask selected URL-encoded request fields in API7 Gateway log output while leaving the upstream request unchanged.
Applies to:API7 Gateway 3.10.x
Deployments:DockerRoles:Platform engineer
Goal
A route that sends the original form body to its upstream while removing or redacting selected values in logger output.
The data-mask (opens in Plugin Hub docs) plugin runs in the log phase. It changes the data visible to access logs and logger plugins, but does not change the request received by the upstream or the response returned to the client. This example uses file-logger so you can compare both results; masking reduces exposure in this log and does not by itself establish compliance with a regulatory standard.
Before you begin
Set the variables before running any checks:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default" # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo-
An API7 Gateway control plane is reachable and your Dashboard token is valid:
curl -ks -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" \ -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected:
200. -
A gateway instance in the selected group reports
Healthy:curl -ks "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" \ -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -lExpected:
1or more. -
jqand Docker are available:jq --version docker version --format '{{.Client.Version}}'Expected: both commands print a version.
-
For the ADC path, ADC is installed and supports label selectors:
adc --version adc sync --help | grep -- '--label-selector'Expected: a version string and a help entry for
--label-selector. The option is documented in ADCv0.30.3; use a version whose help lists it. ADC looks up--gateway-groupby gateway group display name, soAPI7_GATEWAY_GROUP_NAMEis distinct from the UUID used by the compatibility Admin API. Enter the name of the same group identified byAPI7_GATEWAY_GROUP_ID. If similar group names make the lookup ambiguous, stop and confirm which group your installed ADC resolves before continuing.ADC writes
diff.yamlin the current directory. Before saving or running this guide's ADC examples, confirm that none of its local paths already exists:for file in data-mask-adc.yaml data-mask-cleanup.yaml diff.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; doneExpected: no output. This makes the local cleanup safe; otherwise use a new empty working directory.
This guide writes /tmp/mask-urlencoded-body.log inside one gateway container. List the matching containers before choosing the one that receives ${GATEWAY_URL} traffic:
docker ps --filter "name=api7-ee-gateway" --format '{{.Names}}'Expected: one or more candidate names. Select the single container whose published proxy port serves ${GATEWAY_URL}, then set and verify the exact target:
read -rp "Gateway container name: " GATEWAY_CONTAINER && export GATEWAY_CONTAINER
docker inspect --format '{{.Name}}' "${GATEWAY_CONTAINER}"
docker port "${GATEWAY_CONTAINER}"Expected: docker inspect prints only the chosen container name, and docker port shows its proxy listener. Do not continue until that container is the instance that will handle the validation request.
Confirm that this example's log path is unused:
docker exec "${GATEWAY_CONTAINER}" test ! -e /tmp/mask-urlencoded-body.log
echo $?Expected: 0. If the command prints 1, stop or choose another log path and change every occurrence before continuing; cleanup removes this exact file.
This guide creates only these gateway resources: data-mask-httpbin-service and data-mask-route. Check that both Admin API IDs are unused:
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/data-mask-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/routes/data-mask-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"For the Admin API path, expected: both responses report that the requested resource is absent. If a response has HTTP status 200 and a populated matching value.id, that ID is already in use; stop or change every occurrence before continuing. Do not treat another error as proof of absence: correct the token, gateway group, or request until the result is unambiguous. The PUT operations below update a matching ID.
ADC can encounter an object with the same name under a different backend ID. On the ADC path, continue only if adc diff shows creates for exactly this service and nested route and no update, replacement, or deletion of an existing object.
Steps
Choose either the Admin API path in steps 1–2 or the ADC path in step 3. Do not apply both. The example affects only requests matching POST /anything/data-mask; the log file grows with each matching request.
Step 1: Create a service with an upstream
curl -ks "${API7_ADMIN_URL}/apisix/admin/services/data-mask-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "data-mask-httpbin-service",
"labels": {"guide": "data-mask"},
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [{"host": "httpbin.org", "port": 80, "weight": 100}]
}
}'{ "value": { "id": "data-mask-httpbin-service", "name": "data-mask-httpbin-service", … } }What changed: the example service now sends traffic to httpbin.org; no route points to it yet.
Step 2: Create a route and enable the plugins
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/data-mask-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "data-mask-route",
"labels": {"guide": "data-mask"},
"paths": ["/anything/data-mask"],
"methods": ["POST"],
"service_id": "data-mask-httpbin-service",
"plugins": {
"data-mask": {
"request": [
{"type": "body", "body_format": "urlencoded", "name": "password", "action": "remove"},
{"type": "body", "body_format": "urlencoded", "name": "token", "action": "replace", "value": "*****"},
{"type": "body", "body_format": "urlencoded", "name": "card", "action": "regex", "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)", "value": "$1-****-****-$2"}
]
},
"file-logger": {
"include_req_body": true,
"path": "/tmp/mask-urlencoded-body.log"
}
}
}'{ "value": { "id": "data-mask-route", "name": "data-mask-route", … } }What changed: matching requests are proxied unchanged, while the logger receives a body with password removed, token replaced, and the middle card groups redacted.
Step 3: Apply the same configuration with ADC
Skip this alternative if you completed steps 1–2. Save the complete desired state:
services:
- name: data-mask-httpbin-service
labels:
guide: data-mask
upstream:
name: default
scheme: http
nodes:
- host: httpbin.org
port: 80
weight: 100
routes:
- name: data-mask-route
labels:
guide: data-mask
uris:
- /anything/data-mask
methods:
- POST
plugins:
data-mask:
request:
- action: remove
body_format: urlencoded
name: password
type: body
- action: replace
body_format: urlencoded
name: token
type: body
value: "*****"
- action: regex
body_format: urlencoded
name: card
regex: "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)"
type: body
value: "$1-****-****-$2"
file-logger:
include_req_body: true
path: /tmp/mask-urlencoded-body.logLint locally, validate against the backend, and inspect the diff:
adc lint -f data-mask-adc.yaml
adc validate -f data-mask-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=data-mask
adc diff -f data-mask-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=data-maskExpected: lint and validation succeed, and the diff contains only this example's service and nested route. The selector scopes reconciliation to the service labeled guide=data-mask; the route remains part of that selected service.
After reviewing the diff, apply it:
adc sync -f data-mask-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=data-maskExpected: ADC applies only the changes shown for the guide=data-mask partition.
What changed: ADC reconciled the reviewed service and route within the label partition.
Validate
Send the form and inspect what the upstream received:
curl -s "${GATEWAY_URL}/anything/data-mask" \
--data-urlencode "password=abc" \
--data-urlencode "token=xyz" \
--data-urlencode "card=1234-1234-1234-1234" | jq '{form: .form}'{
"form": {
"card": "1234-1234-1234-1234",
"password": "abc",
"token": "xyz"
}
}The original values prove that data-mask did not alter the upstream request. Now inspect the latest logger entry in the exact container selected earlier:
docker exec "${GATEWAY_CONTAINER}" sh -lc 'tail -n 1 /tmp/mask-urlencoded-body.log' | jq '{uri: .request.uri, method: .request.method, fields: (.request.body | split("&") | sort)}'{
"uri": "/anything/data-mask",
"method": "POST",
"fields": [
"card=1234-****-****-1234",
"token=*****"
]
}Roll back / Clean up
Use the cleanup path that matches how you created the resources.
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/data-mask-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/data-mask-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected: both commands print 200.
services: []adc lint -f data-mask-cleanup.yaml
adc diff -f data-mask-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=data-maskExpected: the diff deletes only this service and its nested route. Stop if it lists anything else.
adc sync -f data-mask-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=data-maskExpected: ADC removes only the reviewed resources in this label partition.
After verifying the route is gone, remove only this example's log file from the selected container:
docker exec "${GATEWAY_CONTAINER}" rm -f /tmp/mask-urlencoded-body.logExpected: no output. Another route can still match /anything/data-mask, so the data-plane response is not guaranteed to be 404.
Remove only the local example files you saved:
rm -f data-mask-adc.yaml data-mask-cleanup.yaml diff.yamlExpected: no output.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| The upstream form is masked. | Another plugin or upstream component is changing the request; data-mask runs in the log phase. | Inspect the route's other plugins and the upstream before attributing the change to data-mask. |
| The log file is absent or contains the original values. | The request reached another container, file-logger is unavailable, or the body format, field names, or logger settings differ. | Recheck the container mapping, confirm file-logger is enabled, and compare the URL-encoded input, three request rules, and include_req_body: true. |
| ADC proposes unrelated changes. | The selector is missing or another service uses the same guide label. | Stop before sync; use the exact selector and reserve guide=data-mask for this example. |
Related
data-maskplugin reference (opens in Plugin Hub docs) — review supported data locations, body formats, and actions.- Services and routes — understand the route scope where this logger configuration applies.
- Secure credentials — keep sensitive configuration values out of gateway objects.