├── .github └── workflows │ └── tailscale-just-in-time.yaml.example ├── LICENSE └── README.md /.github/workflows/tailscale-just-in-time.yaml.example: -------------------------------------------------------------------------------- 1 | name: 'Tailscale: Just-in-time Access' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | source-device: 7 | description: FQDN of the source device (e.g. cameron.tail0123456.ts.net) 8 | required: true 9 | type: string 10 | posture: 11 | description: Which posture? 12 | required: true 13 | type: choice 14 | options: 15 | - custom:prodAcccess=true 16 | amount-of-time: 17 | description: For how long? 18 | required: true 19 | type: choice 20 | options: 21 | - '1 hour' 22 | - '12 hours' 23 | - '1 day' 24 | reason: 25 | description: Reason for access 26 | required: true 27 | type: string 28 | 29 | jobs: 30 | request: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: 'Tailscale: Details of Request' 35 | run: | 36 | echo '####################################################################' 37 | echo '# Source device: ${{ github.event.inputs.source-device }}' 38 | echo '# Posture: ${{ github.event.inputs.posture }}' 39 | echo '# Amount of time: ${{ github.event.inputs.amount-of-time }}' 40 | echo '# Reason: ${{ github.event.inputs.reason }}' 41 | echo '####################################################################' 42 | 43 | approve: 44 | runs-on: ubuntu-latest 45 | environment: tailscale-prod 46 | needs: [request] 47 | 48 | steps: 49 | - name: 'Tailscale: Get access token from OAuth Client' 50 | run: | 51 | TAILSCALE_ACCESS_TOKEN=$(curl --silent --fail --show-error --data 'client_id=${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}' --data 'client_secret=${{ secrets.TAILSCALE_OAUTH_CLIENT_SECRET }}' \ 52 | https://api.tailscale.com/api/v2/oauth/token \ 53 | | jq -r '.access_token') 54 | echo "::add-mask::$TAILSCALE_ACCESS_TOKEN" 55 | echo "TAILSCALE_ACCESS_TOKEN=$TAILSCALE_ACCESS_TOKEN" >> $GITHUB_ENV 56 | - name: 'Tailscale: Find device ID from FQDN' 57 | run: | 58 | TAILSCALE_NODE_ID=$(curl --silent --fail --show-error --header "Authorization: Bearer $TAILSCALE_ACCESS_TOKEN" \ 59 | https://api.tailscale.com/api/v2/tailnet/-/devices \ 60 | | jq -r '.devices[] | select(.name == "${{ github.event.inputs.source-device }}") | .nodeId') 61 | [[ -z $TAILSCALE_NODE_ID ]] && echo "No device id found for hostname [${{ github.event.inputs.source-device }}]" && exit 1 62 | echo "::notice::Found device id [${TAILSCALE_NODE_ID}] for hostname [${{ github.event.inputs.source-device }}]" 63 | echo "TAILSCALE_NODE_ID=${TAILSCALE_NODE_ID}" \ 64 | >> $GITHUB_ENV 65 | - name: 'Tailscale: Calculate expiry' 66 | run: | 67 | TAILSCALE_DEVICE_ATTRIBUTE_EXPIRY=$(date -u -d '${{ github.event.inputs.amount-of-time }}' +'%Y-%m-%dT%H:%M:%SZ') 68 | echo "::notice::Setting attribute expiry to [${TAILSCALE_DEVICE_ATTRIBUTE_EXPIRY}] - [${{ github.event.inputs.amount-of-time }}] from now" 69 | echo "TAILSCALE_DEVICE_ATTRIBUTE_EXPIRY=${TAILSCALE_DEVICE_ATTRIBUTE_EXPIRY}" \ 70 | >> $GITHUB_ENV 71 | - name: 'Tailscale: Set device attribute' 72 | run: | 73 | TAILSCALE_DEVICE_ATTRIBUTE_KEY=$(echo "${{ github.event.inputs.posture }}" | cut -d'=' -f1) 74 | TAILSCALE_DEVICE_ATTRIBUTE_VALUE=$(echo "${{ github.event.inputs.posture }}" | cut -d'=' -f2) 75 | curl --silent --fail --show-error --header "Authorization: Bearer $TAILSCALE_ACCESS_TOKEN" \ 76 | --header 'Content-Type: application/json' \ 77 | --data "{ \"value\": \"${TAILSCALE_DEVICE_ATTRIBUTE_VALUE}\", \"expiry\": \"${TAILSCALE_DEVICE_ATTRIBUTE_EXPIRY}\" }" \ 78 | "https://api.tailscale.com/api/v2/device/${TAILSCALE_NODE_ID}/attributes/${TAILSCALE_DEVICE_ATTRIBUTE_KEY}" 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Tailscale Community 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-action-just-in-time-access 2 | 3 | [![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental) 4 | 5 | A GitHub Action allowing Tailscale users to request and approve just-in-time access to resources on your tailnet. The action uses the [`workflow_dispatch` event to manually run a workflow](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow) and the [Posture attributes API with Expiry](https://tailscale.com/kb/1383/tailscale-slack-accessbot#posture-attributes-api-with-expiry) to update [device attributes used as part of network policy](https://tailscale.com/kb/1383/tailscale-slack-accessbot#use-the-attributes-as-part-of-network-policy). 6 | 7 | > :information_source: This functionality is in its early days and requires a feature flag be 8 | > enabled on your account before you can make use of it. Please contact us if 9 | > you'd like to test it - we're eager to hear your feedback. 10 | 11 | ## Set up 12 | 13 | 1. Copy [.github/workflows/tailscale-just-in-time.yaml.example](.github/workflows/tailscale-just-in-time.yaml.example) to your GitHub repo. Remove the `.example` suffix from the filename. 14 | 1. Customize the `inputs` in [.github/workflows/tailscale-just-in-time.yaml](.github/workflows/tailscale-just-in-time.yaml). 15 | 1. Commit your customized `tailscale-just-in-time.yaml` to your repo and push to GitHub. 16 | 1. [Create a **GitHub environment**](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#creating-an-environment). 17 | 1. Name the environment `tailscale-prod`, or a different value if you've changed it in the workflow file. 18 | 1. Set [**Required reviewers**](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#required-reviewers) to individuals or a team required to approve the request. 19 | 1. [Create a Tailscale OAuth Client](https://tailscale.com/kb/1215/oauth-clients) and add the following **Environment secrets** to the GitHub environment: 20 | 21 | ```shell 22 | TAILSCALE_OAUTH_CLIENT_ID 23 | TAILSCALE_OAUTH_CLIENT_SECRET 24 | ``` 25 | 26 | 1. [Manually run the workflow](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/manually-running-a-workflow). 27 | 28 | ## Local testing 29 | 30 | Test locally using . Note: `act` will move directly from `request` to `approve` without waiting for manual approval. 31 | 32 | ```shell 33 | act workflow_dispatch \ 34 | -s TAILSCALE_OAUTH_CLIENT_ID -s TAILSCALE_OAUTH_CLIENT_SECRET \ 35 | --input source-device='cameron.tail0123456.ts.net' \ 36 | --input posture='custom:prodAcccess=true' \ 37 | --input amount-of-time='12 hours' \ 38 | --input reason='testing locally with act' 39 | ``` 40 | --------------------------------------------------------------------------------