---
name: connect-kubernetes-outpost
description: Deploy the SubImage Outpost so SubImage can reach private APIs (private Kubernetes clusters, on-prem Jamf, internal CrowdStrike, etc.) via an outbound Tailscale tunnel. Use when the user asks to "deploy SubImage Outpost", "connect a private Kubernetes cluster to SubImage", "scan an internal API with SubImage", or works in a Helm/Terraform/Docker repo and needs SubImage to reach something not on the public internet. Covers Helm and Docker paths.
---

# Connect a SubImage Outpost (private API access)

## What this does

Deploys a lightweight container in the customer's private network that establishes an outbound Tailscale tunnel back to SubImage and proxies sync traffic to an internal HTTPS endpoint. SubImage modules that support outpost connectivity then route through this tunnel instead of the public internet.

## When to use

✅ The target API is not reachable from the public internet (private Kubernetes clusters, on-prem services, VPN-only IT tools).
✅ User wants to scan an internal Jamf, BigFix, Kandji, SnipeIT, CrowdStrike, LastPass, or Semgrep instance.
✅ User wants the deployment committed in their Helm/Terraform repo.

❌ The cluster's API endpoint is public: no outpost needed; configure the EKS/GKE module directly.
❌ The user wants to grant in-cluster RBAC for SubImage to scan resources: that is the **EKS RBAC** step (Access Entries or `aws-auth` ConfigMap), separate from the outpost. Do that **after** the outpost is up.

Outpost-eligible modules: `bigfix`, `crowdstrike`, `jamf`, `kandji`, `kubernetes` (private API endpoint, EKS or self-managed), `lastpass`, `semgrep`, `snipeit`.

## Required inputs

The Tailscale credential is **issued by SubImage**, not generated by the user. Stop and contact SubImage if you do not already have it. **If any input is missing, ask the user explicitly.**

| Value | Where to find it | If missing, ask |
|---|---|---|
| `<TENANT_ID>` | SubImage tenant slug. Visible in the SubImage UI URL or at **Settings → Modules**. | "What is your SubImage tenant ID (the slug, e.g. `acme`)? You can find it in your SubImage URL." |
| `<TAILSCALE_AUTHKEY>` | Tailscale OAuth client secret, **provided by SubImage Slack support**. Format: `tskey-client-...`. | "Do you already have the Tailscale OAuth client secret from SubImage? It starts with `tskey-client-`. If not, message the SubImage team in Slack to request it for tenant `<TENANT_ID>`." |
| `<NAME>` | Optional. Unique name for this outpost; only matters if you deploy multiple. Default: `subimage`. | "Is this the only outpost for this tenant, or should I assign a unique `NAME` (e.g. `eks-prod`, `it`)?" |
| `<PROXY_TARGET>` | Internal HTTPS URL the outpost will proxy to (e.g. `https://kubernetes.default.svc` for in-cluster, or `https://jamf.corp.example.com`). | "What internal URL should this outpost proxy traffic to?" |
| `<VERIFY_TLS>` | `true` for valid public certs, `false` for self-signed (common with the K8s API). | "Does the target endpoint use a publicly-signed TLS cert? If yes, `VERIFY_TLS=true`. If self-signed (e.g. K8s API), use `false`." |
| Deployment platform | Helm (Kubernetes) or Docker (ECS/EC2/Cloud Run). | "Will you deploy on Kubernetes (Helm chart) or as a standalone container (Docker)?" |

The Tailscale hostname is derived as `<TENANT_ID>-<NAME>-outpost`. You will paste this hostname into the SubImage module config in Step 4.

## Gotchas

Read these before generating any commands; they correct the most common wrong assumptions.

