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.
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.mdOr install the marketplace once with claude plugin marketplace add subimagesec/skills. Full instructions on Agent Skills.
Resources
- Docker Image: github.com/subimagesec/subimage-outpost
- Helm Chart: github.com/subimagesec/helm-charts
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:
Get your SubImage values: your tenant ID and your Outpost registration key, both available from Settings → Outposts (see Step 1)
Deployment platform - Choose one:
- Docker or container runtime in your private network
- Kubernetes cluster (recommended - use the Helm chart)
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.
- Open Settings → Outposts, at
/settings/outpostson your SubImage URL (for examplehttps://acme.subimage.io/settings/outposts). - In the Outpost registration key field, click Reveal, then copy the value.
- 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 | |
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 updateCreate 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: falseThe 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.yamlThe Tailscale hostname is derived automatically: {tenantId}-{name}-outpost (e.g., ). Enter this hostname in the module configuration in SubImage (see Step 4).<TENANT_ID>-subimage-outpost
Security Note: Always use a values file (
-f) for sensitive data like the registration key. Avoid passing secrets via--seton 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 to appear as Online within a minute or two of deploying. If it does not show up, inspect the container locally.<TENANT_ID>-<NAME>-outpost
For Docker deployments:
# Check container status
docker ps | grep subimage-outpost
# View container logs
docker logs subimage-outpost --tail 50For 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 statusA healthy startup logs these lines, in order:
Connected to Tailscale
Starting proxy server on port ...
Exposing proxy via Tailscale serve...
Outpost is ready and servingAnything 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.
Go to Modules in the SubImage UI
Find your module (outpost-eligible modules are marked with a cell tower icon)
Click Config to open the configuration modal
Fill in the module-specific fields (API URL, credentials, etc.)
In the Tailscale outpost hostname field, enter the hostname for your outpost:
<TENANT_ID>-<NAME>-outpostFor example, if you deployed with
NAME=eks-prod, enter. If you used the default name, enter<TENANT_ID>-eks-prod-outpost.<TENANT_ID>-subimage-outpostSave 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:
Copy your tenant ID and the Outpost registration key from Settings → Outposts
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.zOption 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.yamlIn 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
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:
Copy your tenant ID and the Outpost registration key from Settings → Outposts
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.zOption 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.yamlIn SubImage, configure your Jamf module:
- Jamf URL:
https://jamf.corp.acme.com - Tailscale hostname:
<TENANT_ID>-it-outpost - (other Jamf credentials as required)
- Jamf URL:
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) | |
Single outpost setup |
| EKS Prod | eks-prod |
|
Production Kubernetes |
| EKS Staging | eks-staging |
|
Staging Kubernetes |
| IT Services | it |
|
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-outpostKubernetes:
kubectl logs -n subimage-outpost -l app.kubernetes.io/name=subimage-outpostCommon 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=trueon 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:8080Common issues:
PROXY_TARGETURL is incorrect or unreachable from the outpost container- Firewall rules blocking outpost from reaching internal API
- TLS certificate verification failing - prefer pointing
CA_BUNDLE(oroutpost.caBundle) at the right CA, and setVERIFY_TLS=falseonly 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_TOKENorBEARER_TOKEN_PATH), or an EKS access entry forSubImageScanRole
Sync works without outpost but fails with outpost
Check the SubImage backend logs for errors related to the Tailscale proxy.
Common issues:
TAILSCALE_PROXYenvironment 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 variablesKubernetes/Helm:
helm repo update
helm upgrade subimage-outpost subimage/subimage-outpost -f values.yamlKubernetes/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 itsensitive = 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
liston Secrets, andlistreturns Secret values, not just their names. If the outpost does not need Secret contents, setrbac: {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- SubImage backend initiates sync with outpost-enabled module
- Backend routes traffic through Tailscale proxy to your outpost
- Outpost forwards requests to your internal API endpoint
- 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