SubImage Outpost

Set up a SubImage Outpost

SubImage Outpost allows you to securely connect SubImage to internal APIs that are not exposed to the internet. This is accomplished by deploying a lightweight container in your private network that proxies traffic through an encrypted Tailscale connection.

tip

Set up with an AI agent Using Claude Code, Cursor, Codex CLI, or another coding agent? The subimage-setup:connect-kubernetes-outpost skill walks the agent through every choice on this page. Paste this into your agent:

Use curl to download, read and follow: https://skills.subimage.io/plugins/subimage-setup/skills/connect-kubernetes-outpost/SKILL.md

Or install the marketplace once with claude plugin marketplace add subimagesec/skills. Full instructions on Agent Skills.

Resources

When to use SubImage Outpost

Use SubImage Outpost when you need to connect SubImage to:

  • Internal Kubernetes clusters with API endpoints not exposed to the internet
  • On-premise services like BigFix, Jamf, Kandji, or SnipeIT
  • Internal instances of CrowdStrike, LastPass, or Semgrep

Supported modules

The following SubImage modules support outpost connectivity:

  • BigFix
  • CrowdStrike
  • Kubernetes
  • Jamf
  • Kandji
  • LastPass
  • Semgrep
  • SnipeIT

Prerequisites

Before setting up an outpost, you must:

  1. Get your SubImage values: your tenant ID and your Outpost registration key, both available from Settings → Outposts (see Step 1)

  2. Deployment platform - Choose one:

    • Docker or container runtime in your private network
    • Kubernetes cluster (recommended - use the Helm chart)
  3. Network access from the outpost container to your internal API endpoint

Setup steps

Step 1: Get your registration key