- **The Helm chart auto-appends `?ephemeral=true` to the auth key. The Docker path does NOT.** If you copy a Helm-style auth key into a `docker run` command without `?ephemeral=true`, the connection succeeds once and then refuses to reconnect after the first reboot. Always include `?ephemeral=true` in the Docker form.
- **All outposts for the same tenant share the same OAuth secret and Tailscale tag.** Only the hostname (`<TENANT_ID>-<NAME>-outpost`) differs. Do not ask SubImage for separate secrets per outpost; reuse the one you have.
- **The outpost only opens an OUTBOUND WireGuard connection.** No inbound firewall changes are needed. If the security team asks "what ports do we open", the answer is "none". Do not invent ingress rules.
- **An outpost gives reachability, not authorization.** For private EKS, after the outpost is up you still need cluster-level RBAC for `SubImageScanRole` (Access Entries or `aws-auth`). Outpost is the network half; RBAC is the auth half.
- **`verifyTls: false` is the right answer for the K8s API and the wrong default for everything else.** The Kubernetes API server typically uses a self-signed cert, so `false` is correct there. For Jamf, BigFix, etc. that have real public certs, leave it `true` (or unset) and only flip to `false` if you confirm the target really self-signs.
- **Hostname mismatch is the silent failure mode.** The value entered in the SubImage module config must equal `<TENANT_ID>-<NAME>-outpost` exactly. A typo produces no log error; the sync just fails to connect.
- **Do not pass secrets via `--set`.** Helm puts `--set` values in shell history. Use a values file (`-f values.yaml`) for `authKey`. Same caveat for any Terraform variable holding the auth key: mark it `sensitive = true`.
- **Do not pass the placeholder strings.** `<TENANT_ID>`, `<TAILSCALE_AUTHKEY>`, `<PROXY_TARGET>` must be substituted. The container will start with literal angle-brackets in env vars and fail at the Tailscale handshake stage with an unhelpful "invalid auth" message.

## Path A: Helm (recommended for Kubernetes)

```bash
helm repo add subimage https://subimagesec.github.io/helm-charts
helm repo update
```

Create `values.yaml` (do not pass secrets via `--set`, they end up in shell history):

```yaml
outpost:
  tenantId: "<TENANT_ID>"
  authKey: "<TAILSCALE_AUTHKEY>"      # without ?ephemeral=true: chart adds it
  proxyTarget: "<PROXY_TARGET>"
  verifyTls: <VERIFY_TLS>             # boolean, no quotes
  # name: "<NAME>"                    # uncomment if not the only outpost
```

Install:

```bash
helm install subimage-outpost subimage/subimage-outpost -f values.yaml
```

Terraform wrapper if your repo is HCL-driven:

```hcl
resource "helm_release" "subimage_outpost" {
  name       = "subimage-outpost"
  repository = "https://subimagesec.github.io/helm-charts"
  chart      = "subimage-outpost"

  values = [yamlencode({
    outpost = {
      tenantId    = var.subimage_tenant_id
      authKey     = var.subimage_tailscale_authkey  # mark variable sensitive
      proxyTarget = var.subimage_proxy_target
      verifyTls   = var.subimage_verify_tls
      # name      = var.subimage_outpost_name
    }
  })]
}
```

Advanced options (RBAC, corporate proxy, network policies, pod security, node selectors) live in the chart README: https://github.com/subimagesec/helm-charts/blob/main/charts/subimage-outpost/README.md

## Path B: Docker

```bash
docker pull ghcr.io/subimagesec/subimage-outpost:latest

docker run -d \
  --name subimage-outpost \
  --restart unless-stopped \
  -e TAILSCALE_AUTHKEY='<TAILSCALE_AUTHKEY>?ephemeral=true' \
  -e TENANT_ID=<TENANT_ID> \
  -e NAME=<NAME> \
  -e PROXY_TARGET=<PROXY_TARGET> \
  -e VERIFY_TLS=<VERIFY_TLS> \
  ghcr.io/subimagesec/subimage-outpost:latest
```

Optional env vars:

