├── LICENSE ├── README.md ├── assemble-acl ├── .github │ ├── CODEOWNERS │ └── workflows │ │ └── tailscale.yml ├── README.md ├── acls.policy ├── autoapprovers.policy ├── derpmap.policy ├── nodeattrs.policy ├── process │ └── assemble.sh ├── ssh.policy └── tagowners.policy ├── delete-old-nodes ├── README.md └── delete-old-nodes.sh ├── make-auth-key ├── README.md └── makeauthkey.sh ├── remove-suspsended-users └── removeSuspendedUsers.sh ├── temp-grant ├── ACL-entries.txt ├── README.md └── temp-grant.sh └── traveling ├── README.md └── set-traveling.sh /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 | # examples-api-scripts 2 | Minimally functional demonstration scripts for API actions. Meant to be a starting point to develop your own workflows. It will be up to you to draw the rest of the owl. 3 | 4 | Most of these have been put together to demonstrate the usage of an API endpoint. They do not have validation or error handling. 5 | -------------------------------------------------------------------------------- /assemble-acl/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | *.policy @JayWstapleton 2 | derpmap.policy @clstokes 3 | tagowners.policy @JayStaplet -------------------------------------------------------------------------------- /assemble-acl/.github/workflows/tailscale.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: ["main"] 4 | pull_request: 5 | branches: ["main"] 6 | 7 | jobs: 8 | acls: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | # https://tailscale.com/kb/1215/oauth-clients/#generating-long-lived-auth-keys 15 | # https://www.aaron-powell.com/posts/2022-07-14-working-with-add-mask-and-github-actions/ 16 | - name: Generate API Token from OAuth App 17 | run: | 18 | TS_API_TOKEN=$(curl -d "client_id=${{ secrets.TS_OAUTH_CLIENT_ID }}" -d "client_secret=${{ secrets.TS_OAUTH_CLIENT_SECRET }}" \ 19 | "https://api.tailscale.com/api/v2/oauth/token" | jq -r '.access_token') 20 | echo "::add-mask::$TS_API_TOKEN" 21 | echo TS_API_TOKEN=$TS_API_TOKEN >> $GITHUB_ENV 22 | 23 | - name: Concatenate Policy Files 24 | run: bash process/assemble.sh 25 | 26 | - name: Deploy ACL 27 | if: github.event_name == 'push' 28 | id: deploy-acl 29 | uses: tailscale/gitops-acl-action@v1 30 | with: 31 | api-key: ${{ env.TS_API_TOKEN }} 32 | tailnet: ${{ secrets.TS_TAILNET }} 33 | action: apply 34 | policy-file: policy.hujson 35 | 36 | - name: Test ACL 37 | if: github.event_name == 'pull_request' 38 | id: test-acl 39 | uses: tailscale/gitops-acl-action@v1 40 | with: 41 | api-key: ${{ env.TS_API_TOKEN}} 42 | tailnet: ${{ secrets.TS_TAILNET }} 43 | action: test 44 | policy-file: policy.hujson 45 | -------------------------------------------------------------------------------- /assemble-acl/README.md: -------------------------------------------------------------------------------- 1 | Example setup for splitting an ACL file up into sections, then reassembling it with a GitHub Action. 2 | Each .policy file is a segment of the ACL. The script checks trailing commas, then cats the files together before posting via the API. Optionally, you may use CODEOWNERS to require specific approvers for changes to that file. 3 | -------------------------------------------------------------------------------- /assemble-acl/acls.policy: -------------------------------------------------------------------------------- 1 | "acls": [ 2 | // Allow all connections. 3 | {"action": "accept", "src": ["*"], "dst": ["*:*"]}, 4 | { 5 | "action": "accept", 6 | "src": ["*"], 7 | "dst": ["*:22"], 8 | }, 9 | ], 10 | -------------------------------------------------------------------------------- /assemble-acl/autoapprovers.policy: -------------------------------------------------------------------------------- 1 | "autoApprovers": { 2 | "routes": { 3 | "0.0.0.0/0": ["jay-macbook@passkey"], 4 | }, 5 | "exitNode": ["tag:servers"], 6 | }, 7 | -------------------------------------------------------------------------------- /assemble-acl/derpmap.policy: -------------------------------------------------------------------------------- 1 | "derpMap": { 2 | "Regions": 3 | { 4 | // "1": null , 5 | // "2": null, 6 | // "3": null, 7 | // "4": null, 8 | // "5": null, 9 | // "6": null, 10 | // "7": null, 11 | // "8": null, 12 | // "9": null, 13 | // "10": null, 14 | // "11": null, 15 | // "12": null, 16 | "13": null, 17 | // "14": null, 18 | // "15": null, 19 | // "16": null, 20 | // "17": null, 21 | // "18": null, 22 | // "19": null, 23 | // "20": null, 24 | // "21": null, 25 | // "22": null, 26 | // "23": null, 27 | // "24": null, 28 | // "25": null, 29 | // "26": null, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /assemble-acl/nodeattrs.policy: -------------------------------------------------------------------------------- 1 | "nodeAttrs": [ 2 | { 3 | "target": ["*"], 4 | "attr": ["mullvad"], 5 | }, 6 | { 7 | "target": ["*"], 8 | "attr": ["funnel"], 9 | }, 10 | ], 11 | -------------------------------------------------------------------------------- /assemble-acl/process/assemble.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | url="https://github.com/JayWStapleton/pedxing.org" 4 | policyfile="policy.hujson" 5 | 6 | header="// This tailnet's ACLs are maintained in $url" 7 | 8 | echo $header > $policyfile 9 | printf "\n{\n" >> $policyfile 10 | 11 | for policy in ls *.policy; 12 | do 13 | cat $policy 2>/dev/null | sed 's/},/}/g;s/}/},/g' >> $policyfile 14 | done 15 | echo "}" >> $policyfile -------------------------------------------------------------------------------- /assemble-acl/ssh.policy: -------------------------------------------------------------------------------- 1 | "ssh": [ 2 | // Allow all users to SSH into their own devices in check mode. 3 | { 4 | "action": "accept", 5 | "src": ["autogroup:members", "tag:servers"], 6 | "dst": ["tag:servers"], 7 | "users": ["autogroup:nonroot", "root"], 8 | }, 9 | { 10 | "action": "accept", 11 | "src": ["autogroup:members"], 12 | "dst": ["autogroup:self"], 13 | "users": ["autogroup:nonroot", "root"], 14 | }, 15 | ], 16 | -------------------------------------------------------------------------------- /assemble-acl/tagowners.policy: -------------------------------------------------------------------------------- 1 | "tagOwners": { 2 | "tag:servers": ["autogroup:admin"], 3 | "tag:can-funnel": ["autogroup:admin"], 4 | "tag:nothing": ["autogroup:admin"], 5 | "tag:working": ["autogroup:admin"], 6 | }, 7 | -------------------------------------------------------------------------------- /delete-old-nodes/README.md: -------------------------------------------------------------------------------- 1 | #Delete Old Nodes 2 | This will clean out nodes which have not been seen longer than the value of `$oldenough` - I've set this to one month. 3 | 4 | I keep my api key in ~/keys/tailnetname.api.key - if you have them elsewhere, you should change the `$apikey` value. 5 | 6 | And set `$tailnet` to the domain of your tailnet. 7 | 8 | This will do a dry-run as is, and print the ID of the node it will delete. Uncomment the DELETE command to execute. 9 | -------------------------------------------------------------------------------- /delete-old-nodes/delete-old-nodes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tailnet="tailnetname.com" 4 | apikey=$(<$HOME/keys/${tailnet}.api.key) 5 | oldenough=$(date -I --date="1 month ago") 6 | 7 | curl -s "https://api.tailscale.com/api/v2/tailnet/$tailnet/devices" -u "$apikey:" |jq -r '.devices[] | "\(.lastSeen) \(.id)"' | 8 | while read seen id; do 9 | if [[ $seen < $oldenough ]] 10 | then 11 | echo $id " was last seen " $seen " getting rid of it" 12 | ### Uncomment below line to execute script. Defaults to dry run. 13 | #curl -s -X DELETE "https://api.tailscale.com/api/v2/device/$id" -u "$apikey:" 14 | else 15 | echo $id " was last seen " $seen " keeping it" 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /make-auth-key/README.md: -------------------------------------------------------------------------------- 1 | ### Make an auth key from an OAuth client 2 | 3 | Set the variables at the top of the script to your environment. 4 | 5 | This will take an OAuth client ID and Secret, and output a new auth key in a JSON object via a pair of API calls. -------------------------------------------------------------------------------- /make-auth-key/makeauthkey.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tailnet="tsjustworks.net" 4 | #read oauth client from files 5 | oauthsecret=$(<$HOME/keys/${tailnet}.oauth.secret) 6 | oauthid=$(<$HOME/keys/${tailnet}.oauth.id) 7 | #tag needs to be supported by the oauth client 8 | tag="tag:nothing" 9 | 10 | #generate api key from oauth client 11 | apikey=$(curl -sd "client_id=$oauthid" -d "client_secret=$oauthsecret" \ 12 | "https://api.tailscale.com/api/v2/oauth/token" |jq -j '.access_token' ) 13 | 14 | #generate auth key with api key 15 | authkey=$(curl -su $apikey: "https://api.tailscale.com/api/v2/tailnet/$tailnet/keys" \ 16 | --data-binary ' 17 | { 18 | "capabilities": { 19 | "devices": { 20 | "create": { 21 | "reusable": false, 22 | "ephemeral": false, 23 | "preauthorized": false, 24 | "tags": [ "'$tag'" ] 25 | } 26 | } 27 | }, 28 | "expirySeconds": 86400, 29 | "description": "test" 30 | }' 31 | ) 32 | 33 | echo $authkey |jq -------------------------------------------------------------------------------- /remove-suspsended-users/removeSuspendedUsers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apikey=$(cat ~/keys/api.key) 4 | tailnet="tsjustworks.net" 5 | targetstatus="suspended" 6 | 7 | allusers=$(curl -s "https://api.tailscale.com/api/v2/tailnet/-/users" -u "$apikey:" || (echo "API call failed" >&2; exit 1)) 8 | 9 | victims=$(echo $allusers | jq -r '.users[] |select(.status == "'$targetstatus'")') 10 | victimids=$(echo $allusers | jq -r '.users[] |select(.status == "'$targetstatus'") | .id') 11 | v_array=($(echo "$victimids"|xargs)) 12 | if [ ${#v_array[@]} == 0 ] 13 | then 14 | echo "No users found in state "$targetstatus", aborting" 15 | exit 1 16 | fi 17 | 18 | echo "These users are scheduled for removal. Confirm before proceding" 19 | 20 | echo $victims |jq '"User ID: " + .loginName + " status: " + .status' 21 | 22 | read -p "Press [Enter] key to delete all users in the above list" 23 | 24 | for item in "${v_array[@]}"; do 25 | echo "Deleting "$item 26 | #Commented out the scary part. Confirm the dry run before removing the octothorpe below 27 | #curl -s --request POST --url https://api.tailscale.com/api/v2/users/$item/delete -u "$apikey:" || (echo "API call failed" >&2; exit 1) 28 | done 29 | -------------------------------------------------------------------------------- /temp-grant/ACL-entries.txt: -------------------------------------------------------------------------------- 1 | "postures": { 2 | "posture:canaccess": [ 3 | "custom:prod == true", 4 | ], 5 | }, 6 | 7 | 8 | { 9 | "action": "accept", 10 | "src": ["autogroup:members"], 11 | "dst": ["tag:protected:*"], 12 | // Only permit access to any resource which is tagged "protected" while the canaccess posture is true. 13 | "srcPosture": ["posture:canaccess"], 14 | }, -------------------------------------------------------------------------------- /temp-grant/README.md: -------------------------------------------------------------------------------- 1 | ***Note - This is depreciated*** 2 | There is now an expiry available for custom postures, check the Just In Time docs 3 | https://tailscale.com/kb/1443/just-in-time-access 4 | 5 | These are the ingredients for a simple mechanism to grant temporary access based on a manual change to a custom posture attribute 6 | -------------------------------------------------------------------------------- /temp-grant/temp-grant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tailnet="tsjustworks.net" 4 | #keys are stored in a text file under ~/keys 5 | apikey=$(<$HOME/keys/${tailnet}.api.key) 6 | machine=$1 7 | duration=$2 8 | #make it minutes (Defaults to seconds) 9 | increment="m" 10 | 11 | #get the nodeid from the machine name supplied on the command line 12 | nodeid=$(curl -s "https://api.tailscale.com/api/v2/tailnet/$tailnet/devices" -u "$apikey:" | jq -r --arg hostname $machine '.devices[] | select(.hostname==$hostname) .id') 13 | 14 | #set the custom attribute 15 | curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes/custom:prod" -u "$apikey:" --data-binary '{"value": true}' -o /dev/null 16 | echo "Set prod to true for $duration minutes for $machine - $nodeid" 17 | 18 | sleep ${duration}m 19 | 20 | #unset the custom attribute 21 | echo "Removing access from $machine after $duration minutes" 22 | curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes/custom:prod" -u "$apikey:" --data-binary '{"value": false}' -o /dev/null 23 | -------------------------------------------------------------------------------- /traveling/README.md: -------------------------------------------------------------------------------- 1 | Using device posture to manage access for traveling employees. 2 | 3 | Device posture is a powerful tool to control Tailscale permissions and access to devices on your network. They extend our ACL capabilities and allow admins to use fine-grained controls that can be based on some properties on the device, or custom attributes which can be set through automation tools. 4 | 5 | Postures can be changed independently via the API per device, without having to change the ACL. They are ideal for modifying access temporarily or in rapidly changing scenarios. 6 | 7 | In this scenario, we want to restrict access to internal resources while an employee is traveling, while still allowing them to secure their internet access with exit nodes or Mullvad exit nodes. 8 | 9 | This could be used in conjunction with MDM policies which force exit nodes and force Tailscale to stay connected. 10 | 11 | This script can toggle the “traveling” custom attribute: 12 | 13 | ``` 14 | #!/bin/bash 15 | # example run: `./set-traveling.sh jays-macbook-air false` to disable traveling mode, 16 | # or ./set-traveling.sh jays-macbook-air true 17 | 18 | # change to your tailnet name 19 | tailnet="tsjustworks.net" 20 | 21 | # I store my api keys in ~/jay/keys/tsjustworks.net.api.key format 22 | apikey=$(<$HOME/keys/${tailnet}.api.key) 23 | 24 | machine=$1 25 | status=$2 26 | 27 | #get the nodeid from the machine name 28 | nodeid=$(curl -s "https://api.tailscale.com/api/v2/tailnet/$tailnet/devices" -u "$apikey:" | jq -r --arg hostname $machine '.devices[] | select(.hostname==$hostname) .id') 29 | 30 | #set the custom posture attribute 31 | curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes/custom:travelling" -u "$apikey:" --data-binary '{"value": "'$status'"}' -o /dev/null 32 | 33 | output=$(curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes" -u "$apikey:") 34 | 35 | echo $output |jq 36 | ``` 37 | 38 | And this posture would require that custom:traveling be set to false 39 | 40 | ``` 41 | "postures": { 42 | "posture:not-traveling": [ 43 | "custom:traveling == false", 44 | ], 45 | }, 46 | ``` 47 | 48 | If you want this to be applied to all connections, without modifying the ACL policies, you can use the defaultSrcPosture section: 49 | 50 | 51 | ``` 52 | "defaultSrcPosture": [ 53 | “posture:not-traveling” 54 | ], 55 | ``` 56 | 57 | 58 | Otherwise, if there are some resources you still want travelers to use, you can reference the posture directly in the ACLs as such: 59 | 60 | ``` 61 | "acls": [ 62 | { 63 | // Machines only have access to dev resources when not traveling 64 | "action": "accept", 65 | "src": ["autogroup:member"], 66 | "dst": ["tag:development"], 67 | "srcPosture": ["posture:not-traveling"] 68 | }, 69 | { 70 | // People should be able to submit a help desk ticket even when traveling 71 | "action": "accept", 72 | "src": ["autogroup:member"], 73 | "dst": ["tag:ticketing"], 74 | }, 75 | { 76 | // Anyone can access exit nodes whether or not they’re traveling 77 | "action": "accept", 78 | "src": ["autogroup:member"], 79 | "dst": ["autogroup:internet"], 80 | }, 81 | ], 82 | ``` -------------------------------------------------------------------------------- /traveling/set-traveling.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # example run: `./set-traveling.sh jays-macbook-air false` to disable traveling mode, 3 | # or ./set-traveling.sh jays-macbook-air true 4 | 5 | # change to your tailnet name 6 | tailnet="tsjustworks.net" 7 | 8 | # I store my api keys in ~/keys/tsjustworks.net.api.key format 9 | apikey=$(<$HOME/keys/${tailnet}.api.key) 10 | 11 | machine=$1 12 | status=$2 13 | 14 | #get the nodeid from the machine name 15 | nodeid=$(curl -s "https://api.tailscale.com/api/v2/tailnet/$tailnet/devices" -u "$apikey:" | jq -r --arg hostname $machine '.devices[] | select(.hostname==$hostname) .id') 16 | 17 | #set the custom posture attribute 18 | curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes/custom:travelling" -u "$apikey:" --data-binary '{"value": "'$status'"}' -o /dev/null 19 | 20 | output=$(curl -s "https://api.tailscale.com/api/v2/device/$nodeid/attributes" -u "$apikey:") 21 | 22 | echo $output |jq --------------------------------------------------------------------------------