> For the complete documentation index, see [llms.txt](https://docs.qpoint.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.qpoint.io/guides/qscan-guides/aws-eks-setup.md).

# AWS Setup: PII Scanning on EKS

Add PII scanning to your EKS cluster using Qtap, Qplane, AWS S3, and QScan. By the end of this guide, QScan will be scanning HTTP traffic captured by Qtap and reporting PII findings to the Pulse dashboard.

## Who This Is For

**Use this guide if you:**

* Have an EKS cluster (new or existing) where you want PII scanning
* Want to use Qplane for centralized management
* Are storing artifacts in AWS S3

**Choose something else if you:**

* Want a local development setup -- see [Self-Hosted Setup](/guides/qscan-guides/self-hosted-setup.md)
* Need GCP Cloud Run deployment -- see [Cloud Run Installation](/getting-started/qscan/installation/cloud-run.md)

**Time to complete:** 30 minutes

***

## Prerequisites

* EKS cluster with EC2 node groups (at least one node with 12 GB+ available memory for QScan)
* `kubectl` and `helm` configured for the cluster
* `aws` CLI configured
* A Qpoint account at [app.qpoint.io](https://app.qpoint.io)

***

## Set Your Variables

Replace these values throughout the guide:

```bash
export AWS_REGION="us-east-1"
export S3_BUCKET="your-org-qpoint-artifacts"
```

All commands below use these variables. Substitute your actual values before running.

{% hint style="info" %}
Shell variables work in CLI commands but not in YAML or JSON files. Before applying manifests, either replace the placeholder values manually or pipe through `envsubst`:

```bash
envsubst < qscan-deployment.yaml | kubectl apply -f -
```

{% endhint %}

### Create the Namespace

```bash
kubectl create namespace qpoint
```

***

## Step 1: Verify Outbound Access

Confirm the cluster can reach all required endpoints:

| Endpoint                        | Used By      | Purpose                                  |
| ------------------------------- | ------------ | ---------------------------------------- |
| `api.qpoint.io:443`             | Qtap         | Control plane API (registration, config) |
| `pulse.qpoint.io:443`           | Qtap         | Event and telemetry ingestion            |
| `api-pulse.qpoint.io:443`       | QScan        | Scan job polling and result reporting    |
| `s3.<region>.amazonaws.com:443` | Qtap + QScan | Object storage                           |

Run a quick egress test:

{% code overflow="wrap" %}

```bash
kubectl run --rm -it egress-test --image=curlimages/curl -- sh -c \
  "curl -s https://api.qpoint.io/health && \
   curl -s https://pulse.qpoint.io/health && \
   curl -s https://api-pulse.qpoint.io/health && \
   curl -s -o /dev/null -w '%{http_code}' https://s3.${AWS_REGION}.amazonaws.com"
```

{% endcode %}

{% hint style="warning" %}
If egress is restricted, all four endpoints must be allowlisted before proceeding.
{% endhint %}

***

## Step 2: Create AWS S3 Bucket and IAM Credentials

Set up object storage before connecting any agents to Qplane. This ensures captured payloads go directly to your S3 bucket and never to Qpoint Cloud storage.

### Create the S3 Bucket

```bash
aws s3 mb s3://${S3_BUCKET} --region ${AWS_REGION}
```

{% hint style="info" %}
S3 bucket names are globally unique across all AWS accounts. Choose a name with your organization's prefix (e.g., `acme-qpoint-artifacts`) to avoid conflicts.
{% endhint %}

### Harden the Bucket

Block all public access:

```bash
aws s3api put-public-access-block \
  --bucket ${S3_BUCKET} \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
```

Enable default encryption (SSE-S3):

```bash
aws s3api put-bucket-encryption \
  --bucket ${S3_BUCKET} \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
```

Enforce TLS-only access:

```bash
aws s3api put-bucket-policy --bucket ${S3_BUCKET} --policy '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::'"${S3_BUCKET}"'",
        "arn:aws:s3:::'"${S3_BUCKET}"'/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}'
```

{% hint style="info" %}
For stricter encryption requirements, use SSE-KMS instead of SSE-S3.
{% endhint %}

### Create IAM Credentials

Create an IAM user with the minimum required permissions:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::your-org-qpoint-artifacts"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-org-qpoint-artifacts/*"
    }
  ]
}
```

Includes `DeleteObject` for lifecycle policy cleanup. Remove if your retention policy handles cleanup externally.

{% hint style="info" %}
**Key rotation guidance:** Since Qtap requires static IAM keys, rotate them regularly. The process: create a new access key → update the `qpoint-s3-creds` Kubernetes secret → restart Qtap and QScan pods → verify they reconnect → delete the old access key.
{% endhint %}

### Store Credentials in Kubernetes

Create the secret now. Both Qtap and QScan will reference it in later steps.

```bash
kubectl create secret generic qpoint-s3-creds \
  --from-literal=AWS_ACCESS_KEY_ID='<access-key>' \
  --from-literal=AWS_SECRET_ACCESS_KEY='<secret-key>' \
  -n qpoint
