├── .ansible-lint ├── .ansible-lint-ignore ├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── molecule.yml │ ├── pull_request.yml │ ├── pull_request_target.yml │ └── release.yml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── defaults └── main.yml ├── docs ├── demo.gif └── images │ ├── invalid_authkey.png │ ├── oauth_scopes.png │ ├── printed_stderr.png │ ├── printed_stdout.png │ └── redacted_authkey.png ├── filter_plugins └── warn.py ├── handlers └── main.yml ├── meta └── main.yml ├── molecule ├── args │ ├── cleanup.yml │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── default │ ├── cleanup.yml │ ├── converge.yml │ ├── headscale.config.yaml │ ├── init_tailscale_vars.yml │ ├── molecule.yml │ ├── prepare.yml │ └── verify.yml ├── idempotent-up │ ├── cleanup.yml │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── oauth │ ├── cleanup.yml │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── reinstall │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── skip-authentication │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── state-absent │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── state-idempotency │ ├── cleanup.yml │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml └── strategy-free │ ├── cleanup.yml │ ├── converge.yml │ ├── molecule.yml │ └── verify.yml ├── poetry.lock ├── pyproject.toml ├── requirements.yml ├── tasks ├── arch │ ├── install.yml │ └── uninstall.yml ├── centos │ ├── install-legacy.yml │ ├── install.yml │ ├── uninstall-legacy.yml │ └── uninstall.yml ├── debian │ ├── apt-codename.yml │ ├── install.yml │ └── uninstall.yml ├── facts.yml ├── fedora │ ├── install.yml │ ├── systemd.yml │ └── uninstall.yml ├── files │ └── state_readme.md ├── install.yml ├── main.yml ├── opensuse │ ├── install.yml │ └── uninstall.yml ├── templates │ └── state.j2 └── uninstall.yml └── vars └── main.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - .cache/ 3 | - .github/ 4 | - .idea/ 5 | - .vscode/ 6 | - molecule/default/headscale.config.yaml 7 | - molecule/default/init_tailscale_vars.yml 8 | # pre-commit ansible-lint isn't respecting .ansible-lint-ignore 9 | - tasks/centos/install-legacy.yml 10 | - tasks/centos/uninstall-legacy.yml 11 | 12 | skip_list: 13 | - yaml[line-length] 14 | - package-latest 15 | - var-naming[no-role-prefix] 16 | -------------------------------------------------------------------------------- /.ansible-lint-ignore: -------------------------------------------------------------------------------- 1 | tasks/centos/install-legacy.yml fqcn[action-core] 2 | tasks/centos/uninstall-legacy.yml fqcn[action-core] 3 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | "features": { 11 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 12 | "ghcr.io/devcontainers/features/git:1": {}, 13 | "ghcr.io/devcontainers/features/github-cli:1": {}, 14 | // "ghcr.io/devcontainers-contrib/features/ansible:2": {}, 15 | "ghcr.io/devcontainers-extra/features/poetry:2": {}, 16 | "ghcr.io/devcontainers-extra/features/pre-commit:2": {}, 17 | "ghcr.io/devcontainers/features/common-utils:2": { 18 | "configureZshAsDefaultShell": true 19 | } 20 | }, 21 | 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | 25 | // Use 'postCreateCommand' to run commands after the container is created. 26 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 27 | "updateContentCommand": "make install", 28 | // Temporary oncreate command since ansible feature is broken 29 | // https://github.com/devcontainers-contrib/features/issues/607 30 | "onCreateCommand": "sudo apt update && sudo apt install -y pipx; pipx install --include-deps ansible", 31 | "postCreateCommand": "sudo apt update && sudo apt full-upgrade -y", 32 | "postStartCommand": "git pull --prune; make install", 33 | 34 | // Configure tool-specific properties. 35 | "customizations": { 36 | "jetbrains": { 37 | "backend": "PyCharm", 38 | "plugins": [ 39 | "name.kropp.intellij.makefile", 40 | "org.jetbrains.plugins.github", 41 | "org.jetbrains.plugins.yaml" 42 | ] 43 | } 44 | }, 45 | 46 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 47 | // "remoteUser": "root" 48 | } 49 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: artis3n 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug:needs-reproduction 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Target (please complete the following information):** 27 | - OS: [e.g. Ubuntu] 28 | - Ansible version: 29 | - `artis3n.tailscale` version: 30 | - Tailscale version (set `verbose` to true): 31 | 32 | Output of `tailscale status` during role execution (set `verbose` to true): 33 | 34 | ```bash 35 | ok: [instance] => { 36 | "tailscale_status": { 37 | ... 38 | } 39 | } 40 | 41 | ``` 42 | 43 | **Additional context** 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | github-actions: 10 | patterns: 11 | - '*' 12 | 13 | - package-ecosystem: "pip" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | groups: 18 | python-production: 19 | dependency-type: production 20 | python-development: 21 | dependency-type: development 22 | 23 | - package-ecosystem: "devcontainers" 24 | directory: "/" 25 | schedule: 26 | interval: "weekly" 27 | groups: 28 | devcontainer-features: 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /.github/workflows/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Molecule Test 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | scenario: 8 | required: true 9 | type: string 10 | image: 11 | required: false 12 | type: string 13 | default: 'geerlingguy/docker-ubuntu2404-ansible:latest' 14 | command: 15 | required: false 16 | type: string 17 | runner: 18 | required: false 19 | type: string 20 | default: ubuntu-24.04 21 | secrets: 22 | tailscale_key: 23 | required: true 24 | 25 | env: 26 | # https://www.jeffgeerling.com/blog/2020/getting-colorized-output-molecule-and-ansible-on-github-actions-ci 27 | PY_COLORS: '1' 28 | ANSIBLE_FORCE_COLOR: '1' 29 | 30 | jobs: 31 | run-scenario: 32 | name: "Run Molecule Scenario - ${{ inputs.image }} / ${{ inputs.scenario }}" 33 | runs-on: ${{ inputs.runner }} 34 | environment: E2E 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: 39 | ref: ${{ github.event.pull_request.head.sha }} 40 | 41 | - name: Install dependency manager 42 | run: pipx install poetry 43 | 44 | - name: Set up Python 3.x 45 | id: setup-python 46 | uses: actions/setup-python@v5 47 | with: 48 | python-version-file: pyproject.toml 49 | cache: 'poetry' 50 | 51 | - name: Install packages 52 | run: poetry install --no-interaction --no-root 53 | 54 | - name: Run scenario 55 | run: poetry run molecule test --scenario-name "${{ inputs.scenario }}" 56 | env: 57 | MOLECULE_DISTRO: "${{ inputs.image }}" 58 | MOLECULE_COMMAND: "${{ inputs.command }}" 59 | TAILSCALE_CI_KEY: "${{ secrets.tailscale_key }}" 60 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | env: 10 | # https://www.jeffgeerling.com/blog/2020/getting-colorized-output-molecule-and-ansible-on-github-actions-ci 11 | PY_COLORS: '1' 12 | ANSIBLE_FORCE_COLOR: '1' 13 | 14 | jobs: 15 | lint: 16 | name: "Lint Checks" 17 | runs-on: ubuntu-24.04 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install dependency manager 23 | run: pipx install poetry 24 | 25 | - name: Set up Python 3.x 26 | id: setup-python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version-file: pyproject.toml 30 | cache: 'poetry' 31 | 32 | - name: Install packages 33 | run: poetry install --no-interaction --no-root 34 | 35 | - name: Lint 36 | run: make lint 37 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_target.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Molecule Tests 3 | 4 | on: 5 | # Manual approval of environment is required 6 | pull_request_target: 7 | branches: 8 | - main 9 | paths-ignore: 10 | - ".devcontainer/**" 11 | - "README.md" 12 | 13 | env: 14 | # https://www.jeffgeerling.com/blog/2020/getting-colorized-output-molecule-and-ansible-on-github-actions-ci 15 | PY_COLORS: '1' 16 | ANSIBLE_FORCE_COLOR: '1' 17 | 18 | jobs: 19 | molecule: 20 | name: "Test Distros" 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - image: ghcr.io/artis3n/docker-amazonlinux2023-ansible:latest 26 | # - image: geerlingguy/docker-rockylinux8-ansible:latest 27 | # Temporary: until https://github.com/rocky-linux/sig-cloud-instance-images/issues/56 is resolved 28 | # - image: geerlingguy/docker-rockylinux9-ansible:latest 29 | # - image: ghcr.io/artis3n/docker-almalinux8-ansible:latest 30 | # - image: ghcr.io/artis3n/docker-rhel8-ansible:latest 31 | # - image: ghcr.io/artis3n/docker-oraclelinux8-ansible:latest 32 | - image: geerlingguy/docker-ubuntu2404-ansible:latest 33 | - image: ghcr.io/fkonradmain/docker-linuxmint22-ansible:latest 34 | - image: geerlingguy/docker-ubuntu2204-ansible:latest 35 | # - image: geerlingguy/docker-ubuntu2004-ansible:latest 36 | # - image: geerlingguy/docker-ubuntu1804-ansible:latest 37 | # command: /lib/systemd/systemd 38 | - image: geerlingguy/docker-debian12-ansible:latest 39 | - image: geerlingguy/docker-debian11-ansible:latest 40 | command: /lib/systemd/systemd 41 | # - image: geerlingguy/docker-debian10-ansible:latest 42 | # command: /lib/systemd/systemd 43 | - image: geerlingguy/docker-fedora36-ansible:latest 44 | - image: ghcr.io/artis3n/docker-arch-ansible:latest 45 | - image: ghcr.io/artis3n/docker-opensuse-tumbleweed-ansible:latest 46 | - image: ghcr.io/artis3n/docker-opensuse-leap-ansible:latest 47 | command: /lib/systemd/systemd 48 | 49 | uses: ./.github/workflows/molecule.yml 50 | with: 51 | image: ${{ matrix.image }} 52 | command: ${{ matrix.command }} 53 | scenario: 'default' 54 | secrets: 55 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 56 | 57 | # Systems that aren't working with cgroups v2 on ubuntu 22.04, but works on ubuntu 20.04 58 | # molecule-legacy: 59 | # name: "Test Legacy Distros" 60 | # strategy: 61 | # fail-fast: false 62 | # matrix: 63 | # include: 64 | # - image: ghcr.io/artis3n/docker-amazonlinux2-ansible:latest 65 | # command: /usr/lib/systemd/systemd 66 | # 67 | # uses: ./.github/workflows/molecule.yml 68 | # with: 69 | # image: ${{ matrix.image }} 70 | # command: ${{ matrix.command }} 71 | # scenario: 'default' 72 | # runner: ubuntu-20.04 73 | # secrets: 74 | # tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 75 | 76 | molecule-state-absent: 77 | name: "Run Molecule Scenario: state-absent" 78 | uses: ./.github/workflows/molecule.yml 79 | with: 80 | scenario: 'state-absent' 81 | secrets: 82 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 83 | 84 | molecule-skip-auth: 85 | name: "Run Molecule Scenario: skip-authentication" 86 | uses: ./.github/workflows/molecule.yml 87 | with: 88 | scenario: 'skip-authentication' 89 | secrets: 90 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 91 | 92 | molecule-args: 93 | name: "Run Molecule Scenario: args" 94 | uses: ./.github/workflows/molecule.yml 95 | with: 96 | scenario: 'args' 97 | secrets: 98 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 99 | 100 | molecule-state-present: 101 | name: "Run Molecule Scenario: state-idempotency" 102 | uses: ./.github/workflows/molecule.yml 103 | with: 104 | scenario: 'state-idempotency' 105 | secrets: 106 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 107 | 108 | molecule-oauth: 109 | name: "Run Molecule Scenario: oauth" 110 | uses: ./.github/workflows/molecule.yml 111 | with: 112 | scenario: 'oauth' 113 | secrets: 114 | tailscale_key: ${{ secrets.TAILSCALE_OAUTH_CLIENT_SECRET }} 115 | 116 | molecule-reinstall: 117 | name: "Run Molecule Scenario: reinstall" 118 | uses: ./.github/workflows/molecule.yml 119 | with: 120 | scenario: "reinstall" 121 | secrets: 122 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 123 | 124 | molecule-strategy-free: 125 | name: "Run Molecule Scenario: strategy-free" 126 | uses: ./.github/workflows/molecule.yml 127 | with: 128 | scenario: 'strategy-free' 129 | secrets: 130 | tailscale_key: ${{ secrets.TAILSCALE_CI_KEY }} 131 | 132 | molecule-headscale: 133 | name: "Test Headscale Compatibility" 134 | runs-on: ubuntu-24.04 135 | environment: E2E 136 | strategy: 137 | fail-fast: false 138 | matrix: 139 | scenario: 140 | - default 141 | - state-absent 142 | - idempotent-up 143 | - strategy-free 144 | 145 | steps: 146 | - uses: actions/checkout@v4 147 | with: 148 | ref: ${{ github.event.pull_request.head.sha }} 149 | 150 | - name: Install dependency manager 151 | run: pipx install poetry 152 | 153 | - name: Set up Python 3.x 154 | id: setup-python 155 | uses: actions/setup-python@v5 156 | with: 157 | python-version-file: pyproject.toml 158 | cache: 'poetry' 159 | 160 | - name: Install packages 161 | run: poetry install --no-interaction 162 | 163 | - name: Run scenario with Headscale 164 | run: poetry run molecule test --scenario-name ${{ matrix.scenario }} 165 | env: 166 | TAILSCALE_CI_KEY: "unused" 167 | USE_HEADSCALE: "true" 168 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Ansible Galaxy 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install dependency manager 18 | run: pipx install poetry 19 | 20 | - name: Set up Python 3.x 21 | id: setup-python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version-file: pyproject.toml 25 | cache: 'poetry' 26 | 27 | - name: Install packages 28 | run: poetry install --no-interaction --no-root 29 | 30 | - name: Publish to Ansible Galaxy 31 | run: | 32 | poetry run ansible-galaxy role import --token ${{ secrets.ANSIBLE_GALAXY_TOKEN }} artis3n tailscale 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: check-executables-have-shebangs 10 | - id: check-merge-conflict 11 | - id: mixed-line-ending 12 | - id: sort-simple-yaml 13 | 14 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 15 | rev: 3.0.0 16 | hooks: 17 | - id: shellcheck 18 | 19 | - repo: https://github.com/ansible/ansible-lint 20 | rev: v25.1.3 21 | hooks: 22 | - id: ansible-lint 23 | files: \.(yaml|yml)$ 24 | args: 25 | - '--profile=production' 26 | 27 | - repo: https://github.com/rhysd/actionlint 28 | rev: v1.7.7 29 | hooks: 30 | - id: actionlint-docker 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Ari Kalfus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env make 2 | 3 | .PHONY: all 4 | all: lint test 5 | 6 | # Install Python first 7 | .PHONY: install 8 | install: 9 | poetry install --with=dev 10 | poetry run pre-commit install --install-hooks 11 | poetry run ansible-galaxy collection install -r requirements.yml 12 | 13 | .PHONY: clean 14 | clean: 15 | poetry env remove 16 | 17 | .PHONY: update 18 | update: 19 | poetry update --with=dev 20 | poetry run pre-commit autoupdate 21 | 22 | .PHONY: lint 23 | lint: 24 | poetry run ansible-lint --profile=production 25 | 26 | .PHONY: test 27 | test: test-default test-absent 28 | 29 | # If local, make sure TAILSCALE_CI_KEY env var is set. 30 | # This is automatically populated in a GitHub Codespace. 31 | .PHONY: test-all 32 | test-all: 33 | ifndef TAILSCALE_CI_KEY 34 | $(error TAILSCALE_CI_KEY is not set) 35 | else 36 | poetry run molecule test --all 37 | endif 38 | 39 | .PHONY: test-default 40 | test-default: 41 | ifndef TAILSCALE_CI_KEY 42 | $(error TAILSCALE_CI_KEY is not set) 43 | else 44 | poetry run molecule test --scenario-name default 45 | endif 46 | 47 | .PHONY: test-idempotent-up 48 | test-idempotent-up: 49 | ifndef TAILSCALE_CI_KEY 50 | $(error TAILSCALE_CI_KEY is not set) 51 | else 52 | poetry run molecule test --scenario-name idempotent-up 53 | endif 54 | 55 | .PHONY: test-args 56 | test-args: 57 | ifndef TAILSCALE_CI_KEY 58 | $(error TAILSCALE_CI_KEY is not set) 59 | else 60 | poetry run molecule test --scenario-name args 61 | endif 62 | 63 | .PHONY: test-absent 64 | test-absent: 65 | ifndef TAILSCALE_CI_KEY 66 | $(error TAILSCALE_CI_KEY is not set) 67 | else 68 | poetry run molecule test --scenario-name state-absent 69 | endif 70 | 71 | .PHONY: test-oauth 72 | test-oauth: 73 | ifndef TAILSCALE_OAUTH_CLIENT_SECRET 74 | $(error TAILSCALE_OAUTH_CLIENT_SECRET is not set) 75 | else 76 | poetry run molecule test --scenario-name oauth 77 | endif 78 | 79 | .PHONY: test-strategy-free 80 | test-strategy-free: 81 | ifndef TAILSCALE_CI_KEY 82 | $(error TAILSCALE_CI_KEY is not set) 83 | else 84 | poetry run molecule test --scenario-name strategy-free 85 | endif 86 | 87 | .PHONY: test-reinstall 88 | test-reinstall: 89 | ifndef TAILSCALE_CI_KEY 90 | $(error TAILSCALE_CI_KEY is not set) 91 | else 92 | poetry run molecule test --scenario-name reinstall 93 | endif 94 | 95 | .PHONY: test-headscale 96 | test-headscale: 97 | USE_HEADSCALE=true poetry run molecule test --scenario-name default 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # artis3n.tailscale 2 | 3 | [![Ansible Role](https://img.shields.io/ansible/role/d/artis3n/tailscale)](https://galaxy.ansible.com/ui/standalone/roles/artis3n/tailscale/) 4 | [![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/artis3n/ansible-role-tailscale?include_prereleases)](https://github.com/artis3n/ansible-role-tailscale/releases) 5 | [![Molecule Tests](https://github.com/artis3n/ansible-role-tailscale/actions/workflows/pull_request_target.yml/badge.svg)](https://github.com/artis3n/ansible-role-tailscale/actions/workflows/pull_request_target.yml) 6 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6312/badge)](https://bestpractices.coreinfrastructure.org/projects/6312) 7 | ![GitHub last commit](https://img.shields.io/github/last-commit/artis3n/ansible-role-tailscale) 8 | ![GitHub](https://img.shields.io/github/license/artis3n/ansible-role-tailscale) 9 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/artis3n)](https://github.com/sponsors/artis3n) 10 | [![GitHub followers](https://img.shields.io/github/followers/artis3n?style=social)](https://github.com/artis3n/) 11 | 12 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/artis3n/ansible-role-tailscale?quickstart=1) 13 | 14 | This role installs and configures [Tailscale][] on a Linux target. 15 | 16 | > [!IMPORTANT] 17 | > **This standalone role has been migrated into a collection ().** 18 | > 19 | > This role will continue to function but future development work will focus on the collection. 20 | > Please try out the collection and provide feedback on the new repo. 21 | > 22 | > `ansible-galaxy collection install artis3n.tailscale` 23 | 24 | Supported operating systems: 25 | - Debian / Ubuntu 26 | - CentOS / RedHat 27 | - Rocky Linux / AlmaLinux 28 | - Amazon Linux 2023 / Amazon Linux 2 29 | - Fedora 30 | - Arch Linux 31 | - OpenSUSE 32 | - Oracle Linux 33 | - Raspbian 34 | 35 | See the [CI worfklow](https://github.com/artis3n/ansible-role-tailscale/blob/main/.github/workflows/pull_request_target.yml) for the list of distribution versions actively tested in each pull request. 36 | 37 |
38 | 39 |
40 | 41 | > [!TIP] 42 | > This role uses Ansible fully qualified collection names (FQCN) and therefore requires Ansible 2.11+. 43 | > Ansible 2.12 is set as the minimum required version as this was the version tested for compatibility during the FQCN refactor. 44 | 45 | If you or your organization gets value out of this role, I would very much appreciate one-time or recurring [sponsorship](https://github.com/sponsors/artis3n?sponsor=artis3n) of this role. 46 | 47 | - [Role Outputs](#role-outputs) 48 | - [Role Variables](#role-variables) 49 | - [Required](#required) 50 | - [tailscale\_authkey](#tailscale_authkey) 51 | - [tailscale\_tags](#tailscale_tags) 52 | - [tailscale\_up\_skip](#tailscale_up_skip) 53 | - [Optional](#optional) 54 | - [state](#state) 55 | - [tailscale\_args](#tailscale_args) 56 | - [tailscale\_oauth\_ephemeral](#tailscale_oauth_ephemeral) 57 | - [tailscale\_oauth\_preauthorized](#tailscale_oauth_preauthorized) 58 | - [insecurely\_log\_authkey](#insecurely_log_authkey) 59 | - [release\_stability](#release_stability) 60 | - [tailscale\_up\_timeout](#tailscale_up_timeout) 61 | - [verbose](#verbose) 62 | - [Dependencies](#dependencies) 63 | - [Collections](#collections) 64 | - [Example Playbook](#example-playbook) 65 | - [State Tracking](#state-tracking) 66 | - [License](#license) 67 | - [Author Information](#author-information) 68 | - [Development and Contributing](#development-and-contributing) 69 | 70 | 71 | This role will bubble up any stderr messages from the Tailscale binary 72 | to resolve any end-user configuration errors with `tailscale up` arguments. 73 | The `--authkey=` value will be redacted unless [`insecurely_log_authkey`](#insecurely_log_authkey) is set to `true`. 74 | 75 | ![logged stderr](docs/images/printed_stderr.png) 76 | 77 | # Role Outputs 78 | 79 | This role provides the IP v4 and v6 addresses of the Tailscale node as well as the output of `tailscale whois` against the node as facts. 80 | Several key pieces of `whois` information are provided directly, with the rest of the whois output stored as a JSON fact for your convenience. 81 | 82 | Outputted facts: 83 | 84 | ``` 85 | tailscale_node_ipv4 (string): The IPv4 address of the Tailscale node. 86 | tailscale_node_ipv6 (string): The IPv6 address of the Tailscale node. 87 | tailscale_node_hostname_full (string): The full hostname (node.domain.ts.net) of the Tailscale node. 88 | tailscale_node_hostname_short (string): The short hostname (node) of the Tailscale node. 89 | tailscale_node_created_at (string): The ISO-8601 timestamp the Tailscale node was created. 90 | tailscale_node_tags (list): The tags assigned to the Tailscale node. 91 | tailscale_node_services (list): The discovered services running on the Tailscale node. 92 | tailscale_node_whois (dict): The full output of `tailscale whois` against the Tailscale node. 93 | ``` 94 | 95 | # Role Variables 96 | 97 | ## Required 98 | 99 | One of `tailscale_authkey` or `tailscale_up_skip` must be present. 100 | In most cases you will use `tailscale_authkey`. 101 | 102 | If you are uninstalling Tailscale (`state: absent`), 103 | neither `tailscale_authkey` nor `tailscale_up_skip` is required. 104 | 105 | If you are authenticating with an OAuth key, you must also set `tailscale_tags`. 106 | 107 | ### tailscale_authkey 108 | 109 | Is **not** required if `tailscale_up_skip` is set to `true`. 110 | 111 | A Tailscale Node Authorization auth key. 112 | 113 | A Node Authorization key can be generated under your Tailscale account. The role supports two type of keys: 114 | 115 | - Auth key (`tskey-auth-XXX-YYYYY`) 116 | - OAuth key (`tskey-client-XXX-YYYY`) 117 | 118 | > [!IMPORTANT] 119 | > Using an OAuth key requires the following role variables: 120 | > `tailscale_tags` (must be provided), 121 | > `tailscale_oauth_ephemeral` (defaults to `true`), 122 | > and `tailscale_oauth_preauthorized` (defaults to `false`). 123 | 124 | Note that auth keys expire up to a maximum of 90 days after they are generated. 125 | OAuth secrets do not expire unless revoked, 126 | and the generated OAuth access token expires after 1 hour. 127 | 128 | For more information, see Tailscale's [OAuth clients](https://tailscale.com/kb/1215/oauth-clients) page, especially [Generating long-lived auth keys](https://tailscale.com/kb/1215/oauth-clients#generating-long-lived-auth-keys). 129 | 130 | If an OAuth key is used, be sure to grant the `write` Auth Keys scope to the OAuth client. 131 | 132 | OAuth scopes 133 | 134 | This value should be treated as a sensitive secret. 135 | 136 | ### tailscale_tags 137 | 138 | **Default**: `[]` 139 | 140 | Apply supplied tags to the Tailscale nodes configured by this role 141 | (via the `--advertise-tags` flag to `tailscale up`). 142 | For more information, see [What are tags?](https://tailscale.com/kb/1068/acl-tags?q=acl%20tag#what-are-acl-tags) 143 | 144 | > [!NOTE] 145 | > Tags are required for OAuth clients (`tailscale_authkey` OAuth key). 146 | 147 | Entries should not include `tag:`. 148 | For example, `tailscale_tags: ['worker']` translates to `--advertise-tags=tag:worker`. 149 | 150 | ### tailscale_up_skip 151 | 152 | **If set to true, `tailscale_authkey` is not required.** 153 | 154 | **Default**: `false` 155 | 156 | Whether to install and configure Tailscale as a service but skip running `tailscale up`. 157 | Helpful when packaging up a Tailscale installation into a build process, such as AMI creation, 158 | when the server should not yet authenticate to your Tailscale network. 159 | 160 | ## Optional 161 | 162 | ### state 163 | 164 | **Default**: `latest` 165 | 166 | Whether to install or uninstall Tailscale. 167 | If defined, `state` must be either `latest`, `present`, or `absent`. 168 | 169 | This role uses `latest` by default to help ensure your software remains up-to-date 170 | and incorporates the latest security and product features. 171 | For users who desire more control over configuration drift, 172 | `present` will not update Tailscale if it is already installed. 173 | 174 | Changes to [`tailscale_args`](#tailscale_args) will be applied under both `latest` and `present`; 175 | this parameter only impacts the version of Tailscale installed to the target system. 176 | 177 | If set to `absent`, this role will de-register the Tailscale node (if already authenticated) 178 | and clean up or disable all Tailscale artifacts added to the system. 179 | 180 | Note that neither `tailscale_authkey` nor `tailscale_up_skip` is required if `state` is set to `absent`. 181 | 182 | ### tailscale_args 183 | 184 | Pass command-line arguments to `tailscale up`. 185 | 186 | Note that the [command][ansible.builtin.command] module is used, 187 | which does not support subshell expressions (`$()`) or bash operations like `;` and `&`. 188 | Only `tailscale up` arguments can be passed in. 189 | 190 | > [!CAUTION] 191 | > **Do not use this for `--authkey`.** 192 | > Use the `tailscale_authkey` variable instead. 193 | > 194 | > **Do not use this for `--advertise-tags`.** 195 | > Use the `tailscale_tags` variable instead. 196 | > 197 | > **Do not use this for `--timeout`.** 198 | > Use the `tailscale_up_timeout` variable instead. 199 | 200 | Any stdout/stderr output from the `tailscale` binary will be printed. 201 | Since the tasks move quickly in this section, a 5 second pause is introduced 202 | to grant more time for users to realize a message was printed. 203 | 204 | ![printed stdout](docs/images/printed_stdout.png) 205 | 206 | Stderrs will continue to fail the role's execution. 207 | The sensitive `--authkey` value will be redacted by default. 208 | If you need to view the unredacted value, see [`insecurely_log_authkey`](#insecurely_log_authkey). 209 | 210 | ### tailscale_oauth_ephemeral 211 | 212 | > [!NOTE] 213 | > Used only when `tailscale_authkey` is an OAuth key. 214 | 215 | **Default**: `true` 216 | 217 | Register as an [ephemeral node](https://tailscale.com/kb/1111/ephemeral-nodes), if `true`. 218 | 219 | ### tailscale_oauth_preauthorized 220 | 221 | > [!NOTE] 222 | > Used only when `tailscale_authkey` is an OAuth key. 223 | 224 | **Default**: `false` 225 | 226 | Skip [manual device approval](https://tailscale.com/kb/1099/device-approval), if `true`. 227 | 228 | ### insecurely_log_authkey 229 | 230 | **Default**: `false` 231 | 232 | If set to `true`, the "Bring Tailscale Up" command will include the raw value of the Tailscale authkey 233 | when logging any errors encountered during `tailscale up`. 234 | By default, the authkey is not logged in successful task completions 235 | and is redacted in the `stderr` output by this role if an error occurs. 236 | 237 | ![redacted authkey](docs/images/redacted_authkey.png) 238 | 239 | If you are encountering an error bringing Tailscale up 240 | and want the "Bring Tailscale Up" task to _not_ redact the value of the authkey, 241 | set this variable to `true`. 242 | 243 | Regardless, if the authkey is invalid, the role will relay Tailscale's error message on that fact: 244 | 245 | ![invalid authkey](docs/images/invalid_authkey.png) 246 | 247 | ### release_stability 248 | 249 | **Default**: `stable` 250 | 251 | Whether to use the Tailscale stable or unstable track. 252 | 253 | `stable`: 254 | 255 | > Stable releases. If you're not sure which track to use, pick this one. 256 | 257 | `unstable`: 258 | 259 | > The bleeding edge. Pushed early and often. Expect rough edges! 260 | 261 | ### tailscale_up_timeout 262 | 263 | **Default**: `120` 264 | 265 | Defines the timeout duration for the `tailscale up` command in seconds. 266 | 267 | > --timeout duration 268 | > 269 | > maximum amount of time to wait for tailscaled to enter a Running state 270 | 271 | ### verbose 272 | 273 | **Default**: `false` 274 | 275 | Whether to output additional information during role execution. 276 | Helpful for debugging and collecting information to submit in a GitHub issue on this repository. 277 | 278 | # Dependencies 279 | 280 | ## Collections 281 | 282 | - [`community.general`](https://docs.ansible.com/ansible/latest/collections/community/general/index.html) 283 | 284 | # Example Playbook 285 | 286 | ```yaml 287 | - name: Servers 288 | hosts: all 289 | roles: 290 | - role: artis3n.tailscale 291 | vars: 292 | # Example pulling the API key from the env vars on the host running Ansible 293 | tailscale_authkey: "{{ lookup('env', 'TAILSCALE_KEY') }}" 294 | ``` 295 | 296 | Enable Tailscale SSH: 297 | 298 | ```yaml 299 | - name: Servers 300 | hosts: all 301 | roles: 302 | - role: artis3n.tailscale 303 | vars: 304 | # Example pulling the API key from the env vars on the host running Ansible 305 | tailscale_authkey: "{{ lookup('env', 'TAILSCALE_KEY') }}" 306 | tailscale_args: "--ssh" 307 | ``` 308 | 309 | Pass arbitrary command-line arguments: 310 | 311 | ```yaml 312 | - name: Servers 313 | hosts: all 314 | tasks: 315 | - name: Use Headscale 316 | include_role: 317 | name: artis3n.tailscale 318 | vars: 319 | tailscale_args: "--login-server='http://localhost:8080'" 320 | tailscale_authkey: "{{ lookup('env', 'HEADSCALE_KEY') }}" 321 | ``` 322 | 323 | Get verbose output: 324 | 325 | ```yaml 326 | - name: Servers 327 | hosts: all 328 | roles: 329 | - role: artis3n.tailscale 330 | vars: 331 | verbose: true 332 | tailscale_authkey: "{{ lookup('env', 'TAILSCALE_KEY') }}" 333 | ``` 334 | 335 | Connect using an OAuth client secret: 336 | 337 | ```yaml 338 | - name: Servers 339 | hosts: all 340 | roles: 341 | - role: artis3n.tailscale 342 | vars: 343 | verbose: true 344 | tailscale_authkey: "{{ lookup('env', 'TAILSCALE_OAUTH_CLIENT_SECRET') }}" 345 | tailscale_tags: 346 | - "oauth" 347 | # Optionally, also include: 348 | tailscale_oauth_ephemeral: true 349 | tailscale_oauth_preauthorized: false 350 | ``` 351 | 352 | Install Tailscale, but don't authenticate to the network: 353 | 354 | ```yaml 355 | - name: Servers 356 | hosts: all 357 | roles: 358 | - role: artis3n.tailscale 359 | vars: 360 | tailscale_up_skip: true 361 | ``` 362 | 363 | De-register and uninstall a Tailscale node: 364 | 365 | ```yaml 366 | - name: Servers 367 | hosts: all 368 | roles: 369 | - role: artis3n.tailscale 370 | vars: 371 | state: absent 372 | ``` 373 | 374 | # State Tracking 375 | 376 | This role will create an `artis3n-tailscale` directory in the target's [`XDG_STATE_HOME`](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) directory, 377 | or `$HOME/.local/state` if the variable is not present, 378 | in order to maintain a concept of state from the configuration of the arguments passed to `tailscale up`. 379 | This allows the role to idempotently update a Tailscale node's configuration when needed. 380 | Deleting this directory will lead to this role re-configuring Tailscale when it is not needed, 381 | but will not otherwise break anything. 382 | However, it is recommended that you let this Ansible role manage this directory and its contents. 383 | 384 | Note that: 385 | 386 | > Flags are not persisted between runs; you must specify all flags each time. 387 | > 388 | > ... 389 | > 390 | > In Tailscale v1.8 or later, if you forget to specify a flag you added before, 391 | > the CLI will warn you and provide a copyable command that includes all existing flags. 392 | 393 | 394 | 395 | \- [docs: tailscale up][tailscale up docs] 396 | 397 | 398 | 399 | # License 400 | 401 | MIT 402 | 403 | # Author Information 404 | 405 | Ari Kalfus ([@artis3n](https://www.artis3nal.com/)) 406 | 407 | # Development and Contributing 408 | 409 | This GitHub repository uses a dedicated "test" Tailscale account to authenticate Tailscale during CI runs. 410 | Each Docker container creates a new authorized machine in that test account. 411 | The machines are authorized with [ephemeral auth keys][] 412 | and are automatically cleaned up. 413 | 414 | This authkey is stored in a [GitHub Action secret][] with the name `TAILSCALE_CI_KEY`. 415 | To test OAuth authkey compatibility, a Tailscale OAuth client secret is stored as `TAILSCALE_OAUTH_CLIENT_SECRET`. 416 | If you are a Collaborator on this repository, 417 | you can open a GitHub CodeSpace and these secrets will be pre-populated for you in the environment. 418 | 419 | To test this role locally, store the Tailscale ephemeral auth key in a `TAILSCALE_CI_KEY` env var 420 | and, if running the `oauth` Molecule scenario, 421 | add an OAuth client secret in a `TAILSCALE_OAUTH_CLIENT_SECRET` env var. 422 | 423 | Alternatively for [Molecule][] testing, 424 | you can use a [Headscale][] container that is spun up as part of the create/prepare steps. 425 | To do this, set a `USE_HEADSCALE` env variable. 426 | For example: 427 | 428 | ```bash 429 | USE_HEADSCALE=true molecule test 430 | ``` 431 | 432 | [ansible.builtin.command]: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/command_module.html 433 | [ephemeral auth keys]: https://tailscale.com/kb/1111/ephemeral-nodes/ 434 | [github action secret]: https://docs.github.com/en/actions/reference/encrypted-secrets 435 | [molecule]: https://ansible.readthedocs.io/projects/molecule/ 436 | [tailscale]: https://tailscale.com/ 437 | [tailscale up docs]: https://tailscale.com/kb/1080/cli/#up 438 | [headscale]: https://github.com/juanfont/headscale/ 439 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Variables that a user may want to modify. 3 | 4 | # Whether to install or uninstall Tailscale. Either 'latest', 'present', or 'absent' 5 | state: latest 6 | # Required Node auth key to authenticate to Tailscale 7 | tailscale_authkey: "" 8 | # Optional command-line arguments for 'tailscale up' 9 | tailscale_args: "" 10 | # Apply provided tags to node 11 | tailscale_tags: [] 12 | # Set timeout for 'tailscale up' command in seconds 13 | tailscale_up_timeout: "120" 14 | 15 | # Used for OAuth authentication 16 | # Register as an ephemeral node (recommended) 17 | tailscale_oauth_ephemeral: true 18 | # Skip manual device approval 19 | tailscale_oauth_preauthorized: false 20 | 21 | # Whether to output debug information during role execution 22 | verbose: false 23 | # Whether to skip 'tailscale up' 24 | tailscale_up_skip: false 25 | # Whether to use the stable or unstable upstream Tailscale build. 26 | # Strongly recommend to leave on 'stable' unless you know what you're doing 27 | release_stability: stable 28 | # Whether to log your Tailscale authkey in the event of some error with the "Bring Tailscale Up" command. 29 | # Since this value is sensitive, log output is disabled by default. 30 | # You can easily toggle this value by setting this variable to "true". 31 | insecurely_log_authkey: false 32 | # Whether to store hashed authkey in state 33 | # Setting to false won't re-run 'tailscale up' on authkey change 34 | auth_key_in_state: true 35 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/demo.gif -------------------------------------------------------------------------------- /docs/images/invalid_authkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/images/invalid_authkey.png -------------------------------------------------------------------------------- /docs/images/oauth_scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/images/oauth_scopes.png -------------------------------------------------------------------------------- /docs/images/printed_stderr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/images/printed_stderr.png -------------------------------------------------------------------------------- /docs/images/printed_stdout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/images/printed_stdout.png -------------------------------------------------------------------------------- /docs/images/redacted_authkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artis3n/ansible-role-tailscale/6c999cfffe64ce2b9e1215bfd1e431ba6c1a0843/docs/images/redacted_authkey.png -------------------------------------------------------------------------------- /filter_plugins/warn.py: -------------------------------------------------------------------------------- 1 | from ansible.utils.display import Display 2 | 3 | 4 | class FilterModule(object): 5 | def filters(self): return {'print_warn': self.warn_filter} 6 | 7 | def warn_filter(self, message, **kwargs): 8 | lines = message.splitlines() 9 | for line in lines: 10 | Display().warning(line) 11 | return message 12 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fetch Tailscale status 3 | listen: Confirm Tailscale is Connected 4 | ansible.builtin.command: tailscale status --json 5 | changed_when: false 6 | register: tailscale_status 7 | 8 | - name: Parse status JSON 9 | listen: Confirm Tailscale is Connected 10 | ansible.builtin.set_fact: 11 | tailscale_is_online: "{{ (tailscale_status.stdout | from_json).Self.Online }}" 12 | 13 | - name: Tailscale online status 14 | listen: Confirm Tailscale is Connected 15 | ansible.builtin.debug: 16 | msg: "Online: {{ tailscale_is_online }}" 17 | when: verbose 18 | 19 | - name: Assert Tailscale is Connected 20 | listen: Confirm Tailscale is Connected 21 | ansible.builtin.assert: 22 | that: 23 | - tailscale_is_online 24 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: tailscale 4 | author: artis3n 5 | description: Install and enable a Tailscale node. 6 | license: license (MIT) 7 | 8 | min_ansible_version: "2.18" 9 | 10 | # Find supported platforms on https://galaxy.ansible.com/api/v1/platforms/ 11 | platforms: 12 | - name: Ubuntu 13 | versions: 14 | - eoan 15 | - bionic 16 | - focal 17 | - groovy 18 | - hirsute 19 | - jammy 20 | - noble 21 | - name: EL 22 | versions: 23 | - "8" 24 | - name: Amazon Linux 25 | versions: 26 | - "2" 27 | - "2023" 28 | - name: Debian 29 | versions: 30 | - buster 31 | - stretch 32 | - bullseye 33 | - bookworm 34 | - sid 35 | - name: Fedora 36 | versions: 37 | - "31" 38 | - name: ArchLinux 39 | versions: 40 | - all 41 | - name: opensuse 42 | versions: 43 | - "15.1" 44 | - "15.2" 45 | - "15.3" 46 | - "15.4" 47 | - "15.5" 48 | # - "15.6" 49 | # Linux Mint not supported in schema 50 | # https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/meta.json 51 | # - name: LinuxMint 52 | # versions: 53 | # - "22" 54 | 55 | galaxy_tags: 56 | - tailscale 57 | - vpn 58 | - networking 59 | 60 | collections: 61 | - community.general 62 | 63 | dependencies: [] 64 | -------------------------------------------------------------------------------- /molecule/args/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instance 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/args/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Set custom up argument 6 | ansible.builtin.set_fact: 7 | tailscale_args: "--accept-dns" 8 | 9 | - name: Init tailscale credentials variables 10 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 11 | 12 | - name: Install Tailscale with custom up argument 13 | ansible.builtin.include_role: 14 | name: artis3n.tailscale 15 | -------------------------------------------------------------------------------- /molecule/args/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | playbooks: 33 | prepare: ../default/prepare.yml 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: args 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - verify 46 | - cleanup 47 | - destroy 48 | -------------------------------------------------------------------------------- /molecule/args/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/default/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instance 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Init tailscale credentials variables 6 | ansible.builtin.include_tasks: init_tailscale_vars.yml 7 | 8 | - name: "Include artis3n.tailscale" 9 | ansible.builtin.include_role: 10 | name: artis3n.tailscale 11 | vars: 12 | verbose: true 13 | -------------------------------------------------------------------------------- /molecule/default/headscale.config.yaml: -------------------------------------------------------------------------------- 1 | # minimal Headscale configuration for local testing 2 | # See upstream example file for full description of all options: 3 | # https://github.com/juanfont/headscale/blob/main/config-example.yaml 4 | server_url: http://headscale:8080 5 | listen_addr: 0.0.0.0:8080 6 | metrics_listen_addr: 0.0.0.0:9090 7 | grpc_listen_addr: 0.0.0.0:50443 8 | grpc_allow_insecure: true 9 | 10 | noise: 11 | private_key_path: /etc/headscale/noise_private.key 12 | 13 | # Default Tailscale prefixes 14 | prefixes: 15 | v6: fd7a:115c:a1e0::/48 16 | v4: 100.64.0.0/10 17 | 18 | derp: 19 | server: 20 | enabled: true 21 | region_id: 999 22 | region_code: "headscale" 23 | region_name: "Headscale Embedded DERP" 24 | stun_listen_addr: "0.0.0.0:3478" 25 | private_key_path: /etc/headscale/derp_server_private.key 26 | 27 | database: 28 | type: sqlite 29 | sqlite: 30 | path: /etc/headscale/db.sqlite 31 | 32 | dns: 33 | base_domain: molecule.internal 34 | -------------------------------------------------------------------------------- /molecule/default/init_tailscale_vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Use tailscale service 3 | ansible.builtin.set_fact: 4 | tailscale_authkey: "{{ lookup('ansible.builtin.env', 'TAILSCALE_CI_KEY') }}" 5 | when: not lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) 6 | 7 | - name: Fetch headscale preauth key 8 | delegate_to: localhost 9 | changed_when: false 10 | community.docker.docker_container_exec: 11 | container: headscale 12 | command: headscale preauthkeys list -u test -o json 13 | register: preauth_list 14 | when: lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) 15 | 16 | - name: Use headscale service 17 | vars: 18 | combined_args: "{{ tailscale_args|default('') }} --login-server=http://headscale:8080" 19 | ansible.builtin.set_fact: 20 | tailscale_authkey: "{{ (preauth_list.stdout|from_json)[0].key }}" 21 | tailscale_args: "{{ combined_args }}" 22 | when: lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) 23 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | verifier: 33 | name: ansible 34 | scenario: 35 | name: default 36 | test_sequence: 37 | - dependency 38 | - destroy 39 | - syntax 40 | - create 41 | - prepare 42 | - converge 43 | - idempotence 44 | - verify 45 | - cleanup 46 | - destroy 47 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: localhost 4 | gather_facts: false 5 | tasks: 6 | - name: Create Headscale user 7 | community.docker.docker_container_exec: 8 | container: headscale 9 | command: headscale users create test 10 | 11 | - name: Create preauth key 12 | community.docker.docker_container_exec: 13 | container: headscale 14 | command: headscale preauthkeys create -u test --reusable 15 | 16 | - name: Fetch Headscale container info 17 | community.docker.docker_container_info: 18 | name: headscale 19 | register: headscale_info 20 | 21 | - name: Fetch Headscale network info 22 | community.docker.docker_network_info: 23 | name: headscale 24 | register: headscale_network 25 | 26 | - name: Get instance names 27 | ansible.builtin.set_fact: 28 | instance_names: "{{ headscale_network.network.Containers | dict2items | selectattr('value.Name', 'match', '^instance') | map(attribute='value.Name') | list }}" 29 | 30 | - name: Set hosts override for Headscale 31 | delegate_to: "{{ item }}" 32 | loop: "{{ instance_names }}" 33 | ansible.builtin.lineinfile: 34 | path: /etc/hosts 35 | line: "{{ headscale_info.container.NetworkSettings.Networks.headscale.IPAddress }} headscale" 36 | unsafe_writes: true # Hosts file in the docker container can't be written to atomically 37 | -------------------------------------------------------------------------------- /molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/idempotent-up/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instance 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/idempotent-up/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Set custom up argument 6 | ansible.builtin.set_fact: 7 | tailscale_args: "--advertise-exit-node" 8 | 9 | - name: Init tailscale credentials variables 10 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 11 | 12 | - name: Configure Tailscale with a new up argument 13 | ansible.builtin.include_role: 14 | name: artis3n.tailscale 15 | 16 | - name: Attempt to configure Tailscale without previous up argument 17 | block: 18 | - name: Set custom up argument 19 | ansible.builtin.set_fact: 20 | tailscale_args: null 21 | 22 | - name: Init tailscale credentials variables 23 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 24 | 25 | - name: Run tailscale up 26 | ansible.builtin.include_role: 27 | name: artis3n.tailscale 28 | register: should_fail 29 | 30 | - name: Check execution halted 31 | ansible.builtin.fail: 32 | msg: Should not be reached 33 | register: should_not_run 34 | rescue: 35 | - name: Ensure tailscale up failed 36 | ansible.builtin.assert: 37 | that: 38 | - should_fail is defined 39 | - should_not_run is not defined 40 | 41 | - name: Set custom up argument 42 | ansible.builtin.set_fact: 43 | tailscale_args: "--reset" 44 | 45 | - name: Init tailscale credentials variables 46 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 47 | 48 | - name: Reset all previous Tailscale arguments 49 | ansible.builtin.include_role: 50 | name: artis3n.tailscale 51 | -------------------------------------------------------------------------------- /molecule/idempotent-up/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | playbooks: 33 | prepare: ../default/prepare.yml 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: idempotent-up 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - verify 46 | - cleanup 47 | - destroy 48 | -------------------------------------------------------------------------------- /molecule/idempotent-up/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/oauth/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instance 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/oauth/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Init tailscale credentials variables 6 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 7 | 8 | - name: "Include artis3n.tailscale" 9 | ansible.builtin.include_role: 10 | name: artis3n.tailscale 11 | vars: 12 | verbose: true 13 | tailscale_tags: 14 | - "ci-worker" 15 | -------------------------------------------------------------------------------- /molecule/oauth/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | playbooks: 33 | prepare: ../default/prepare.yml 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: oauth 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - idempotence 46 | - verify 47 | - cleanup 48 | - destroy 49 | -------------------------------------------------------------------------------- /molecule/oauth/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/reinstall/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Init tailscale credentials variables 6 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 7 | 8 | - name: Install latest Tailscale version using artis3n.tailscale 9 | ansible.builtin.include_role: 10 | name: artis3n.tailscale 11 | vars: 12 | state: latest 13 | 14 | # Force these to run before we start uninstalling things 15 | - name: Flush Handlers 16 | ansible.builtin.meta: flush_handlers 17 | 18 | - name: Remove Tailscale using artis3n.tailscale 19 | ansible.builtin.include_role: 20 | name: artis3n.tailscale 21 | vars: 22 | state: absent 23 | 24 | - name: Reinstall Tailscale using artis3n.tailscale 25 | ansible.builtin.include_role: 26 | name: artis3n.tailscale 27 | vars: 28 | state: latest 29 | -------------------------------------------------------------------------------- /molecule/reinstall/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | # We're not using the same container image as the other scenarios because this scenario 11 | # is specifically testing behaviour in a CentOS like environment 12 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-rockylinux9-ansible:latest} 13 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 14 | volumes: 15 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 16 | docker_networks: 17 | - name: headscale 18 | networks: 19 | - name: bridge 20 | - name: headscale 21 | cgroupns_mode: host 22 | privileged: true 23 | pre_build_image: true 24 | - name: headscale 25 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 26 | command: serve 27 | pre_build_image: true 28 | networks: 29 | - name: headscale 30 | volumes: 31 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 32 | provisioner: 33 | name: ansible 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: "reinstall" 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - verify 46 | - cleanup 47 | - destroy 48 | -------------------------------------------------------------------------------- /molecule/reinstall/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/skip-authentication/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: "Include artis3n.tailscale" 6 | ansible.builtin.include_role: 7 | name: artis3n.tailscale 8 | vars: 9 | tailscale_up_skip: true 10 | -------------------------------------------------------------------------------- /molecule/skip-authentication/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | cgroupns_mode: host 15 | privileged: true 16 | pre_build_image: true 17 | provisioner: 18 | name: ansible 19 | verifier: 20 | name: ansible 21 | scenario: 22 | name: skip-authentication 23 | test_sequence: 24 | - dependency 25 | - destroy 26 | - syntax 27 | - create 28 | - converge 29 | - idempotence 30 | - verify 31 | - destroy 32 | -------------------------------------------------------------------------------- /molecule/skip-authentication/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | failed_when: 11 | - tailscale_status.rc != 0 12 | - "'Logged out.' not in tailscale_status.stdout" 13 | - "'not logged in' not in tailscale_status.stdout" 14 | 15 | - name: Assertions 16 | ansible.builtin.assert: 17 | that: 18 | - "'Logged out.' in tailscale_status.stdout" 19 | -------------------------------------------------------------------------------- /molecule/state-absent/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Init tailscale credentials variables 6 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 7 | 8 | # Should be idempotent and also not error out if run before Tailscale has ever been installed 9 | - name: Uninstall Tailscale 10 | ansible.builtin.include_role: 11 | name: artis3n.tailscale 12 | vars: 13 | state: absent 14 | 15 | - name: Install Tailscale 16 | ansible.builtin.include_role: 17 | name: artis3n.tailscale 18 | 19 | # Force these to run before we start uninstalling things 20 | - name: Flush Handlers 21 | ansible.builtin.meta: flush_handlers 22 | 23 | - name: Get Tailscale status 24 | become: true 25 | ansible.builtin.command: tailscale status 26 | changed_when: false 27 | register: tailscale_status 28 | 29 | - name: Assert Tailscale is installed 30 | ansible.builtin.assert: 31 | that: 32 | - "'Logged out.' not in tailscale_status.stdout" 33 | - "'not logged in' not in tailscale_status.stdout" 34 | 35 | - name: Uninstall Tailscale 36 | ansible.builtin.include_role: 37 | name: artis3n.tailscale 38 | vars: 39 | state: absent 40 | -------------------------------------------------------------------------------- /molecule/state-absent/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | playbooks: 33 | prepare: ../default/prepare.yml 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: state-absent 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - verify 46 | - destroy 47 | -------------------------------------------------------------------------------- /molecule/state-absent/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Gather Service Facts 6 | ansible.builtin.service_facts: 7 | 8 | - name: Get Tailscale path 9 | ansible.builtin.command: which tailscale 10 | changed_when: false 11 | failed_when: 12 | - tailscale_status.rc != 0 13 | - tailscale_status.rc != 1 14 | register: tailscale_status 15 | 16 | - name: Get idempotent state directory status 17 | ansible.builtin.stat: 18 | path: "{{ ansible_env.HOME }}/.artis3n-tailscale" 19 | register: state_dir 20 | 21 | - name: Assertions 22 | ansible.builtin.assert: 23 | that: 24 | - tailscale_status.rc == 1 25 | - not state_dir.stat.exists 26 | - "'tailscaled' not in ansible_facts.services or ansible_facts.services['tailscaled']['status'] == 'stopped'" 27 | 28 | - name: Get tailscale package state file status 29 | ansible.builtin.stat: 30 | path: "/var/lib/tailscale/tailscaled.state" 31 | register: ts_state_dir 32 | 33 | - name: Assertions 34 | ansible.builtin.assert: 35 | that: 36 | - not ts_state_dir.stat.exists 37 | -------------------------------------------------------------------------------- /molecule/state-idempotency/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instance 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/state-idempotency/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instance 4 | tasks: 5 | - name: Init tailscale credentials variables 6 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 7 | 8 | - name: "Include artis3n.tailscale" 9 | ansible.builtin.include_role: 10 | name: artis3n.tailscale 11 | vars: 12 | state: present 13 | verbose: true 14 | -------------------------------------------------------------------------------- /molecule/state-idempotency/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 11 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | docker_networks: 15 | - name: headscale 16 | networks: 17 | - name: bridge 18 | - name: headscale 19 | cgroupns_mode: host 20 | privileged: true 21 | pre_build_image: true 22 | - name: headscale 23 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 24 | command: serve 25 | pre_build_image: true 26 | networks: 27 | - name: headscale 28 | volumes: 29 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 30 | provisioner: 31 | name: ansible 32 | playbooks: 33 | prepare: ../default/prepare.yml 34 | verifier: 35 | name: ansible 36 | scenario: 37 | name: state-idempotency 38 | test_sequence: 39 | - dependency 40 | - destroy 41 | - syntax 42 | - create 43 | - prepare 44 | - converge 45 | - idempotence 46 | - verify 47 | - cleanup 48 | - destroy 49 | -------------------------------------------------------------------------------- /molecule/state-idempotency/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instance 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /molecule/strategy-free/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: instances 4 | tasks: 5 | - name: De-register Tailscale node 6 | become: true 7 | ansible.builtin.command: tailscale logout 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/strategy-free/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: instances 4 | strategy: free 5 | tasks: 6 | - name: Init tailscale credentials variables 7 | ansible.builtin.include_tasks: ../default/init_tailscale_vars.yml 8 | 9 | - name: "Include artis3n.tailscale" 10 | ansible.builtin.include_role: 11 | name: artis3n.tailscale 12 | vars: 13 | verbose: true 14 | -------------------------------------------------------------------------------- /molecule/strategy-free/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance-1 10 | groups: 11 | - instances 12 | image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2404-ansible:latest} 13 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 14 | volumes: 15 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 16 | docker_networks: 17 | - name: headscale 18 | networks: 19 | - name: bridge 20 | - name: headscale 21 | cgroupns_mode: host 22 | privileged: true 23 | pre_build_image: true 24 | - name: instance-2 25 | groups: 26 | - instances 27 | image: 'ghcr.io/artis3n/docker-amazonlinux2023-ansible:latest' 28 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 29 | volumes: 30 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 31 | docker_networks: 32 | - name: headscale 33 | networks: 34 | - name: bridge 35 | - name: headscale 36 | cgroupns_mode: host 37 | privileged: true 38 | pre_build_image: true 39 | - name: instance-3 40 | groups: 41 | - instances 42 | image: 'cisagov/docker-debian12-ansible:latest' 43 | command: ${MOLECULE_COMMAND:-/usr/sbin/init} 44 | volumes: 45 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 46 | docker_networks: 47 | - name: headscale 48 | networks: 49 | - name: bridge 50 | - name: headscale 51 | cgroupns_mode: host 52 | privileged: true 53 | pre_build_image: true 54 | - name: headscale 55 | image: ${HEADSCALE_IMAGE:-headscale/headscale:stable} 56 | command: serve 57 | pre_build_image: true 58 | networks: 59 | - name: headscale 60 | volumes: 61 | - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" 62 | provisioner: 63 | name: ansible 64 | playbooks: 65 | prepare: ../default/prepare.yml 66 | verifier: 67 | name: ansible 68 | scenario: 69 | name: strategy-free 70 | test_sequence: 71 | - dependency 72 | - destroy 73 | - syntax 74 | - create 75 | - prepare 76 | - converge 77 | - idempotence 78 | - verify 79 | - cleanup 80 | - destroy 81 | -------------------------------------------------------------------------------- /molecule/strategy-free/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: instances 4 | tasks: 5 | - name: Get Tailscale status 6 | become: true 7 | ansible.builtin.command: tailscale status 8 | changed_when: false 9 | register: tailscale_status 10 | 11 | - name: Assertions 12 | ansible.builtin.assert: 13 | that: 14 | - "'Logged out.' not in tailscale_status.stdout" 15 | - "'not logged in' not in tailscale_status.stdout" 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "artis3n.tailscale" 3 | version = "5.0.0" 4 | description = "Ansible role to install and enable a Tailscale node." 5 | authors = ["Artis3n "] 6 | license = "MIT" 7 | 8 | package-mode = false 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.12" 12 | ansible = "^11.0.0" 13 | 14 | [tool.poetry.group.dev.dependencies] 15 | ansible-lint = { version = "^25.1.3", markers = "platform_system != 'Windows'" } 16 | molecule = "^25.3.1" 17 | molecule-plugins = {extras = ["docker"], version = "^23.5.3"} 18 | pre-commit = "^4.0.1" 19 | pytest = "^8.3.3" 20 | 21 | [build-system] 22 | requires = ["poetry-core>=1.0.0"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - name: https://github.com/ansible-collections/community.general 3 | type: git 4 | - name: https://github.com/ansible-collections/community.docker 5 | type: git 6 | -------------------------------------------------------------------------------- /tasks/arch/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Arch | Install Tailscale 3 | become: true 4 | community.general.pacman: 5 | name: '{{ tailscale_package }}' 6 | update_cache: true 7 | state: '{{ state }}' 8 | -------------------------------------------------------------------------------- /tasks/arch/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Arch | Remove Tailscale 3 | become: true 4 | community.general.pacman: 5 | name: '{{ tailscale_package }}' 6 | state: absent 7 | -------------------------------------------------------------------------------- /tasks/centos/install-legacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: CentOS | Yum Dependencies 3 | become: true 4 | ansible.builtin.yum: 5 | name: "{{ tailscale_yum_dependencies }}" 6 | state: present 7 | 8 | - name: CentOS | Add Tailscale Repo 9 | become: true 10 | ansible.builtin.command: yum-config-manager --add-repo {{ tailscale_yum_repos[ansible_distribution] }} 11 | args: 12 | creates: /etc/yum.repos.d/tailscale.repo 13 | register: add_tailscale_repo 14 | 15 | - name: CentOS | Install Tailscale 16 | become: true 17 | ansible.builtin.yum: 18 | name: "{{ tailscale_package }}" 19 | update_cache: '{{ add_tailscale_repo.changed | default(false) | bool }}' 20 | state: '{{ state }}' 21 | -------------------------------------------------------------------------------- /tasks/centos/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: CentOS | DNF Dependencies 3 | become: true 4 | ansible.builtin.dnf: 5 | name: "{{ tailscale_yum_dependencies }}" 6 | state: present 7 | 8 | - name: CentOS | Check if Tailscale Repo is already present 9 | become: true 10 | ansible.builtin.stat: 11 | path: /etc/yum.repos.d/tailscale.repo 12 | register: tailscale_repo_state 13 | 14 | - name: CentOS | Check if Tailscale Repo is already enabled 15 | become: true 16 | changed_when: false 17 | ansible.builtin.command: "dnf repolist enabled" 18 | register: tailscale_repolist_enabled 19 | 20 | - name: CentOS | Enable Tailscale Repo # noqa no-changed-when 21 | become: true 22 | when: "'tailscale' not in tailscale_repolist_enabled.stdout and tailscale_repo_state.stat.exists" 23 | ansible.builtin.command: "dnf config-manager --set-enabled tailscale*" 24 | register: add_tailscale_repo 25 | 26 | - name: CentOS | Add Tailscale Repo 27 | become: true 28 | when: not tailscale_repo_state.stat.exists 29 | ansible.builtin.command: dnf config-manager --add-repo {{ tailscale_yum_repos[ansible_distribution] }} 30 | args: 31 | creates: /etc/yum.repos.d/tailscale.repo 32 | register: add_tailscale_repo 33 | 34 | - name: CentOS | Install Tailscale 35 | become: true 36 | ansible.builtin.dnf: 37 | name: "{{ tailscale_package }}" 38 | update_cache: "{{ add_tailscale_repo.changed | default(false) | bool }}" 39 | state: "{{ state }}" 40 | -------------------------------------------------------------------------------- /tasks/centos/uninstall-legacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: CentOS | Remove Tailscale 3 | become: true 4 | ansible.builtin.yum: 5 | name: "{{ tailscale_package }}" 6 | state: absent 7 | 8 | # noqa command-instead-of-module 9 | - name: CentOS | Check for Tailscale Repo 10 | ansible.builtin.shell: | 11 | set -o pipefail 12 | yum repolist | grep tailscale 13 | register: repolist_tailscale 14 | changed_when: false 15 | failed_when: 16 | - repolist_tailscale.rc != 0 17 | - repolist_tailscale.rc != 1 18 | 19 | - name: CentOS | Remove Tailscale Repo 20 | become: true 21 | ansible.builtin.command: yum-config-manager --disable tailscale-{{ release_stability | lower }} 22 | register: yum_config_output 23 | changed_when: yum_config_output.rc != 0 24 | when: repolist_tailscale.rc != 1 25 | -------------------------------------------------------------------------------- /tasks/centos/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: CentOS | Remove Tailscale 3 | become: true 4 | ansible.builtin.dnf: 5 | name: "{{ tailscale_package }}" 6 | state: absent 7 | 8 | # noqa command-instead-of-module 9 | - name: CentOS | Check for Tailscale Repo 10 | ansible.builtin.shell: | 11 | set -o pipefail 12 | dnf repolist | grep tailscale 13 | register: repolist_tailscale 14 | changed_when: false 15 | failed_when: 16 | - repolist_tailscale.rc != 0 17 | - repolist_tailscale.rc != 1 18 | 19 | - name: CentOS | Remove Tailscale Repo 20 | become: true 21 | ansible.builtin.command: dnf config-manager --disable tailscale-{{ release_stability | lower }} 22 | register: dnf_config_output 23 | changed_when: dnf_config_output.rc != 0 24 | when: repolist_tailscale.rc != 1 25 | -------------------------------------------------------------------------------- /tasks/debian/apt-codename.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Retrieve "ID_LIKE" from /etc/os-release 3 | # ID_LIKE has the following values, depending on the operating system 4 | # | OS | ID_LIKE | 5 | # | Debian | -- does not exist -- | 6 | # | Raspberry Pi OS | -- does not exist -- | 7 | # | Proxmox | -- does not exist -- | 8 | # | Devuan | "debian" | 9 | # | Raspbian | "debian" | 10 | # | Ubuntu | "debian" | 11 | # | Pop!OS | "ubuntu debian" | 12 | # | Linux Mint | "ubuntu debian" | 13 | - name: Debian | Retrieve Apt ID_LIKE 14 | check_mode: false 15 | ansible.builtin.command: >- 16 | awk -F= '/^ID_LIKE/ {print $2}' {{ ansible_distribution_file_path }} 17 | register: apt_id_like 18 | changed_when: false 19 | 20 | # Ubuntu and its derivatives all have a value called "UBUNTU_CODENAME" set in their "/etc/os-release" 21 | # This value points to the actual ubuntu Version. 22 | # Ansible does not pick up that value on setup. Usually it picks up "VERSION_CODENAME" 23 | - name: Debian | Retrieve ubuntu_codename on Ubuntu derivatives 24 | check_mode: false 25 | ansible.builtin.command: >- 26 | awk -F= '/^UBUNTU_CODENAME/ {print $2}' {{ ansible_distribution_file_path }} 27 | register: ubuntu_codename 28 | when: apt_id_like.stdout == '"ubuntu debian"' 29 | changed_when: false 30 | 31 | - name: Debian | Set codename for apt 32 | ansible.builtin.set_fact: 33 | apt_codename: "{{ ubuntu_codename.stdout | default(ansible_distribution_release) }}" 34 | -------------------------------------------------------------------------------- /tasks/debian/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Debian | Include apt codename generation tasks 3 | ansible.builtin.include_tasks: 4 | file: apt-codename.yml 5 | 6 | - name: Debian | Apt Dependencies 7 | become: true 8 | ansible.builtin.apt: 9 | name: "{{ tailscale_apt_dependencies }}" 10 | cache_valid_time: 3600 11 | state: present 12 | 13 | - name: Debian | Legacy Apt Dependencies 14 | become: true 15 | ansible.builtin.apt: 16 | name: "{{ tailscale_legacy_apt_dependencies }}" 17 | when: 18 | - ansible_distribution_major_version | int < 11 19 | # Prelease versions of debian tend to not have a version integer yet 20 | - ansible_distribution_major_version != "testing" 21 | - ansible_distribution_major_version != "n/a" 22 | 23 | - name: Debian | Add Tailscale Signing Key 24 | become: true 25 | ansible.builtin.get_url: 26 | url: "{{ tailscale_apt_signkey }}" 27 | dest: "{{ tailscale_apt_keyring_path }}" 28 | mode: '0644' 29 | 30 | - name: Debian | Add Tailscale Deb 31 | become: true 32 | ansible.builtin.apt_repository: 33 | repo: "{{ tailscale_apt_deb }}" 34 | filename: "tailscale" 35 | state: present 36 | codename: "{{ apt_codename }}" 37 | 38 | - name: Debian | Install Tailscale 39 | become: true 40 | ansible.builtin.apt: 41 | name: "{{ tailscale_package }}" 42 | cache_valid_time: 3600 43 | state: '{{ state }}' 44 | -------------------------------------------------------------------------------- /tasks/debian/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Debian | Include apt codename generation tasks 3 | ansible.builtin.include_tasks: 4 | file: apt-codename.yml 5 | 6 | - name: Debian | Apt Dependencies 7 | become: true 8 | ansible.builtin.apt: 9 | name: "{{ tailscale_apt_dependencies }}" 10 | cache_valid_time: 3600 11 | state: present 12 | 13 | - name: Debian | Remove Tailscale 14 | become: true 15 | ansible.builtin.apt: 16 | name: "{{ tailscale_package }}" 17 | state: absent 18 | 19 | - name: Debian | Remove Tailscale Deb 20 | become: true 21 | ansible.builtin.apt_repository: 22 | repo: "{{ tailscale_apt_deb }}" 23 | state: absent 24 | codename: "{{ apt_codename }}" 25 | 26 | - name: Debian | Remove Tailscale Signing Key from trusted.gpg 27 | become: true 28 | ansible.builtin.apt_key: 29 | url: "{{ tailscale_apt_signkey }}" 30 | state: absent 31 | 32 | - name: Debian | Remove Tailscale Signing Key from keyrings 33 | become: true 34 | ansible.builtin.file: 35 | path: "{{ tailscale_apt_keyring_path }}" 36 | state: absent 37 | -------------------------------------------------------------------------------- /tasks/facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Facts | Get IPv4 address 3 | ansible.builtin.command: 4 | cmd: tailscale ip --4 5 | register: tailscale_ipv4_cmd 6 | changed_when: false 7 | 8 | - name: Facts | Get IPv6 address 9 | ansible.builtin.command: 10 | cmd: tailscale ip --6 11 | register: tailscale_ipv6_cmd 12 | changed_when: false 13 | 14 | - name: Facts | Register IP facts 15 | ansible.builtin.set_fact: 16 | tailscale_node_ipv4: "{{ tailscale_ipv4_cmd.stdout }}" 17 | tailscale_node_ipv6: "{{ tailscale_ipv6_cmd.stdout }}" 18 | 19 | - name: Facts | Get Tailscale host facts 20 | ansible.builtin.command: 21 | cmd: tailscale whois --json {{ tailscale_node_ipv4 }} 22 | register: tailscale_whois_cmd 23 | changed_when: false 24 | 25 | - name: Facts | Parse Tailscale host information 26 | ansible.builtin.set_fact: 27 | tailscale_whois: "{{ tailscale_whois_cmd.stdout | from_json }}" 28 | 29 | - name: Facts | Set Tailscale host facts 30 | ansible.builtin.set_fact: 31 | tailscale_node_hostname_full: "{{ tailscale_whois.Node.Name }}" 32 | tailscale_node_hostname_short: "{{ tailscale_whois.Node.Hostinfo.Hostname }}" 33 | tailscale_node_created_at: "{{ tailscale_whois.Node.Created }}" 34 | tailscale_node_services: "{{ tailscale_whois.Node.Hostinfo.Services | default([]) }}" 35 | tailscale_node_tags: "{{ tailscale_whois.Node.Tags | default([]) }}" 36 | tailscale_node_whois: "{{ tailscale_whois.Node }}" 37 | 38 | - name: Facts | Display key facts 39 | ansible.builtin.debug: 40 | var: "{{ item }}" 41 | when: verbose 42 | loop: 43 | - tailscale_node_hostname_full 44 | - tailscale_node_hostname_short 45 | - tailscale_node_created_at 46 | - tailscale_node_ipv4 47 | - tailscale_node_ipv6 48 | - tailscale_node_tags 49 | -------------------------------------------------------------------------------- /tasks/fedora/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fedora | Prepare Systemd 3 | ansible.builtin.import_tasks: ./systemd.yml 4 | 5 | - name: Fedora | DNF Plugins 6 | become: true 7 | ansible.builtin.dnf: 8 | name: "{{ tailscale_dnf_plugins }}" 9 | update_cache: true 10 | state: present 11 | 12 | - name: Fedora | DNF Dependencies 13 | become: true 14 | ansible.builtin.dnf: 15 | name: "{{ tailscale_dnf_dependencies }}" 16 | state: present 17 | 18 | - name: Fedora | Add DNF Repo 19 | become: true 20 | ansible.builtin.command: dnf config-manager --add-repo {{ tailscale_dnf_repos[ansible_distribution] }} 21 | args: 22 | creates: /etc/yum.repos.d/tailscale.repo 23 | 24 | - name: Fedora | Install Tailscale 25 | become: true 26 | ansible.builtin.dnf: 27 | name: "{{ tailscale_package }}" 28 | update_cache: true 29 | state: '{{ state }}' 30 | -------------------------------------------------------------------------------- /tasks/fedora/systemd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://www.jeffgeerling.com/blog/2020/resolving-fedora-dnf-error-no-such-file-or-directory-varlibdnfrpmdblockpid 3 | - name: Fedora | Wait for systemd to complete initialization. # noqa command-instead-of-module 4 | ansible.builtin.command: systemctl is-system-running 5 | register: systemctl_status 6 | until: > 7 | 'running' in systemctl_status.stdout or 8 | 'degraded' in systemctl_status.stdout 9 | retries: 30 10 | delay: 5 11 | when: ansible_service_mgr == 'systemd' 12 | changed_when: false 13 | failed_when: 14 | - systemctl_status.rc != 0 15 | # It is not a failure if 'degraded' exit code 1 is returned, otherwise it is 16 | - not (systemctl_status.rc == 1 and 'degraded' in systemctl_status.stdout ) 17 | -------------------------------------------------------------------------------- /tasks/fedora/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fedora | Prepare Systemd 3 | ansible.builtin.import_tasks: ./systemd.yml 4 | 5 | - name: Fedora | Remove Tailscale 6 | become: true 7 | ansible.builtin.dnf: 8 | name: "{{ tailscale_package }}" 9 | state: absent 10 | 11 | # noqa command-instead-of-module 12 | - name: Fedora | Check for Tailscale Repo 13 | ansible.builtin.shell: | 14 | set -o pipefail 15 | dnf repolist | grep tailscale 16 | register: repolist_tailscale 17 | changed_when: false 18 | failed_when: 19 | - repolist_tailscale.rc != 0 20 | - repolist_tailscale.rc != 1 21 | 22 | - name: Fedora | Remove Dnf Repo 23 | become: true 24 | ansible.builtin.command: dnf config-manager --disable tailscale-{{ release_stability | lower }} 25 | register: dnf_config_output 26 | changed_when: dnf_config_output.rc != 0 27 | when: repolist_tailscale.rc != 1 28 | -------------------------------------------------------------------------------- /tasks/files/state_readme.md: -------------------------------------------------------------------------------- 1 | # artis3n-tailscale/ 2 | 3 | This directory is used to manage idempotency checks in the [artis3n.tailscale](https://github.com/artis3n/ansible-role-tailscale) Ansible role. The state file holds the SHA256 hash of the `tailscale up` arguments so the role knows when state has changed and to re-run `up` if the configuration has changed. 4 | 5 | Please do not modify files in this repository manually unless you want to purge this role from your system. 6 | If you are trying to uninstall, I recommend running the role with `state: absent`. 7 | -------------------------------------------------------------------------------- /tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install | CentOS and related families 3 | when: > 4 | ansible_distribution in tailscale_centos_family_distros 5 | ansible.builtin.include_tasks: centos/install.yml 6 | 7 | - name: Install | Amazon Linux 2 8 | when: 9 | - ansible_distribution == 'Amazon' 10 | - ansible_distribution_major_version | int < 2023 11 | ansible.builtin.include_tasks: centos/install-legacy.yml 12 | 13 | - name: Install | Debian and related families 14 | when: ansible_distribution in tailscale_debian_family_distros 15 | ansible.builtin.include_tasks: debian/install.yml 16 | 17 | - name: Install | Fedora and related families 18 | when: > 19 | ansible_distribution == 'Fedora' 20 | or (ansible_distribution == 'Amazon' and ansible_distribution_major_version | int >= 2023) 21 | ansible.builtin.include_tasks: fedora/install.yml 22 | 23 | - name: Install | Arch 24 | when: ansible_distribution == 'Archlinux' 25 | ansible.builtin.include_tasks: arch/install.yml 26 | 27 | - name: Install | OpenSUSE 28 | when: ansible_distribution in tailscale_opensuse_family_distros 29 | ansible.builtin.include_tasks: opensuse/install.yml 30 | 31 | - name: Install | Remove legacy state folder 32 | ansible.builtin.file: 33 | path: "{{ ansible_env.HOME }}/.artis3n-tailscale" 34 | state: absent 35 | 36 | - name: Install | Determine state folder 37 | ansible.builtin.set_fact: 38 | # Following https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 39 | tailscale_state_folder: "{{ ansible_env.XDG_STATE_HOME | default(ansible_env.HOME + '/.local/state') }}" 40 | 41 | - name: Install | Set state idempotency folder 42 | ansible.builtin.file: 43 | path: "{{ item }}" 44 | state: directory 45 | owner: "{{ ansible_user_uid }}" 46 | group: "{{ ansible_user_gid }}" 47 | mode: '0700' 48 | loop: 49 | - "{{ tailscale_state_folder }}" 50 | - "{{ tailscale_state_folder }}/artis3n-tailscale" 51 | 52 | - name: Install | Store state idempotency README 53 | ansible.builtin.copy: 54 | src: files/state_readme.md 55 | dest: "{{ tailscale_state_folder }}/artis3n-tailscale/README.md" 56 | owner: "{{ ansible_user_uid }}" 57 | group: "{{ ansible_user_gid }}" 58 | mode: '0644' 59 | 60 | - name: Install | Enable Service 61 | become: true 62 | ansible.builtin.service: 63 | name: "{{ tailscale_service }}" 64 | state: started 65 | enabled: true 66 | 67 | - name: Install | Fetch Tailscale status 68 | ansible.builtin.command: tailscale status --json 69 | changed_when: false 70 | register: tailscale_status 71 | 72 | - name: Install | Parse status JSON 73 | vars: 74 | tailscale_status_parsed: "{{ tailscale_status.stdout | from_json }}" 75 | ansible.builtin.set_fact: 76 | tailscale_is_online: "{{ tailscale_status_parsed.Self.Online }}" 77 | tailscale_version: "{{ tailscale_status_parsed.Version }}" 78 | 79 | - name: Install | Tailscale version and online status 80 | ansible.builtin.debug: 81 | msg: "Ver: {{ tailscale_version }} Online: {{ tailscale_is_online }}" 82 | when: verbose 83 | 84 | - name: Install | Prepend 'tag:' to each item in the list 85 | ansible.builtin.set_fact: 86 | tailscale_prepared_tags: "{{ tailscale_tags | map('regex_replace', '^', 'tag:') | list }}" 87 | 88 | # OAuth key starts with 'tskey-client-', auth key starts with 'tskey-auth-', with headscale it can be 'unused' 89 | - name: Install | Build `tailscale up` arguments strings 90 | ansible.builtin.set_fact: 91 | tailscale_authkey_type: >- 92 | {# Check if the key is an OAuth key #} 93 | {% if tailscale_authkey.startswith('tskey-client-') %} 94 | OAuth Client Secret 95 | {% elif tailscale_authkey.startswith('tskey-auth-') %} 96 | API Token 97 | {% else %} 98 | Unknown token format 99 | {% endif %} 100 | tailscale_authkey_string: >- 101 | {# Check if the key is an OAuth key #} 102 | {% if tailscale_authkey.startswith('tskey-client-') %} 103 | {{ tailscale_authkey }}?ephemeral={{ tailscale_oauth_ephemeral | bool }}&preauthorized={{ tailscale_oauth_preauthorized | bool }} 104 | {# Check if the key is not OAuth (regular authkey or unused) #} 105 | {% else %} 106 | {{ tailscale_authkey }} 107 | {% endif %} 108 | tailscale_tags_string: >- 109 | {% if tailscale_tags | length > 0 %} 110 | --advertise-tags={{ tailscale_prepared_tags | join(',') }} 111 | {% endif %} 112 | no_log: "{{ not (insecurely_log_authkey | bool) }}" 113 | 114 | - name: Install | Authkey Type 115 | ansible.builtin.debug: 116 | msg: "{{ tailscale_authkey_type | trim }}" 117 | when: verbose 118 | 119 | - name: Install | Build the final tailscale_args 120 | ansible.builtin.set_fact: 121 | tailscale_args_string: > 122 | {{ tailscale_args }} 123 | {{ tailscale_tags_string | trim if tailscale_tags_string is not none else '' }} 124 | --timeout={{ tailscale_up_timeout | trim }}s 125 | 126 | - name: Install | Final `tailscale up` arguments string 127 | ansible.builtin.debug: 128 | msg: "{{ tailscale_args_string | trim }}" 129 | when: verbose 130 | 131 | - name: Install | Save State 132 | ansible.builtin.template: 133 | src: state.j2 134 | dest: "{{ tailscale_state_folder }}/artis3n-tailscale/state" 135 | owner: "{{ ansible_user_uid }}" 136 | group: "{{ ansible_user_gid }}" 137 | mode: '0644' 138 | register: state_file 139 | 140 | - name: Install | Bring Tailscale Up 141 | become: true 142 | ansible.builtin.command: "tailscale up {{ tailscale_args_string | trim }} --authkey={{ tailscale_authkey_string | trim }}" 143 | # Since the auth key is included in this task's output, we do not want to log output 144 | no_log: "{{ not (insecurely_log_authkey | bool) }}" 145 | changed_when: true 146 | register: tailscale_start 147 | # If a failure occurred due to state changes, we still want to log a redacted version of the error if "no_log" is true 148 | ignore_errors: true 149 | when: 150 | - not tailscale_up_skip 151 | - state_file is changed or not tailscale_is_online 152 | notify: Confirm Tailscale is Connected 153 | async: "{{ (tailscale_up_timeout | trim | int) + 10 }}" 154 | poll: 5 155 | 156 | - name: Install | Report non-sensitive stdout from "tailscale up" # noqa: no-handler 157 | ansible.builtin.debug: 158 | msg: "{{ tailscale_start.stdout | replace(tailscale_authkey, 'REDACTED') | regex_replace('\\t', '') | split('\n') }}" 159 | when: 160 | - tailscale_start is failed 161 | - tailscale_start.stdout | length > 0 162 | register: nonsensitive_stdout 163 | 164 | - name: Install | Pausing to highlight stdout message above 165 | ansible.builtin.wait_for: 166 | timeout: 5 167 | when: nonsensitive_stdout is not skipped 168 | 169 | - name: Install | Clear State Upon Error 170 | ansible.builtin.file: 171 | path: "{{ tailscale_state_folder }}/artis3n-tailscale/state" 172 | state: absent 173 | when: 174 | - tailscale_start is failed 175 | 176 | - name: Install | Report redacted failure from "tailscale up" # noqa: no-handler 177 | ansible.builtin.fail: 178 | msg: "{{ tailscale_start.stderr | default () | regex_replace(tailscale_authkey, 'REDACTED') | regex_replace('\\t', '') | split('\n') }}" 179 | when: 180 | - tailscale_start is failed 181 | 182 | - name: Install | Register role facts 183 | ansible.builtin.include_tasks: facts.yml 184 | when: 185 | - not tailscale_up_skip 186 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: State Validation 3 | ansible.builtin.fail: 4 | msg: "'state' must be 'latest', 'present', or 'absent'." 5 | when: 6 | - state != "latest" 7 | - state != "present" 8 | - state != "absent" 9 | 10 | - name: Tailscale Auth Key Required 11 | ansible.builtin.fail: 12 | msg: > 13 | You must include a Node Authorization auth key. 14 | Set a `tailscale_authkey` variable. 15 | You can create this key from: https://login.tailscale.com/admin/settings/keys . 16 | when: 17 | - not tailscale_authkey 18 | - not tailscale_up_skip 19 | - state != "absent" 20 | 21 | - name: Tailscale Tags Required with OAuth Key 22 | ansible.builtin.fail: 23 | msg: "When `tailscale_authkey` is an OAuth key, you must supply one or more tags in `tailscale_tags`." 24 | when: 25 | - tailscale_authkey.startswith('tskey-client-') 26 | - not tailscale_tags 27 | - state != "absent" 28 | - not tailscale_up_skip 29 | 30 | - name: Tailscale timeout verification 31 | ansible.builtin.fail: 32 | msg: "`tailscale_up_timeout` variable should be parseable as an integer." 33 | when: 34 | - tailscale_up_timeout != 0 35 | - (tailscale_up_timeout | type_debug == "bool") or not (tailscale_up_timeout | int) 36 | - state != "absent" 37 | - not tailscale_up_skip 38 | 39 | - name: Use tailscale_tags instead of tailscale_args for tags 40 | ansible.builtin.debug: 41 | msg: You must use `tailscale_tags` instead of `tailscale_args` to assign tags. 42 | when: 43 | - '"--advertise-tags" in tailscale_args' 44 | - not tailscale_tags 45 | - state != "absent" 46 | - not tailscale_up_skip 47 | 48 | - name: Skipping Authentication 49 | ansible.builtin.debug: 50 | msg: You have set 'tailscale_up_skip', so this node will not authenticate to your Tailscale network. 51 | when: tailscale_up_skip 52 | 53 | - name: Unstable Warning 54 | # Print an error message to the console but proceed anyway 55 | ansible.builtin.fail: 56 | msg: > 57 | !!!!! 58 | Installing Tailscale from the unstable branch. 59 | This is bleeding edge and may have issues. 60 | Be warned. 61 | !!!!! 62 | when: release_stability | lower == 'unstable' 63 | failed_when: false 64 | 65 | - name: Prepare lsb_release if missing 66 | # Any system that Ansible can't gather versions from is missing lsb_release 67 | # Only encountered on Debian so far during pre-release "testing" stage 68 | when: tailscale_original_distribution_major_version == "NA" 69 | block: 70 | - name: Install lsb_release 71 | become: true 72 | ansible.builtin.package: 73 | name: lsb-release 74 | cache_valid_time: 3600 75 | state: present 76 | when: ansible_distribution in tailscale_debian_family_distros or ansible_distribution == 'ArchLinux' 77 | 78 | - name: Install lsb_release 79 | become: true 80 | ansible.builtin.package: 81 | name: redhat-lsb-core 82 | state: present 83 | when: ansible_distribution in tailscale_centos_family_distros or ansible_distribution == 'Fedora' 84 | 85 | - name: Refresh Setup 86 | ansible.builtin.setup: 87 | 88 | - name: Operating System 89 | ansible.builtin.debug: 90 | msg: "{{ ansible_distribution }} {{ ansible_distribution_major_version }} ({{ ansible_distribution_release }})" 91 | 92 | - name: Install Tailscale 93 | when: state == "present" or state == "latest" 94 | ansible.builtin.include_tasks: install.yml 95 | 96 | - name: Uninstall Tailscale 97 | when: state == "absent" 98 | ansible.builtin.include_tasks: uninstall.yml 99 | 100 | - name: "Warning: Role migrated into Collection" 101 | ansible.builtin.meta: end_role 102 | delegate_to: localhost 103 | run_once: true 104 | when: "('This standalone role has been migrated into a collection.\n\nPlease review the collection and plan a drop-in migration.\nThis role will eventually be archived. A notice period will be communicated.\n\nhttps://github.com/artis3n/ansible-collection-tailscale' | print_warn())" 105 | -------------------------------------------------------------------------------- /tasks/opensuse/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: OpenSUSE | Install Tumbleweed 3 | when: ansible_distribution == "openSUSE Tumbleweed" 4 | block: 5 | - name: OpenSUSE | Install Tumbleweed Repo Key 6 | become: true 7 | ansible.builtin.rpm_key: 8 | state: present 9 | key: "{{ tailscale_opensuse_tumbleweed_key }}" 10 | 11 | - name: OpenSUSE | Install Tumbleweed Repo 12 | become: true 13 | community.general.zypper_repository: 14 | repo: "{{ tailscale_opensuse_tumbleweed_repository }}" 15 | state: present 16 | 17 | - name: OpenSUSE | Install Leap 18 | when: ansible_distribution == "openSUSE Leap" 19 | block: 20 | - name: OpenSUSE | Install Leap Repo Key 21 | become: true 22 | ansible.builtin.rpm_key: 23 | state: present 24 | key: "{{ tailscale_opensuse_leap_key }}" 25 | 26 | - name: OpenSUSE | Install Leap Repo 27 | become: true 28 | community.general.zypper_repository: 29 | repo: "{{ tailscale_opensuse_leap_repository }}" 30 | state: present 31 | 32 | - name: OpenSUSE | Install Tailscale 33 | become: true 34 | community.general.zypper: 35 | name: tailscale 36 | update_cache: true 37 | state: '{{ state }}' 38 | -------------------------------------------------------------------------------- /tasks/opensuse/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: OpenSUSE | Remove Tailscale 3 | become: true 4 | community.general.zypper: 5 | name: tailscale 6 | update_cache: true 7 | state: absent 8 | key: "{{ tailscale_opensuse_key }}" 9 | 10 | - name: OpenSUSE | Remove Tumbleweed 11 | when: ansible_distribution == "openSUSE Tumbleweed" 12 | block: 13 | - name: OpenSUSE | Remove Tumbleweed Repo Key 14 | become: true 15 | ansible.builtin.rpm_key: 16 | state: absent 17 | key: "{{ tailscale_opensuse_tumbleweed_key }}" 18 | 19 | - name: OpenSUSE | Remove Tumbleweed Repo 20 | become: true 21 | community.general.zypper_repository: 22 | repo: "{{ tailscale_opensuse_tumbleweed_repository }}" 23 | state: absent 24 | 25 | - name: OpenSUSE | Remove Leap 26 | when: ansible_distribution == "openSUSE Leap" 27 | block: 28 | - name: OpenSUSE | Remove Leap Repo Key 29 | become: true 30 | ansible.builtin.rpm_key: 31 | state: absent 32 | key: "{{ tailscale_opensuse_leap_key }}" 33 | 34 | - name: OpenSUSE | Remove Leap Repo 35 | become: true 36 | community.general.zypper_repository: 37 | repo: "{{ tailscale_opensuse_leap_repository }}" 38 | state: absent 39 | -------------------------------------------------------------------------------- /tasks/templates/state.j2: -------------------------------------------------------------------------------- 1 | {%- if auth_key_in_state -%} 2 | {{ (tailscale_args_string + ' --authkey=' + tailscale_authkey) | trim | hash('sha256') }} 3 | {% else %} 4 | {{ tailscale_args_string | trim | hash('sha256') }} 5 | {%- endif -%} 6 | -------------------------------------------------------------------------------- /tasks/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Uninstall | Check If Tailscale Is Connected 3 | ansible.builtin.command: tailscale status 4 | changed_when: false 5 | failed_when: false 6 | register: tailscale_status 7 | 8 | - name: Uninstall | De-register Tailscale Node 9 | become: true 10 | # Hack to get correct changed/ok status 11 | ansible.builtin.shell: tailscale status; tailscale logout 12 | register: tailscale_logout 13 | changed_when: "'Logged out.' not in tailscale_status.stdout and 'not logged in' not in tailscale_status.stdout" 14 | when: 15 | # [Errno 2] No such file or directory: 'tailscale' 16 | - tailscale_status.rc != 2 17 | # "bash: tailscale: command not found" 18 | - tailscale_status.rc != 127 19 | 20 | - name: Uninstall | Delete Tailscale State 21 | ansible.builtin.file: 22 | path: "{{ ansible_env.HOME }}/.artis3n-tailscale" 23 | state: absent 24 | 25 | - name: Uninstall | Gather Service Facts 26 | ansible.builtin.service_facts: 27 | 28 | - name: Uninstall | Disable Tailscale Service 29 | become: true 30 | ansible.builtin.service: 31 | name: "{{ tailscale_service }}" 32 | state: stopped 33 | enabled: false 34 | when: tailscale_service in ansible_facts.services 35 | 36 | - name: Uninstall | CentOS and related families 37 | when: > 38 | ansible_distribution in tailscale_centos_family_distros 39 | ansible.builtin.include_tasks: centos/uninstall.yml 40 | 41 | - name: Uninstall | Amazon Linux 2 42 | when: 43 | - ansible_distribution == 'Amazon' 44 | - ansible_distribution_major_version | int < 2023 45 | ansible.builtin.include_tasks: centos/uninstall-legacy.yml 46 | 47 | - name: Uninstall | Debian and related families 48 | when: ansible_distribution in tailscale_debian_family_distros 49 | ansible.builtin.include_tasks: debian/uninstall.yml 50 | 51 | - name: Uninstall | Fedora and related families 52 | when: > 53 | ansible_distribution == 'Fedora' 54 | or (ansible_distribution == 'Amazon' and ansible_distribution_major_version | int >= 2023) 55 | ansible.builtin.include_tasks: fedora/uninstall.yml 56 | 57 | - name: Uninstall | Arch 58 | when: ansible_distribution == 'Archlinux' 59 | ansible.builtin.include_tasks: arch/uninstall.yml 60 | 61 | - name: Uninstall | OpenSUSE 62 | when: ansible_distribution in tailscale_opensuse_family_distros 63 | ansible.builtin.include_tasks: opensuse/uninstall.yml 64 | 65 | - name: Uninstall | Remove Tailscale Daemon State and Logs 66 | become: true 67 | ansible.builtin.file: 68 | path: "/var/lib/tailscale" 69 | state: absent 70 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Variables that an end user should likely never modify. 3 | 4 | tailscale_debian_family_distros: 5 | - Ubuntu 6 | - Debian 7 | - Pop!_OS 8 | - OSMC 9 | - "Linux Mint" 10 | 11 | tailscale_centos_family_distros: 12 | - CentOS 13 | - OracleLinux 14 | - RedHat 15 | - Rocky 16 | - AlmaLinux 17 | 18 | tailscale_opensuse_family_distros: 19 | - "openSUSE Leap" 20 | - "openSUSE Tumbleweed" 21 | 22 | tailscale_package: tailscale 23 | tailscale_service: tailscaled 24 | 25 | tailscale_apt_dependencies: 26 | - gnupg 27 | - gnupg-agent 28 | - apt-transport-https 29 | - python3-apt 30 | 31 | tailscale_legacy_apt_dependencies: 32 | # Only install on legacy Debian systems 33 | - python-apt 34 | 35 | tailscale_debian_distro: 36 | pop!_os: ubuntu 37 | ubuntu: ubuntu 38 | debian: debian 39 | osmc: debian 40 | "linux mint": ubuntu 41 | 42 | tailscale_apt_keyring_path: /usr/share/keyrings/tailscale-archive-keyring.gpg 43 | 44 | tailscale_apt_deb: deb [signed-by={{ tailscale_apt_keyring_path }}] https://pkgs.tailscale.com/{{ release_stability | lower }}/{{ tailscale_debian_distro[ansible_distribution | lower] }} {{ apt_codename | lower }} main 45 | 46 | tailscale_apt_signkey: https://pkgs.tailscale.com/{{ release_stability | lower }}/{{ tailscale_debian_distro[ansible_distribution | lower] }}/{{ apt_codename | lower }}.noarmor.gpg 47 | 48 | tailscale_yum_dependencies: 49 | - yum-utils 50 | - gnupg 51 | 52 | tailscale_yum_repos: 53 | Amazon: https://pkgs.tailscale.com/{{ release_stability | lower }}/amazon-linux/{{ ansible_distribution_major_version }}/tailscale.repo 54 | CentOS: https://pkgs.tailscale.com/{{ release_stability | lower }}/centos/{{ ansible_distribution_major_version }}/tailscale.repo 55 | RedHat: https://pkgs.tailscale.com/{{ release_stability | lower }}/{{ "rhel" if ansible_distribution_major_version | int == 8 else "centos" }}/{{ ansible_distribution_major_version }}/tailscale.repo 56 | OracleLinux: https://pkgs.tailscale.com/{{ release_stability | lower }}/centos/{{ ansible_distribution_major_version }}/tailscale.repo 57 | Rocky: https://pkgs.tailscale.com/{{ release_stability | lower }}/centos/{{ ansible_distribution_major_version }}/tailscale.repo 58 | AlmaLinux: https://pkgs.tailscale.com/{{ release_stability | lower }}/centos/{{ ansible_distribution_major_version }}/tailscale.repo 59 | 60 | tailscale_dnf_dependencies: 61 | - python-dnf 62 | 63 | tailscale_dnf_plugins: 64 | - dnf-plugins-core 65 | 66 | tailscale_dnf_repos: 67 | Amazon: https://pkgs.tailscale.com/{{ release_stability | lower }}/fedora/tailscale.repo 68 | Fedora: https://pkgs.tailscale.com/{{ release_stability | lower }}/fedora/tailscale.repo 69 | 70 | tailscale_opensuse_leap_key: https://pkgs.tailscale.com/{{ release_stability | lower }}/opensuse/leap/{{ ansible_distribution_version }}/repo.gpg 71 | tailscale_opensuse_leap_repository: https://pkgs.tailscale.com/stable/opensuse/leap/{{ ansible_distribution_version }}/tailscale.repo 72 | tailscale_opensuse_tumbleweed_key: https://pkgs.tailscale.com/{{ release_stability | lower }}/opensuse/tumbleweed/repo.gpg 73 | tailscale_opensuse_tumbleweed_repository: https://pkgs.tailscale.com/{{ release_stability | lower }}/opensuse/tumbleweed/tailscale.repo 74 | 75 | tailscale_original_distribution_major_version: "{{ ansible_distribution_major_version }}" 76 | --------------------------------------------------------------------------------