The registration key is self-serve. Do this first: everything after it is deployment mechanics.

  1. Open Settings → Outposts, at /settings/outposts on your SubImage URL (for example https://acme.subimage.io/settings/outposts).
  2. In the Outpost registration key field, click Reveal, then copy the value.
  3. The key starts with tskey-client-. It authenticates your outpost to the SubImage tailnet. SubImage manages the Tailscale account, so you never sign up for Tailscale yourself.

Your tenant ID is the tenant slug in the same URL (acme in https://acme.subimage.io).

A few things worth knowing before you deploy:

  • Revealing the key requires the admin role. Operators can see the registered outposts but not the key. If you are not an admin, ask one to copy it for you.
  • The key is not show-once. You can reveal it again at any time from the same page, so there is no "lost key, please reissue" path.
  • One key per deployment and environment. Every outpost registers with the same key; only the hostname differs. There is no per-outpost key to request.
  • Outposts sharing a key cannot connect to each other: Tailscale ACL rules block outpost-to-outpost traffic.

If the key field says outposts are not enabled for your deployment, ask the SubImage team via Slack to provision one. If it returns an error instead, that is a fault on the SubImage side. Either way it is not something you can fix from the settings page.

Step 2: Choose deployment method

SubImage Outpost can be deployed using:

  • Option A: Docker image (for ECS, Google Cloud Run, or anywhere else a Docker container can run)
  • Option B: Helm chart (recommended for Kubernetes clusters)

Both options use the same public image from GitHub Container Registry.

Option A: Deploy with Docker

Pull the image

The SubImage Outpost image is publicly available on GitHub Container Registry.

Pin an explicit version so restarts and rebuilds are reproducible and upgrades stay deliberate. Pick the latest release tag from the image's package page and substitute it for x.y.z below.

OUTPOST_VERSION=x.y.z

docker pull ghcr.io/subimagesec/subimage-outpost:${OUTPOST_VERSION}

:latest also works and picks up updates automatically, at the cost of a container whose version changes under you on the next restart.

Run the container

Make sure to append ?ephemeral=true to the TAILSCALE_AUTHKEY. Nothing appends it for you on the Docker path, and without it the node registers once and then refuses to reconnect after a restart.

If you are deploying multiple outposts, give each one a unique NAME so SubImage can generate a distinct Tailscale hostname for each deployment:

docker run -d \
  --name subimage-outpost \
  --restart unless-stopped \
  -e TAILSCALE_AUTHKEY='<outpost-registration-key>?ephemeral=true' \
  -e TENANT_ID=<TENANT_ID> \
  -e NAME=<unique-name-for-this-outpost> \
  -e PROXY_TARGET=<your-internal-api-url> \
  -e VERIFY_TLS=false \
  ghcr.io/subimagesec/subimage-outpost:${OUTPOST_VERSION}

Environment variables:

Variable Description Example
TAILSCALE_AUTHKEY Outpost registration key tskey-client-abc123...?ephemeral=true
TENANT_ID Your tenant ID <TENANT_ID>
PROXY_TARGET Internal API URL to proxy https://eks.internal.acme.com
NAME (Optional) Name for this outpost eks-prod (default: subimage)
VERIFY_TLS Verify TLS certificates (true or false) false (for self-signed certs)
PROXY_HOST (Optional) Override Host header sent to target eks.internal.acme.com
CA_BUNDLE (Optional) Path in the container to a CA bundle, used when verifying /etc/ssl/corp-ca.crt
PROXY_CONNECT_TIMEOUT (Optional) Seconds to wait for a connection to the target 15 (default)
PROXY_READ_TIMEOUT (Optional) Seconds to wait for the target's response 60 (default)
ENVIRONMENT Internal to SubImage. Leave unset unless told otherwise prod (default if not set)

On VERIFY_TLS: the defaults differ between the two deployment paths. The container image defaults to false, while the Helm chart defaults verifyTls to true. Set it explicitly rather than relying on either default. Raise PROXY_CONNECT_TIMEOUT and PROXY_READ_TIMEOUT when cluster DNS is slow or list calls are large.

On ENVIRONMENT: this is SubImage's own deployment environment, not yours. It only feeds the Tailscale tag, so setting it to staging because the cluster is a staging cluster breaks outpost discovery.

The TENANT_ID, ENVIRONMENT, and NAME automatically configure:

  • Tailscale hostname: {TENANT_ID}-{NAME}-outpost
  • Tailscale tags: tag:{TENANT_ID}-{ENVIRONMENT}-outpost

Note: All outposts within the same tenant+environment share the same Tailscale tag, regardless of their NAME.

Option B: Deploy with Helm (Kubernetes)

For Kubernetes environments, the Helm chart provides easier deployment and management.

Add the Helm repository

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

Create a values file

Copy the following minimum configuration and fill in your values:

# values.yaml
outpost:
  # REQUIRED: Your tenant ID
  tenantId: "<TENANT_ID>"

  # REQUIRED: Internal API URL to proxy traffic to
  proxyTarget: "https://kubernetes.default.svc"

  # Verify TLS certificates. Keep this true for the in-cluster Kubernetes API:
  # the outpost auto-detects the serviceaccount CA and verifies properly.
  verifyTls: true

  # OPTIONAL: Unique name for this outpost (default: "subimage")
  # Only needed if deploying multiple outposts for the same tenant
  # name: "eks-prod"

  # REQUIRED: Outpost registration key from Settings > Outposts.
  # Note the nested "value" key: a bare string here does not render.
  authKey:
    # Do not add "?ephemeral=true": the chart appends it when it creates
    # the secret, which is the default
    value: "<outpost-registration-key>"

# OPTIONAL: the chart grants cluster-wide list on Secrets by default, which
# returns Secret values. Uncomment to opt out if the outpost does not need them.
# rbac:
#   secrets: false

The authKey block is the one place worth double-checking. It must be authKey.value, not authKey: "<key>"; the scalar form fails to render (see Troubleshooting).

The chart appends ?ephemeral=true only when it creates the secret. If you point it at a pre-existing secret with authKey.secret.create: false, include ?ephemeral=true in that secret's value yourself.

Install

The chart creates the subimage-outpost namespace and deploys into it, so you do not need to create one or pass --namespace:

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

The Tailscale hostname is derived automatically: {tenantId}-{name}-outpost (e.g., <TENANT_ID>-subimage-outpost). Enter this hostname in the module configuration in SubImage (see Step 4).

Security Note: Always use a values file (-f) for sensitive data like the registration key. Avoid passing secrets via --set on the command line, as they are exposed in shell history.

Full Helm chart documentation

For advanced options (RBAC, corporate proxy, network policies, pod security, node selectors), see the Helm chart README.

Step 3: Verify the outpost is running

Start in SubImage: Settings → Outposts lists every outpost registered for your deployment, with its hostname, Online or Offline status, last-seen time, and outpost version. When SubImage can reach the outpost, a Logs button on its row shows that outpost's logs without any cluster access, which is the fastest check available.

The Logs button appears only for outposts SubImage can reach and whose version it can read, so a container that never finished starting up, or one running a release older than 1.1.0, will not offer it. In those cases fall back to the local commands below.

Expect the hostname <TENANT_ID>-<NAME>-outpost to appear as Online within a minute or two of deploying. If it does not show up, inspect the container locally.

For Docker deployments:

# Check container status
docker ps | grep subimage-outpost

# View container logs
docker logs subimage-outpost --tail 50

For Helm/Kubernetes deployments:

The chart deploys into the subimage-outpost namespace, and the deployment is named after your Helm release (subimage-outpost if you used the install command above):

# Check pod status
kubectl get pods -n subimage-outpost -l app.kubernetes.io/name=subimage-outpost

# View logs
kubectl logs -n subimage-outpost -l app.kubernetes.io/name=subimage-outpost --tail=50

# Check Tailscale connection
kubectl exec -n subimage-outpost deployment/subimage-outpost -- tailscale status

A healthy startup logs these lines, in order:

Connected to Tailscale
Starting proxy server on port ...
Exposing proxy via Tailscale serve...
Outpost is ready and serving

Anything short of Outpost is ready and serving means the outpost is not usable yet.

Step 4: Connect a module to the outpost

Once the outpost is running, you need to tell SubImage which modules should route traffic through it.

  1. Go to Modules in the SubImage UI

  2. Find your module (outpost-eligible modules are marked with a cell tower icon)

  3. Click Config to open the configuration modal

  4. Fill in the module-specific fields (API URL, credentials, etc.)

  5. In the Tailscale outpost hostname field, enter the hostname for your outpost:

    <TENANT_ID>-<NAME>-outpost

    For example, if you deployed with NAME=eks-prod, enter <TENANT_ID>-eks-prod-outpost. If you used the default name, enter <TENANT_ID>-subimage-outpost.

  6. Save and trigger a sync

Traffic for this module will now be routed through the outpost to reach your internal API.

Kubernetes: per-cluster outpost hostname

For Kubernetes, each cluster row can use a different outpost instead of relying on one module-level hostname. For EKS rows, check "override outpost hostname?" and enter the hostname for that cluster's outpost. For self-managed rows, enter the outpost hostname directly on the row. Row-level hostnames take precedence over the module-level Tailscale hostname.

Example: EKS cluster with private endpoint

Scenario: Your EKS cluster API endpoint is only accessible from within your VPC.

Setup:

  1. Copy your tenant ID and the Outpost registration key from Settings → Outposts

  2. Deploy the outpost in your VPC:

Option A: Docker (e.g., on an EC2 instance or ECS task):

docker run -d \
  --name subimage-outpost-eks \
  --restart unless-stopped \
  -e TAILSCALE_AUTHKEY='tskey-client-xyz789...?ephemeral=true' \
  -e TENANT_ID=<TENANT_ID> \
  -e NAME=eks-prod \
  -e PROXY_TARGET=https://ABCD1234.gr7.us-east-1.eks.amazonaws.com \
  -e VERIFY_TLS=false \
  ghcr.io/subimagesec/subimage-outpost:x.y.z

Option B: Helm (if running on Kubernetes in the same VPC):

# values.yaml
outpost:
  tenantId: "<TENANT_ID>"
  name: "eks-prod"
  proxyTarget: "https://kubernetes.default.svc"
  verifyTls: true
  authKey:
    value: "<outpost-registration-key>"
helm install subimage-outpost-eks subimage/subimage-outpost -f values.yaml
  1. In SubImage, configure the Kubernetes module:

    • Type: EKS
    • Cluster ARN: arn:aws:eks:us-east-1:123456789012:cluster/production
    • Custom outpost hostname: <TENANT_ID>-eks-prod-outpost
  2. Save and trigger a sync. SubImage will connect to your EKS cluster via the outpost.

Example: On-premise Jamf server

Scenario: Your Jamf server runs on-premise at https://jamf.corp.acme.com.

Setup:

  1. Copy your tenant ID and the Outpost registration key from Settings → Outposts

  2. Deploy the outpost in your corporate network:

Option A: Docker:

docker run -d \
  --name subimage-outpost-jamf \
  --restart unless-stopped \
  -e TAILSCALE_AUTHKEY='tskey-client-abc456...?ephemeral=true' \
  -e TENANT_ID=<TENANT_ID> \
  -e NAME=it \
  -e PROXY_TARGET=https://jamf.corp.acme.com \
  -e VERIFY_TLS=true \
  ghcr.io/subimagesec/subimage-outpost:x.y.z

Option B: Helm (if you have a Kubernetes cluster in your corporate network):

# values.yaml
outpost:
  tenantId: "<TENANT_ID>"
  name: "it"
  proxyTarget: "https://jamf.corp.acme.com"
  verifyTls: true
  authKey:
    value: "tskey-client-abc456..."
helm install subimage-outpost-jamf subimage/subimage-outpost -f values.yaml
  1. In SubImage, configure your Jamf module:

    • Jamf URL: https://jamf.corp.acme.com
    • Tailscale hostname: <TENANT_ID>-it-outpost
    • (other Jamf credentials as required)
  2. Save and trigger a sync.

Multiple outposts

You can deploy multiple outposts for different networks or services. Use different NAME values to create unique hostnames:

Outpost NAME Hostname Use case
Default (not set) <TENANT_ID>-subimage-outpost Single outpost setup
EKS Prod eks-prod <TENANT_ID>-eks-prod-outpost Production Kubernetes
EKS Staging eks-staging <TENANT_ID>-eks-staging-outpost Staging Kubernetes
IT Services it <TENANT_ID>-it-outpost SnipeIT, Jamf, etc.

Note: All outposts within the same tenant+environment share the same registration key and Tailscale tag. Only the hostname differs based on the NAME variable.

Troubleshooting

Container won't start or exits immediately

Check logs:

Docker:

docker logs subimage-outpost

Kubernetes:

kubectl logs -n subimage-outpost -l app.kubernetes.io/name=subimage-outpost

Common issues:

  • Invalid TAILSCALE_AUTHKEY - re-reveal the registration key from Settings > Outposts and compare it character for character. The key is stable, so a mismatch means a copy error, not a rotation
  • On Docker, a missing ?ephemeral=true on the auth key
  • Missing TENANT_ID - ensure you set the tenant ID from SubImage
  • Missing required environment variables - ensure all required vars are set
  • Port conflicts - ensure no other service is using the internal proxy port

Helm install fails to render

Error: can't evaluate field secret in type interface {}

Your values file sets outpost.authKey to a plain string. The chart expects the nested form:

outpost:
  authKey:
    value: "<outpost-registration-key>"

Outpost connects but sync fails

Verify network connectivity from inside the container:

docker exec subimage-outpost curl -v http://localhost:8080

Common issues:

  • PROXY_TARGET URL is incorrect or unreachable from the outpost container
  • Firewall rules blocking outpost from reaching internal API
  • TLS certificate verification failing - prefer pointing CA_BUNDLE (or outpost.caBundle) at the right CA, and set VERIFY_TLS=false only if the target really self-signs and no bundle is available

Kubernetes API returns 403 through the outpost

Reachability is working and authorization is not. An outpost is the network half only; which side owns the auth half depends on how you deployed it:

  • Helm with rbac.create: true (the default): the chart creates a ServiceAccount plus ClusterRole and ClusterRoleBinding, mounts the token, and the proxy injects it as a bearer token. Nothing else to grant
  • Docker, or Helm with rbac.create: false: authorization has to come from elsewhere, such as a bearer token you supply (BEARER_TOKEN or BEARER_TOKEN_PATH), or an EKS access entry for SubImageScanRole

Sync works without outpost but fails with outpost

Check the SubImage backend logs for errors related to the Tailscale proxy.

Common issues:

  • TAILSCALE_PROXY environment variable not set in SubImage backend
  • Hostname mismatch between outpost and module configuration
  • Tailscale ACL rules blocking traffic from backend to outpost

How to update the outpost container

Settings → Outposts marks an outpost outdated next to its hostname. Read that flag as "version not confirmed current" rather than "old image": SubImage also shows it when it cannot read the version at all, which happens when the outpost is offline or predates 1.1.0. Check the Online status first. If the outpost is offline, fix the connectivity before upgrading, since a restart will not help.

Docker:

Pull the new image and restart:

OUTPOST_VERSION=<new-release-tag>

docker pull ghcr.io/subimagesec/subimage-outpost:${OUTPOST_VERSION}
docker stop subimage-outpost
docker rm subimage-outpost
# Run the docker run command again with the same environment variables

Kubernetes/Helm:

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

Kubernetes/Helm-specific issues

For Kubernetes and Helm deployment troubleshooting, see the Helm chart troubleshooting guide.

Common Kubernetes issues include:

  • Pod stuck in Pending (insufficient resources, node selector issues)
  • ImagePullBackOff (network connectivity to ghcr.io)
  • Service mesh interference (check namespace labels)
  • RBAC errors (check ServiceAccount and ClusterRole configuration)

Security considerations

  • Authentication: The registration key should be treated as a secret. Keep it out of shell history by using a values file rather than --set, and mark any Terraform variable holding it sensitive = true
  • Network access: The outpost needs internet access and access to your internal API
  • No inbound firewall changes required: The outpost establishes an outbound WireGuard connection, as such it does not require any open ports
  • Encryption: All traffic between SubImage and the outpost is encrypted via WireGuard
  • Kubernetes RBAC: The Helm chart's default RBAC includes cluster-wide list on Secrets, and list returns Secret values, not just their names. If the outpost does not need Secret contents, set rbac: {secrets: false} in your values file before installing

Architecture

graph LR
    A[SubImage Backend<br/>Tailscale Client] -->|Encrypted<br/>WireGuard| B[SubImage Outpost<br/>Tailscale Proxy]
    B -->|HTTP/HTTPS| C[Internal API<br/>EKS, Jamf, etc.]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#e8f5e9

    subgraph "Your Private Network"
        B
        C
    end
  1. SubImage backend initiates sync with outpost-enabled module
  2. Backend routes traffic through Tailscale proxy to your outpost
  3. Outpost forwards requests to your internal API endpoint
  4. Responses flow back through the same path

Support

For questions or issues with SubImage Outpost:

  • Docker image issues: GitHub Issues - subimage-outpost
  • Helm chart issues: GitHub Issues - helm-charts
  • General support: Contact the SubImage team via Slack
  • Include your outpost container logs when reporting issues:
    • From SubImage: the Logs button on the outpost's row in Settings → Outposts
    • Docker: docker logs subimage-outpost
    • Kubernetes: kubectl logs -n subimage-outpost -l app.kubernetes.io/name=subimage-outpost
  • Specify which module you're trying to connect