```

***

## Step 3: Configure Qplane

Configure your Qplane environment with object storage **before** deploying Qtap agents. This prevents any payloads from being sent to Qpoint Cloud storage.

{% hint style="warning" %}
New Qplane accounts include a default "Basic Reporting and Error Detection" stack that begins capturing error payloads immediately when an agent connects. If object storage is not configured first, those payloads will be sent to Qpoint Cloud. See [Object Storage Configuration](/getting-started/qplane/configuration/object-storage.md) for details.
{% endhint %}

### Create an Environment

1. Go to [app.qpoint.io](https://app.qpoint.io)
2. Navigate to Settings -> Deploy -> Environments
3. Create a new environment and copy the registration token

### Add Object Storage

1. Navigate to Settings -> Deploy -> Services -> Object Stores
2. Click **"+ Add Object Store"** and configure:

| Field      | Value                                                                     |
| ---------- | ------------------------------------------------------------------------- |
| Endpoint   | `s3.us-east-1.amazonaws.com`                                              |
| Bucket     | `your-org-qpoint-artifacts`                                               |
| Region     | Your AWS region (e.g., `us-east-1`)                                       |
| Access URL | `https://your-org-qpoint-artifacts.s3.us-east-1.amazonaws.com/{{DIGEST}}` |

{% hint style="info" %}
Use the regionalized S3 endpoint (`s3.<region>.amazonaws.com`) to avoid redirect issues with buckets outside `us-east-1`.
{% endhint %}

### Verify in Snapshot YAML

Go to Settings -> Deploy -> Snapshot and confirm your object store appears in the `services.object_stores` section with the correct endpoint, bucket, and region.

***

## Step 4: Deploy Qtap

Now that Qplane has object storage configured, deploy Qtap. Choose the option that matches your deployment.

### Option A: Deploy with Helm (recommended)

```bash
# Store the registration token (key must be named "token" for the Helm chart)
kubectl create secret generic qtap-registration \
  --from-literal=token='<your-registration-token>' \
  -n qpoint

# Add the Qpoint Helm repo
helm repo add qpoint https://helm.qpoint.io
helm repo update

# Deploy Qtap with S3 credentials
helm install qtap qpoint/qtap \
  -n qpoint --create-namespace \
  --set registrationTokenSecretRefName="qtap-registration" \
  --set extraEnv[0].name="AWS_ACCESS_KEY_ID" \
  --set extraEnv[0].valueFrom.secretKeyRef.name="qpoint-s3-creds" \
  --set extraEnv[0].valueFrom.secretKeyRef.key="AWS_ACCESS_KEY_ID" \
  --set extraEnv[1].name="AWS_SECRET_ACCESS_KEY" \
  --set extraEnv[1].valueFrom.secretKeyRef.name="qpoint-s3-creds" \
  --set extraEnv[1].valueFrom.secretKeyRef.key="AWS_SECRET_ACCESS_KEY"
```

### Option B: Add Qplane to an existing manifest

If you manage Qtap through your own Kubernetes manifests, add the registration token and S3 credentials to your existing deployment. Qtap reads the `REGISTRATION_TOKEN` environment variable directly:

```bash
kubectl create secret generic qtap-registration \
  --from-literal=REGISTRATION_TOKEN='<your-registration-token>' \
  -n qpoint
```

