From 172ed73357eeaed998e56e16aeac43101f917251 Mon Sep 17 00:00:00 2001 From: Max Kramarenko <32900310+McSim85@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:30:19 +0100 Subject: [PATCH] [FEAT] add OAuth key integration and additional URL-style params to --auth-key flags (ephemeral & preauthorized) (#399) * Add auth-key additional params * fix bool * fix identation * add tailscale_authkey_sting, more docs * add key info * fix headscale case and README * make md linter happy * Update README.md Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Update defaults/main.yml Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Update README.md Co-authored-by: Ari Kalfus * Apply suggestions from code review Co-authored-by: Ari Kalfus * molecule fix * fix linter --------- Co-authored-by: Ari Kalfus --- .ansible-lint | 3 +- README.md | 30 +++++++++++++++-- defaults/main.yml | 6 ++++ molecule/oauth/cleanup.yml | 8 +++++ molecule/oauth/converge.yml | 12 +++++++ molecule/oauth/headscale.config.yaml | 34 +++++++++++++++++++ molecule/oauth/init_tailscale_vars.yml | 22 ++++++++++++ molecule/oauth/molecule.yml | 46 ++++++++++++++++++++++++++ molecule/oauth/prepare.yml | 26 +++++++++++++++ molecule/oauth/verify.yml | 15 +++++++++ tasks/install.yml | 15 ++++++++- 11 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 molecule/oauth/cleanup.yml create mode 100644 molecule/oauth/converge.yml create mode 100644 molecule/oauth/headscale.config.yaml create mode 100644 molecule/oauth/init_tailscale_vars.yml create mode 100644 molecule/oauth/molecule.yml create mode 100644 molecule/oauth/prepare.yml create mode 100644 molecule/oauth/verify.yml diff --git a/.ansible-lint b/.ansible-lint index f7f5123d..5b1bd2d0 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -5,7 +5,8 @@ exclude_paths: - .vscode/ - molecule/default/headscale.config.yaml - molecule/default/init_tailscale_vars.yml - + - molecule/oauth/headscale.config.yaml + - molecule/oauth/init_tailscale_vars.yml skip_list: - yaml[line-length] diff --git a/README.md b/README.md index 3896a9f9..fd9109cc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This role installs and configures [Tailscale][] on a Linux target. Supported operating systems: + - Debian / Ubuntu - CentOS / RedHat - Rocky Linux / AlmaLinux @@ -76,11 +77,35 @@ Is **not** required if `tailscale_up_skip` is set to `true`. A Tailscale Node Authorization auth key. -A Node Authorization auth key can be generated under your Tailscale account at . -Note that reusable authorization keys now expire 90 days after they are generated. +A Node Authorization key can be generated under your Tailscale account. The role supports two type of keys: + +- Auth key (`tskey-auth-XXX-YYYYY`) +- OAuth key (`tskey-client-XXX-YYYY`) + +Note that auth keys expire up to a maximum of 90 days after they are generated. OAuth secrets do not expire unless revoked, and the generated OAuth access token expires after 1 hour. + +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). This value should be treated as a sensitive secret. +### tailscale_oauth_ephemeral + +> [!NOTE] +> Used only when `tailscale_authkey` is an OAuth key. + +**Default**: `true` + +Register as an [ephemeral node](https://tailscale.com/kb/1111/ephemeral-nodes), if `true`. + +### tailscale_oauth_preauthorized + +> [!NOTE] +> Used only when `tailscale_authkey` is an OAuth key. + +**Default**: `false` + +Skip [manual device approval](https://tailscale.com/kb/1099/device-approval), if `true`. + ### tailscale_up_skip **If set to true, `tailscale_authkey` is not required.** @@ -271,6 +296,5 @@ USE_HEADSCALE=true molecule test [ephemeral auth keys]: https://tailscale.com/kb/1111/ephemeral-nodes/ [github action secret]: https://docs.github.com/en/actions/reference/encrypted-secrets [tailscale]: https://tailscale.com/ -[tailscale account]: https://login.tailscale.com/start [tailscale up docs]: https://tailscale.com/kb/1080/cli/#up [headscale]: https://github.com/juanfont/headscale/ diff --git a/defaults/main.yml b/defaults/main.yml index efa4de4e..a75353f1 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -7,6 +7,12 @@ state: latest tailscale_authkey: "" # Optional command-line arguments for 'tailscale up' tailscale_args: "" +# Used for OAuth authentication +# Register as an ephemeral node (recommended) +tailscale_oauth_ephemeral: true +# Used for OAuth authentication +# Skip manual device approval +tailscale_oauth_preauthorized: false # Whether to output debug information during role execution verbose: false # Whether to skip 'tailscale up' diff --git a/molecule/oauth/cleanup.yml b/molecule/oauth/cleanup.yml new file mode 100644 index 00000000..dba10450 --- /dev/null +++ b/molecule/oauth/cleanup.yml @@ -0,0 +1,8 @@ +--- +- name: Cleanup + hosts: instance + tasks: + - name: De-register Tailscale node + become: true + ansible.builtin.command: tailscale logout + changed_when: false diff --git a/molecule/oauth/converge.yml b/molecule/oauth/converge.yml new file mode 100644 index 00000000..ed95700e --- /dev/null +++ b/molecule/oauth/converge.yml @@ -0,0 +1,12 @@ +--- +- name: Converge + hosts: instance + tasks: + - name: Init tailscale credentials variables + ansible.builtin.include_tasks: init_tailscale_vars.yml + + - name: "Include artis3n.tailscale" + ansible.builtin.include_role: + name: artis3n.tailscale + vars: + verbose: true diff --git a/molecule/oauth/headscale.config.yaml b/molecule/oauth/headscale.config.yaml new file mode 100644 index 00000000..7cef9a5c --- /dev/null +++ b/molecule/oauth/headscale.config.yaml @@ -0,0 +1,34 @@ +# minimal Headscale configuration for local testing +# See upstream example file for full description of all options: +# https://github.com/juanfont/headscale/blob/main/config-example.yaml +server_url: http://headscale:8080 +listen_addr: 0.0.0.0:8080 +metrics_listen_addr: 0.0.0.0:9090 +private_key_path: /etc/headscale/private.key +noise: + private_key_path: /etc/headscale/noise_private.key +db_type: sqlite3 +db_path: /etc/headscale/db.sqlite + +# Default Tailscale prefixes +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 + +# Disable TLS +tls_cert_path: "" +tls_key_path: "" + +# Add DNS configuration so we can --accept-dns +dns_config: + override_local_dns: true + nameservers: + - 1.1.1.1 + +derp: + server: + enabled: true + region_id: 999 + region_code: "headscale" + region_name: "Headscale Embedded DERP" + stun_listen_addr: "0.0.0.0:3478" diff --git a/molecule/oauth/init_tailscale_vars.yml b/molecule/oauth/init_tailscale_vars.yml new file mode 100644 index 00000000..498bd4bd --- /dev/null +++ b/molecule/oauth/init_tailscale_vars.yml @@ -0,0 +1,22 @@ +--- +- name: Use tailscale service + ansible.builtin.set_fact: + tailscale_authkey: "{{ lookup('ansible.builtin.env', 'TAILSCALE_OAUTH_CLIENT_SECRET') }}" + when: not lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) + +- name: Fetch headscale preauth key + delegate_to: localhost + changed_when: false + community.docker.docker_container_exec: + container: headscale + command: headscale preauthkeys list -u test -o json + register: preauth_list + when: lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) + +- name: Use headscale service + vars: + combined_args: "{{ tailscale_args|default('') }} --login-server=http://headscale:8080" + ansible.builtin.set_fact: + tailscale_authkey: "{{ (preauth_list.stdout|from_json)[0].key }}" + tailscale_args: "{{ combined_args }}" + when: lookup('ansible.builtin.env', 'USE_HEADSCALE', default=false) diff --git a/molecule/oauth/molecule.yml b/molecule/oauth/molecule.yml new file mode 100644 index 00000000..9d505403 --- /dev/null +++ b/molecule/oauth/molecule.yml @@ -0,0 +1,46 @@ +--- +dependency: + name: galaxy + options: + requirements-file: requirements.yml +driver: + name: docker +platforms: + - name: instance + image: ${MOLECULE_DISTRO:-geerlingguy/docker-ubuntu2204-ansible:latest} + command: ${MOLECULE_COMMAND:-/usr/sbin/init} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + docker_networks: + - name: headscale + networks: + - name: bridge + - name: headscale + cgroupns_mode: host + privileged: true + pre_build_image: true + - name: headscale + image: ${HEADSCALE_IMAGE:-headscale/headscale:latest} + command: headscale serve + pre_build_image: true + networks: + - name: headscale + volumes: + - "${MOLECULE_PROJECT_DIRECTORY}/molecule/default/headscale.config.yaml:/etc/headscale/config.yaml" +provisioner: + name: ansible +verifier: + name: ansible +scenario: + name: oauth + test_sequence: + - dependency + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - verify + - cleanup + - destroy diff --git a/molecule/oauth/prepare.yml b/molecule/oauth/prepare.yml new file mode 100644 index 00000000..5ae2d73c --- /dev/null +++ b/molecule/oauth/prepare.yml @@ -0,0 +1,26 @@ +--- +- name: Prepare + hosts: localhost + gather_facts: false + tasks: + - name: Create Headscale user + community.docker.docker_container_exec: + container: headscale + command: headscale users create test + + - name: Create preauth key + community.docker.docker_container_exec: + container: headscale + command: headscale preauthkeys create -u test --reusable + + - name: Fetch Headscale container info + community.docker.docker_container_info: + name: headscale + register: headscale_info + + - name: Set hosts override for Headscale + delegate_to: instance + ansible.builtin.lineinfile: + path: /etc/hosts + line: "{{ headscale_info.container.NetworkSettings.Networks.headscale.IPAddress }} headscale" + unsafe_writes: true # Hosts file in the docker container can't be written to atomically diff --git a/molecule/oauth/verify.yml b/molecule/oauth/verify.yml new file mode 100644 index 00000000..73edd6b4 --- /dev/null +++ b/molecule/oauth/verify.yml @@ -0,0 +1,15 @@ +--- +- name: Verify + hosts: instance + tasks: + - name: Get Tailscale status + become: true + ansible.builtin.command: tailscale status + changed_when: false + register: tailscale_status + + - name: Assertions + ansible.builtin.assert: + that: + - "'Logged out.' not in tailscale_status.stdout" + - "'not logged in' not in tailscale_status.stdout" diff --git a/tasks/install.yml b/tasks/install.yml index bda1003d..3ccf841f 100644 --- a/tasks/install.yml +++ b/tasks/install.yml @@ -85,9 +85,22 @@ mode: '0644' register: state_file +# OAuth key starts with 'tskey-client-', auth key starts with 'tskey-auth-', with headscale it can be 'unused' +- name: Install | Create authkey string + ansible.builtin.set_fact: + tailscale_authkey_sting: >- + {# Check if the key is an OAuth key #} + {% if tailscale_authkey.startswith('tskey-client-') %} + {{ tailscale_authkey }}?ephemeral={{ tailscale_oauth_ephemeral | bool }}&preauthorized={{ tailscale_oauth_preauthorized | bool }} + {# Check if the key is not OAuth (regular authkey or unused) #} + {% else %} + {{ tailscale_authkey }} + {% endif %} + no_log: "{{ not (insecurely_log_authkey | bool) }}" + - name: Install | Bring Tailscale Up become: true - ansible.builtin.command: "tailscale up {{ tailscale_args | trim }} --authkey={{ tailscale_authkey }}" + ansible.builtin.command: "tailscale up {{ tailscale_args | trim }} --authkey='{{ tailscale_authkey_sting | trim }}'" # Since the auth key is included in this task's output, we do not want to log output no_log: "{{ not (insecurely_log_authkey | bool) }}" changed_when: true