Docs
API7 GatewaySecurity and complianceAPI SecurityConfigure Data Masking

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 -l

    Expected: 1 or more.

  • jq and 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 ADC v0.30.3; use a version whose help lists it. ADC looks up --gateway-group by gateway group display name, so API7_GATEWAY_GROUP_NAME is distinct from the UUID used by the compatibility Admin API. Enter the name of the same group identified by API7_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.yaml in 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; }; done

    Expected: 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:

data-mask-adc.yaml
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.log

Lint 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-mask

Expected: 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-mask

Expected: 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.

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.log

Expected: 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.yaml

Expected: no output.

Troubleshoot

SymptomCauseFix
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.