Add `envFrom` to your existing Qtap container spec to inject both the registration token and S3 credentials:

```yaml
          envFrom:
            - secretRef:
                name: qtap-registration
            - secretRef:
                name: qpoint-s3-creds
```

Remove any local configuration (config file mounts, `--config` args) -- Qplane pushes configuration to the agent once it connects. Apply the updated manifest.

{% hint style="info" %}
If your cluster has restricted namespaces or security policies, you may need to adjust the namespace or add appropriate pod security labels. The Qtap pod requires privileged access for eBPF operations.
{% endhint %}

### Verify

1. Confirm the agent appears in the Qplane dashboard
2. Generate some HTTP traffic through a Qtap-monitored service
3. Check that objects appear in your S3 bucket:

```bash
aws s3 ls s3://${S3_BUCKET}/ --recursive | head
```

***

## Step 5: Deploy QScan

QScan is a separate container that runs PII detection models. It polls Pulse for scan jobs, pulls artifacts from S3, scans them, and reports findings back. Payload artifacts and scan processing stay in your AWS account. Only anonymized metadata and authentication flow to Qpoint Cloud over TLS.

### QScan Credentials

QScan needs AWS credentials for S3 access and a registration token for Pulse. Create a secret for the registration token:

```bash
kubectl create secret generic qscan-registration \
  --from-literal=REGISTRATION_TOKEN='<your-registration-token>' \
  -n qpoint
```

QScan uses the `qpoint-s3-creds` secret created in Step 2 for S3 access.

{% hint style="info" %}
Both Qtap and QScan require explicit static credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`). Neither supports IRSA or ambient credential chains.
{% endhint %}

{% code title="qscan-deployment.yaml" %}

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: qscan
  namespace: qpoint
  labels:
    app: qscan
spec:
  replicas: 1
  selector:
    matchLabels:
      app: qscan
  template:
    metadata:
      labels:
        app: qscan
    spec:
      containers:
        - name: qscan
          image: us-docker.pkg.dev/qpoint-edge/public/qscan:latest
          env:
            - name: REGISTRATION_TOKEN
              valueFrom:
                secretKeyRef:
                  name: qscan-registration
                  key: REGISTRATION_TOKEN
            - name: AWS_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: qpoint-s3-creds
                  key: AWS_ACCESS_KEY_ID
            - name: AWS_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: qpoint-s3-creds
                  key: AWS_SECRET_ACCESS_KEY
            - name: S3_ENDPOINT_URL
              value: "https://s3.us-east-1.amazonaws.com"  # Replace with your region
            - name: S3_BUCKET_NAME
              value: "your-org-qpoint-artifacts"  # Replace with your bucket
            - name: S3_REGION_NAME
              value: "us-east-1"  # Replace with your region
            - name: NUM_POLLERS
              value: "1"
            - name: NUM_SCANNERS
              value: "1"
            - name: LOG_LEVEL
              value: "info"
            - name: METRICS_PORT
              value: "8080"
          ports:
            - containerPort: 8080
              name: metrics
          resources:
            requests:
              cpu: "2"
              memory: "12Gi"
            limits:
              cpu: "6"
              memory: "24Gi"
          livenessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 30
            failureThreshold: 10
```

{% endcode %}

### Apply and Verify

```bash
kubectl apply -f qscan-deployment.yaml
```

{% hint style="warning" %}
QScan loads approximately 8.4 GB of ML models at startup. 12 GB memory is the absolute minimum; 24 GB is recommended. Initial model loading takes 1-2 minutes -- the startup probe accounts for this.
{% endhint %}

{% hint style="info" %}
`S3_ENDPOINT_URL` must be set explicitly for customer S3 buckets. Without it, QScan defaults to an internal Qpoint endpoint that will not work with your AWS credentials.
{% endhint %}

```bash
kubectl get pods -n qpoint -l app=qscan
kubectl logs -n qpoint deployment/qscan --tail 30
```

Look for log messages indicating that QScan has loaded models and is polling Pulse for scan jobs.

***

## Step 6: Enable PII Scanning in Qplane

Add the QScan plugin to your stack in the Qplane UI (Stacks and Plugins section).

