Secure, temporary access to Tailscale resources using GitHub Actions.
Grant just-in-time access to your Tailscale-protected infrastructure with GitHub's built-in security and approvals. No persistent permissions needed.
- Configure Tailscale ACLs with posture-based access rules (see example)
- Fork this GitHub repository or copy the workflow files into your own repository
- Configure secrets for Tailscale API access
- Trigger access requests via GitHub Actions
{
// Define postures that check for custom attributes set by GitHub Actions workflows
"postures": {
"posture:jit_ssh_granted": ["custom:ssh_jit_granted == true"],
"posture:jit_arr_granted": ["custom:arr_jit_granted == true"],
"posture:jit_monitoring_granted": ["custom:monitoring_jit_granted == true"],
"posture:jit_admin_granted": ["custom:admin_jit_granted == true"]
},
"grants": [
{
// example of using posture-based grant for SSH access - can be applied to any service by defining appropriate postures and grants
"src": ["autogroup:member"],
"srcPosture": ["posture:jit_ssh_granted"],
"dst": ["tag:production-server"],
"ip": ["tcp:22"]
},
{
// example of using the same pattern for access to a web service, like the arr suite - can be applied to any service by defining appropriate postures and grants
"src": ["autogroup:member"],
"srcPosture": ["posture:jit_arr_granted"],
"dst": ["tag:arr-server"],
"ip": ["tcp:80", "tcp:443", "tcp:6767"]
},
{
// example of using the same pattern for monitoring access - can be applied to any service by defining appropriate postures and grants
"src": ["autogroup:member"],
"srcPosture": ["posture:jit_monitoring_granted"],
"dst": ["tag:monitoring-server"],
"ip": ["tcp:9090"]
},
{
// example of full access grant - use with caution!
"src": ["autogroup:member"],
"srcPosture": ["posture:jit_admin_granted"],
"dst": ["tag:monitoring-server", "tag:production-server", "tag:arr-server"],
"ip": ["*"]
}
]
}- GitHub-Native Security: Leverage GitHub's authentication, secrets, and approval workflows
- Temporary Access: Grant time-limited access with automatic expiration
- Device-Level Control: Target specific devices by hostname
- Bulk Operations: Expire access across all devices when needed
- Mobile-Friendly: Request access from GitHub Mobile app
- Telegram Notifications: (Optional) Real-time notifications for access grants and revocations
- Customizable: Base framework that can be extended with approvers and custom logic
- Audit Trail: Full logging through GitHub Actions and Tailscale API
- Posture Definition: Define Tailscale postures that check for custom attributes (e.g.,
custom:ssh_jit_granted,custom:arr_jit_granted,custom:monitoring_jit_granted,custom:admin_jit_granted) - Grants: Use
srcPosturein grants to restrict access to devices with the required attributes - GitHub Actions: Workflows use Tailscale API to set/remove custom attributes on devices
- JIT Access: When attribute is present, posture condition is met, granting access
- Access Types: Choose from ssh, arr, monitoring, admin, or all access types. Use "all" to revoke all JIT attributes simultaneously.
| Workflow | File | Description |
|---|---|---|
| JIT Access | .github/workflows/jit.yml | Grants temporary access (ssh, arr, monitoring, or admin) to a specific device by setting the corresponding custom attribute with expiration. |
| Expire JIT Access (Specific Device) | .github/workflows/jit-expire-device.yml | Revokes JIT access (ssh, arr, monitoring, admin, or all) from a specific device by removing the corresponding custom attribute(s). Use "all" to revoke all JIT attributes at once. |
| Expire All JIT Access | .github/workflows/jit-expire-all.yml | Revokes JIT access (ssh, arr, monitoring, admin, or all) from all devices in the tailnet by removing the corresponding custom attribute(s) where present. Use "all" to revoke all JIT attributes at once. |
- GitHub repository with Actions enabled
- Tailscale OAuth client configured
-
Create OAuth Client in Tailscale admin console:
- Go to Settings β OAuth clients
- Create client with scopes:
devices:core:read,devices:posture_attributes
-
Define Postures in your tailnet policy file:
"postures": { "posture:jit_ssh_granted": ["custom:ssh_jit_granted == true"], "posture:jit_arr_granted": ["custom:arr_jit_granted == true"], "posture:jit_monitoring_granted": ["custom:monitoring_jit_granted == true"], "posture:jit_admin_granted": ["custom:admin_jit_granted == true"] }
-
Configure Grants to use posture-based access:
"grants": [ { "src": ["autogroup:member"], "srcPosture": ["posture:jit_ssh_granted"], "dst": ["tag:secure-resource"], "ip": ["tcp:22"] }, { "src": ["autogroup:member"], "srcPosture": ["posture:jit_arr_granted"], "dst": ["tag:arr-server"], "ip": ["tcp:80", "tcp:443", "tcp:6767"] }, { "src": ["autogroup:member"], "srcPosture": ["posture:jit_monitoring_granted"], "dst": ["tag:monitoring-server"], "ip": ["tcp:9090"] }, { "src": ["autogroup:member"], "srcPosture": ["posture:jit_admin_granted"], "dst": ["tag:monitoring-server", "tag:production-server", "tag:arr-server"], "ip": ["*"] } ]
-
Add Repository Secrets:
TS_OAUTH_CLIENT_ID: Your Tailscale OAuth client IDTS_OAUTH_CLIENT_SECRET: Your Tailscale OAuth client secretTELEGRAM_BOT_TOKEN: (Optional) Telegram bot token for notificationsTELEGRAM_CHAT_ID: (Optional) Telegram chat ID for notifications
-
Set up Telegram Notifications (optional - workflows will work without notifications):
- Create a Telegram bot via @BotFather
- Get your bot token from BotFather
- Create a private Telegram channel or group for notifications
- Add the bot as an administrator to the channel/group
- Get the chat ID using methods like sending a message to the channel and checking
https://api.telegram.org/bot<YourBOTToken>/getUpdates - Set
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDas repository secrets - If not configured, workflows will show a warning and continue without notifications
-
Copy Workflows to
.github/workflows/in your repository -
Customize workflows as needed (notification channels, approval requirements, etc.)
- Go to Actions β JIT Access
- Enter device hostname, duration, and access type (ssh, arr, monitoring, or admin)
- Click Run workflow
- Single Device: Use "Expire JIT Access (Specific Device)"
- Specify device hostname and access type (ssh, arr, monitoring, admin, or all)
- Use "all" to revoke all JIT attributes from the device at once
- All Devices: Use "Expire All JIT Access"
- Specify access type (ssh, arr, monitoring, admin, or all)
- Use "all" to revoke all JIT attributes from all devices at once
Test and debug workflows locally using act, which simulates the GitHub Actions environment on your machine.
Use secret-tool-run to load secrets from your system keyring without storing plaintext files on disk.
- Install secret-tool-run (see the secret-tool-run README)
- Run act through secret-tool-run using file descriptor mode
Example:
secret-tool-run ./bin/act workflow_dispatch \
--secret-file @SECRETS@ \
--eventpath event-grant.json \
-j grant-access \
-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latestWhen prompted the first time, paste your secrets in this format:
TS_OAUTH_CLIENT_ID=your_client_id
TS_OAUTH_CLIENT_SECRET=your_client_secret
TELEGRAM_BOT_TOKEN=your_token (optional)
TELEGRAM_CHAT_ID=your_chat_id (optional)
secret-tool-run stores the secrets in your local keyring. You can check stored entries with secret-tool lookup app <appname> and delete them with secret-tool clear app <appname>.
- Install act
- Create a
.secretsfile in your repository root with your secrets:TS_OAUTH_CLIENT_ID=your_client_id TS_OAUTH_CLIENT_SECRET=your_client_secret TELEGRAM_BOT_TOKEN=your_token (optional) TELEGRAM_CHAT_ID=your_chat_id (optional)
Use the event-grant.json file to simulate workflow inputs:
act workflow_dispatch \
--eventpath event-grant.json \
--secret-file .secrets \
-j grant-access \
-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latestParameters:
workflow_dispatch: Trigger type (matches the workflow'son: workflow_dispatch)--eventpath event.json: Path to file containing workflow inputs--secret-file .secrets: Path to file with repository secrets-j grant-access: Job ID to run (find in workflow YAML)-P ubuntu-latest=...: Container image for the runner
TS_OAUTH_CLIENT_ID=your_client_id
TS_OAUTH_CLIENT_SECRET=your_client_secret
TELEGRAM_BOT_TOKEN=your_token (optional)
TELEGRAM_CHAT_ID=your_chat_id (optional)
{
"inputs": {
"source_hostname": "my-server",
"duration_minutes": "30",
"access_type": "ssh"
}
}- Add Approvals: Enable GitHub environment protection rules
- Custom Attributes: Extend for VPN, database, or admin access
- Notifications: Replace Telegram with Slack, Teams, or webhooks
- Validation: Add time restrictions or device health checks
- OAuth with minimal scopes
- Automatic access expiration
- Complete audit trails
- GitHub secrets management
MIT License