Deploying to private VPS servers from GitHub Actions typically requires exposing SSH ports or maintaining complex VPN configurations. Tailscale's zero-trust networking eliminates these security risks by creating ephemeral, encrypted connections between GitHub-hosted runners and your infrastructure.
This guide shows how to connect GitHub Actions to a private VPS using Tailscale's OAuth-based authentication, following the principle of least privilege.
Architecture Overview
When a GitHub Actions workflow runs, the Tailscale GitHub Action installs the Tailscale client on the GitHub runner, creating an ephemeral Tailscale node that joins your tailnet. This ephemeral node receives a tag-based identity (tag:app-ci) that identifies its role as a CI/CD runner. Access to resources is granted based on ACL rules matching this tag. After the workflow completes, the ephemeral node is automatically removed.
The ephemeral node is the Tailscale client running on the GitHub runner. It's tagged with tag:app-ci to identify its role, and ACL rules grant this tag access to your VPS (tag:app-server). All traffic is encrypted via WireGuard, and your VPS never needs public-facing ports.
Tailscale Ephemeral Nodes, WireGuard Encryption
Prerequisites
- Tailscale account with Owner, Admin, or Network admin permissions
- GitHub repository with admin access
- VPS running Tailscale with appropriate tags
- At least one configured tag in your tailnet
Setting up Tailscale on a Server, Using Tags
Step 1: Configure Tailscale OAuth Client
Create an OAuth client in the Tailscale admin console. This is preferred over auth keys because it provides automatic cleanup and better security isolation.
Required OAuth Scopes (principle of least privilege):
Configured in Trust credentials settings in Tailscale admin console
devices:core
devices:core:read
devices:posture_attributes
devices:posture_attributes:read
devices:routes:read
device_invites:read
policy_file:read
dns:read
api_access_tokens:read
auth_keys
oauth_keys
users:read
services
Store the OAuth Client ID and Secret securely—you'll add them to GitHub Secrets in the next step.
Step 2: Configure GitHub Secrets
Add the following secrets to your GitHub repository:
-
TS_OAUTH_CLIENT_ID: Your Tailscale OAuth Client ID -
TS_OAUTH_SECRET: Your Tailscale OAuth Client Secret -
VPS_HOST: Tailscale hostname or IP of your VPS (e.g.,vps-name.tailnet-name.ts.net) -
VPS_USER: SSH user for VPS access
Also, configure a repository variable:
-
TS_CI_RUNNER_TAG: The tag assigned to ephemeral nodes (e.g.,tag:app-ci)
References: GitHub Encrypted Secrets, GitHub Variables
Step 3: GitHub Actions Workflow Configuration
Add the Tailscale GitHub Action to your workflow before any steps that need VPS access:
- name: Setup Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: ${{ vars.TS_CI_RUNNER_TAG }}
Complete Example (deployment workflow):
name: Deploy to VPS
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: ${{ vars.TS_CI_RUNNER_TAG }}
- name: Deploy via Ansible
working-directory: infrastructure/ansible
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
ansible-playbook playbooks/deploy.yml
After the Tailscale step, your workflow can access the VPS using its Tailscale hostname or IP. The ephemeral node is automatically logged out and removed when the workflow completes.
Step 4: Tailscale ACL Configuration
Configure your Tailscale ACL to grant the CI runner tag access to your VPS tag. Here's a minimal configuration:
{
"tagOwners": {
"tag:app-server": ["autogroup:admin"],
"tag:app-ci": ["autogroup:admin"]
},
"grants": [
{
"src": ["tag:app-ci"],
"dst": ["tag:app-server"],
"ip": ["*"]
}
],
"ssh": [
{
"action": "accept",
"src": ["tag:app-ci"],
"dst": ["tag:app-server"],
"users": ["autogroup:nonroot", "root"]
}
]
}
Key Points:
-
tagOwnersdefines who can assign tags (admins in this case) -
grantsallowstag:app-cito reachtag:app-serveron all ports -
sshrules explicitly permit SSH access from CI runners to servers
Ensure your VPS is tagged with tag:app-server in the Tailscale admin console.
How It Works
- Workflow starts: GitHub Action installs Tailscale client on the GitHub runner
-
Ephemeral node created: The Tailscale client authenticates with OAuth and creates an ephemeral node with
tag:app-cithat joins your tailnet -
Tag-based identity: The tag (
tag:app-ci) identifies this node as a CI/CD runner -
ACL enforcement: ACL rules grant the
tag:app-ciaccess totag:app-serverresources - Connection established: The GitHub runner connects to the VPS via the Tailscale network using the VPS's Tailscale hostname or IP
- Automatic cleanup: When the workflow completes, the ephemeral node logs out and is automatically removed from your tailnet
The ephemeral node is the Tailscale client running on the GitHub runner. It's not a separate machine—it's the Tailscale connection that enables the runner to reach your private VPS. Ephemeral nodes are pre-approved on tailnets with device approval enabled, eliminating manual approval steps.
Security Benefits
- No public ports: VPS remains completely private
- Least privilege: OAuth scopes and ACL tags limit access to the minimum required actions
- Ephemeral access: Nodes exist only during workflow execution
- Encrypted traffic: All communication uses WireGuard encryption
- Zero-trust: Every connection is authenticated and authorized
References: Zero Trust Networking
Troubleshooting
If connectivity issues occur, use the ping parameter to verify connectivity:
- name: Setup Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: ${{ vars.TS_CI_RUNNER_TAG }}
ping: vps-name.tailnet-name.ts.net
Alternatively, test connectivity manually by adding the app-ci tag to your personal laptop and check if the tailscale ACL rules are working as expected
tailscale ping vps-name.tailnet-name.ts.net
Conclusion
Tailscale's GitHub Action provides a secure, zero-trust way to connect CI/CD pipelines to private infrastructure. By using OAuth-based authentication and tag-based ACLs, you eliminate the need for public-facing ports while maintaining granular access control.
The ephemeral node pattern ensures that access is temporary and automatically cleaned up, reducing the attack surface compared to persistent VPN connections or exposed SSH ports.

Top comments (0)