### Recommended Settings

| Setting           | Recommended | Description                                          |
| ----------------- | ----------- | ---------------------------------------------------- |
| `sample_baseline` | `10`        | Always scan first 10 requests per endpoint           |
| `sample_rate`     | `0.1`       | Then scan 10% of subsequent requests                 |
| `cache_ttl`       | `24h`       | Cache results for 24 hours to reduce duplicate scans |
| `record_document` | `false`     | Do not store full document content with findings     |

### Monitor Types

Enable the PII types you want to detect:

`PERSON`, `EMAIL_ADDRESS`, `PHONE_NUMBER`, `US_SSN`, `CREDIT_CARD`, `STREET_ADDRESS`, `US_BANK_NUMBER`, `US_DRIVER_LICENSE`, `LOCATION`, `ORGANIZATION`

Set `record_value: false` on all monitors to report detections without storing the actual sensitive values.

{% hint style="info" %}
For initial testing, set `sample_rate: 1.0` to scan every request. Once you've confirmed detections are working, lower to `0.1` for production traffic.
{% endhint %}

***

## Step 7: Verify PII Detections

Once the plugin is active, QScan begins processing captured HTTP traffic:

1. **Qtap** captures HTTP payloads and stores artifacts in S3
2. **Pulse** schedules scan jobs for new artifacts
3. **QScan** polls for jobs, downloads artifacts, scans with ML models, and reports findings
4. **Findings** surface in the Qplane dashboard

Check QScan logs to confirm scanning activity:

```bash
kubectl logs -n qpoint deployment/qscan --tail 50
```

Open [app.qpoint.io](https://app.qpoint.io) and check the dashboards for PII findings associated with your endpoints.

***

## Scaling

If the scan queue backs up, scale QScan horizontally:

```bash
kubectl scale deployment qscan -n qpoint --replicas=3
```

Each replica polls Pulse independently. You can also increase `NUM_POLLERS` and `NUM_SCANNERS` per replica for more concurrency within a single pod, though this requires proportionally more memory.

***

## Production Considerations

| Consideration    | Detail                                                                                                                                                                                                                                                            |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Capture level    | QScan requires full HTTP capture (request and response bodies). Summary-only capture will not produce artifacts for scanning.                                                                                                                                     |
| S3 must match    | Qtap and QScan must point to the same S3 bucket and endpoint.                                                                                                                                                                                                     |
| Data sovereignty | Payload artifacts and scan processing stay in your AWS account. Only anonymized metadata and authentication flow to Qpoint Cloud over TLS.                                                                                                                        |
| S3 credentials   | Both Qtap and QScan require explicit `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. Neither supports IRSA or ambient credential chains. Rotate keys regularly: create new key → update Kubernetes secret → restart pods → delete old key. |
| S3 endpoint      | Always set `S3_ENDPOINT_URL` explicitly (e.g., `https://s3.us-east-1.amazonaws.com`). The default endpoint is an internal Qpoint service, not AWS S3.                                                                                                             |
| QScan resources  | Plan node capacity for 12 GB+ memory per QScan pod. Use dedicated node groups if needed. Resource values in this guide are starting points -- tune based on your workload.                                                                                        |
| Monitoring       | QScan exposes Prometheus metrics on the configured metrics port. Use `kubectl port-forward` to verify: `kubectl port-forward -n qpoint deployment/qscan 8080:8080` then `curl localhost:8080/metrics`.                                                            |

***

## Order of Operations

```
Egress   ->  S3 Bucket  ->  Configure  ->  Deploy   ->  Deploy  ->  Enable  ->  Scanning
Test         + IAM          Qplane         Qtap         QScan       Plugin      Active
```

1. Verify egress to Qpoint and S3 endpoints
2. Create S3 bucket and IAM credentials
3. Configure Qplane: environment, object store, verify in Snapshot
4. Deploy Qtap via Helm or manifest (S3 is already configured, so no payloads leak to Qpoint Cloud)
5. Deploy QScan (12 GB+ RAM, ML models load at startup)
6. Add QScan plugin in Qplane (monitors, sampling)
7. PII findings appear in Qplane dashboards