- `ENVIRONMENT`: defaults to `prod`. Used in the Tailscale tag `tag:<TENANT_ID>-<ENVIRONMENT>-outpost`. All outposts in the same tenant+environment share that tag regardless of `NAME`.
- `PROXY_HOST`: overrides the `Host` header sent to `PROXY_TARGET`. Useful when the target expects a virtual host different from the URL (e.g. `eks.internal.acme.com` while `PROXY_TARGET` is an IP).

`?ephemeral=true` matters: the docker path requires it on the auth key (the Helm chart adds it automatically).

For production stability, pin a version (`:1.0.0`) instead of `:latest` and roll forward intentionally.

## Verify the outpost is running

Helm/Kubernetes:

```bash
kubectl get pods -l app.kubernetes.io/name=subimage-outpost
kubectl logs -l app.kubernetes.io/name=subimage-outpost --tail=50
kubectl exec deployment/subimage-outpost-subimage-outpost -- tailscale status
```

Docker:

```bash
docker ps | grep subimage-outpost
docker logs subimage-outpost --tail 50
```

The logs should show: Tailscale connection successful, Proxy server started, Tailscale serve started.

## Step 4: Connect a SubImage module to the outpost

Once the outpost is up, the actual scan still happens through whichever SubImage module needs to reach the private endpoint.

1. SubImage UI → **Modules**.
2. Find the target module (outpost-eligible ones show a cell tower icon).
3. **Config** → fill in the module's normal fields (URL, credentials, etc.).
4. **Tailscale outpost hostname** field: enter `<TENANT_ID>-<NAME>-outpost` (or `<TENANT_ID>-subimage-outpost` if you used the default name).
5. Save and **Run Sync**.

The Kubernetes module supports a per-row override that takes precedence over the module-level hostname:

- **EKS rows**: when adding a cluster ARN, check **override outpost hostname?** and enter the per-cluster hostname.
- **Self-managed rows**: enter the outpost hostname directly on the row.

For private EKS, after the outpost is reachable you still need to grant cluster-level RBAC to `SubImageScanRole`. The full Access Entries / `aws-auth` recipe lives at https://app.subimage.io/docs/modules/eks. The short version:

```bash
aws eks create-access-entry \
  --cluster-name <cluster-name> \
  --principal-arn arn:aws:iam::<aws-account-id>:role/SubImageScanRole \
  --type STANDARD \
  --username subimage-scan

# Then apply the subimage-viewer ClusterRole + ClusterRoleBinding from the doc.
```

## Updating the outpost

Helm:

```bash
helm repo update
helm upgrade subimage-outpost subimage/subimage-outpost -f values.yaml
```

Docker:

```bash
docker pull ghcr.io/subimagesec/subimage-outpost:latest
docker stop subimage-outpost && docker rm subimage-outpost
# Re-run the same docker run command.
```

## Troubleshooting

- **Container exits immediately**: usually an invalid `TAILSCALE_AUTHKEY` or missing `TENANT_ID`. Check `docker logs` / `kubectl logs`. If invalid, ping SubImage in Slack to reissue.
- **Outpost connects but module sync fails**: the `PROXY_TARGET` URL is wrong or unreachable from the outpost container. Verify with `docker exec subimage-outpost curl -v <PROXY_TARGET>` (or `kubectl exec`).
- **TLS errors talking to the target**: set `VERIFY_TLS=false` only if the target really has a self-signed cert (the K8s API does).
- **Hostname mismatch**: the value in the module config and the value derived from `<TENANT_ID>-<NAME>-outpost` must match exactly.

## Security notes

- The outpost only opens an **outbound** WireGuard connection to Tailscale. No inbound firewall changes are needed.
- All traffic between SubImage and the outpost is encrypted via WireGuard.
- The `TAILSCALE_AUTHKEY` is a credential. Keep it out of shell history (use a values file, not `--set`).

## References

- Canonical doc: https://app.subimage.io/docs/subimage_outpost
- Outpost image: https://github.com/subimagesec/subimage-outpost
- Helm chart: https://github.com/subimagesec/helm-charts
