Tailscale
Run OrcaCD privately in your tailnet with Tailscale
With Tailscale you can run the OrcaCD Hub without exposing it to the public internet. The Hub is only reachable from devices in your tailnet, and Agents on other machines connect to it over the tailnet as well. Tailscale Serve replaces the reverse proxy: it terminates TLS with a certificate for the MagicDNS name of the machine and forwards the requests to the Hub.
Prerequisites
- A Tailscale account with MagicDNS and HTTPS certificates enabled in the admin console. HTTPS is required, because the login cookie is always marked as
Secure. - The Hub (and optionally an Agent) installed as described in the installation guide.
The MagicDNS name of your machine, e.g. orca.tail1234.ts.net, will be published in the public
Certificate Transparency logs once a certificate is issued for it. The Hub itself stays private.
Tailscale on the Host
The easiest setup is to install Tailscale directly on the machine that runs the Hub. The Hub keeps listening on 127.0.0.1:8080 as in the default docker-compose.yml.
Install Tailscale
Install Tailscale on the host and connect it to your tailnet, see the Tailscale install guide:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --hostname=orcaThe Hub will be reachable at https://orca.<tailnet>.ts.net. You can find the name of your tailnet in the DNS settings of the admin console.
Configure the Hub
Set APP_URL to the MagicDNS name of the machine. It has to match the URL in the browser exactly:
APP_URL=https://orca.tail1234.ts.netTailscale Serve sends the IP address of the client in the X-Forwarded-For header. The Hub only uses this header if the request comes from a trusted proxy. Requests from the host to the published port reach the Hub from the gateway of the Docker network, so add it to TRUSTED_PROXIES:
docker network inspect orca_default --format '{{(index .IPAM.Config 0).Gateway}}'Replace orca_default with the network of your Compose project (docker network ls), and use the output:
TRUSTED_PROXIES=172.18.0.1Recreate the Hub so it picks up the new values:
docker compose up -d hubStart Tailscale Serve
Forward HTTPS traffic from the tailnet to the Hub:
sudo tailscale serve --bg 8080The configuration is persistent and survives restarts. Check it with tailscale serve status. Open https://orca.<tailnet>.ts.net from a device in your tailnet and continue with creating an admin account.
Tailscale as a Sidecar Container
If you don't want to install Tailscale on the host, you can run it as a container next to the Hub. The Hub shares the network of the Tailscale container, so it is only reachable through the tailnet.
Create an auth key in the admin console and add a Tailscale service to your docker-compose.yml:
services:
tailscale:
image: tailscale/tailscale:latest
container_name: orca-tailscale
hostname: orca
restart: unless-stopped
environment:
- TS_AUTHKEY=tskey-auth-...
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
volumes:
- ./data/tailscale:/var/lib/tailscale
- ./tailscale:/config
hub:
image: ghcr.io/orcacd/hub:latest
container_name: orca-hub
restart: unless-stopped
env_file: .env
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
network_mode: service:tailscale
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./data/hub:/app/dataCreate tailscale/serve.json to forward HTTPS traffic to the Hub. Tailscale replaces ${TS_CERT_DOMAIN} with the MagicDNS name of the container:
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:8080"
}
}
}
}
}In this setup Tailscale Serve connects to the Hub via 127.0.0.1, so set the .env values accordingly:
APP_URL=https://orca.tail1234.ts.net
TRUSTED_PROXIES=127.0.0.1If the Agent runs in the same Compose project, it can no longer reach the Hub as hub, because the Hub now uses the network of the Tailscale container. Set HUB_URL=http://tailscale:8080 in its agent.env instead. The connection between Hub and Agent is end-to-end encrypted, even without TLS.
Connecting Agents
An Agent on the same machine as the Hub doesn't need Tailscale. It can connect to the Hub through the Docker network, e.g. with HUB_URL=http://hub:8080.
For Agents on other machines, install Tailscale on these machines as well and set HUB_URL to the MagicDNS name of the Hub:
HUB_URL=https://orca.tail1234.ts.netContainers can reach the tailnet through Tailscale on the host, but they don't always use MagicDNS to resolve names. If the Agent can't resolve the name of the Hub, add it with its Tailscale IP (tailscale ip -4 on the Hub machine) to the Agent service:
services:
agent:
extra_hosts:
- "orca.tail1234.ts.net:100.101.102.103"Restricting Access
Everyone who can reach the Hub in your tailnet can open the login page. Use the access control policy of your tailnet to restrict access to the Hub to your admins and Agents, for example with tags:
{
"tagOwners": {
"tag:orca-hub": ["autogroup:admin"],
"tag:orca-agent": ["autogroup:admin"]
},
"grants": [
{
"src": ["autogroup:admin", "tag:orca-agent"],
"dst": ["tag:orca-hub"],
"ip": ["tcp:443"]
}
]
}Additionally, you can restrict the Hub to Tailscale IP addresses with ALLOWED_IPS:
ALLOWED_IPS=100.64.0.0/10,fd7a:115c:a1e0::/48Webhooks and GitHub Actions
Git providers, container registries and GitHub Actions can't reach a Hub that is only available in your tailnet. You can either rely on polling, or make the Hub publicly reachable with Tailscale Funnel instead of Tailscale Serve:
sudo tailscale funnel --bg 8080For the sidecar container, enable Funnel in tailscale/serve.json instead:
{
"AllowFunnel": {
"${TS_CERT_DOMAIN}:443": true
},
"TCP": { ... },
"Web": { ... }
}With Funnel the whole Hub is public. Restrict the UI and API to your tailnet with ALLOWED_IPS, as shown above. ALLOWED_IPS does not apply to webhooks, GitHub Actions, the health check and Agent connections, so they keep working from the internet. Tailscale passes the real IP of public clients to the Hub, so they are blocked by ALLOWED_IPS and rate limiting works as expected.
Only use Funnel together with ALLOWED_IPS and follow the other recommendations on the security
page, as the Hub is exposed to the internet.
Services in your Tailnet
The Hub blocks requests to private IP addresses, including the Tailscale range 100.64.0.0/10, to prevent SSRF attacks. If your Git server, OIDC provider or notification service is only reachable in your tailnet, allow its Tailscale IP with ALLOWED_INTERNAL_IPS:
ALLOWED_INTERNAL_IPS=100.101.102.104Allow single IPs rather than the whole tailnet range.
This requires that the Hub can reach the tailnet. With Tailscale on the host this works out of the box. The sidecar container uses userspace networking by default, which only handles incoming connections. To make outgoing connections work, switch it to kernel networking:
services:
tailscale:
environment:
- TS_USERSPACE=false
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_adminLast updated on