From 96f5cea091ea629067a4010640d0a9d6d297a7cd Mon Sep 17 00:00:00 2001 From: Julian Foad Date: Mon, 29 Aug 2022 12:42:43 +0100 Subject: [PATCH] initial version, under construction --- defaults/main.yml | 3 + setup-headscale.yml | 52 +++++++ tasks/main.yml | 62 ++++++++ templates/config-example.yaml | 268 ++++++++++++++++++++++++++++++++++ templates/config.yaml.j2 | 69 +++++++++ 5 files changed, 454 insertions(+) create mode 100644 defaults/main.yml create mode 100644 setup-headscale.yml create mode 100644 tasks/main.yml create mode 100644 templates/config-example.yaml create mode 100644 templates/config.yaml.j2 diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..9f14c3f --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,3 @@ +--- +headscale_dir: /home/headscale +headscale_domain: ~ diff --git a/setup-headscale.yml b/setup-headscale.yml new file mode 100644 index 0000000..68828ad --- /dev/null +++ b/setup-headscale.yml @@ -0,0 +1,52 @@ +--- +# This is an Ansible playbook example to set up Headscale +# (for a self-managed tailscale deployment) +# +# Includes: +# +# - headscale (control-plane server) +# - headscale-ui (web UI for managing headscale) +# - labels to configure Traefik-2 reverse-proxy +# - ### currently exposes the ports for local connections, for debugging +# +# Configuration: +# +# - `templates/config.yaml.j2`: +# config used by this role, with customisations and variables +# +# - `templates/config-example.yaml`: +# a copy of upstream config example, with documentation +# +# Usage: +# +# - set up by running your playbook +# (add `-e pull=yes` to update to the latest docker images) +# - tell your tailscale clients to connect to: +# - access headscale admin web page at: + +# The following is an example of how a playbook can import the role: + +- name: "Set up headscale" + hosts: example.org + vars: + pull: false + become: yes + + tasks: + + - import_role: name=headscale + # example of tags you may wish to use for this task + tags: headscale + # the following variable must be defined to use the role + vars: + # the (sub)domain of this server + # (your reverse-proxy will route it) + # (use just a (sub)domain; a URL path part is NOT currently supported) + headscale_domain: headscale.example.org + # where to store the config file and the database on your server + headscale_dir: /home/headscale + # namespaces ("tailnets") to create (and/or you can create them manually) + headscale_namespaces: + - my_tailnet_name + # whether to update to the latest docker images + headscale_pull: "{{ pull }}" diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..0cfe743 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,62 @@ +--- + +- name: dirs + file: state=directory path="{{ item }}" # mode= o= g= + loop: + - "{{ headscale_dir }}" + - "{{ headscale_dir }}/config" + - "{{ headscale_dir }}/data" + +- name: config file + template: src="config.yaml.j2" dest="{{ headscale_dir }}/config/config.yaml" # mode= o= g= + +# create an empty database file if it doesn't exist +- name: database file + copy: force="no" content="" dest="{{ headscale_dir }}/data/db.sqlite" # mode= o= g= + +- name: headscale coordination server + docker_container: + name: headscale + image: headscale/headscale:latest-alpine + pull: "{{ headscale_pull }}" + command: headscale serve + restart_policy: unless-stopped + volumes: + - "{{ headscale_dir }}/config:/etc/headscale:ro" + - "{{ headscale_dir }}/data:/var/lib/headscale" + labels: + traefik.enable: "true" + traefik.http.routers.headscale.entryPoints: "web_https" + traefik.http.routers.headscale.rule: "Host(`{{ headscale_domain }}`)" + traefik.http.routers.headscale.middlewares: "chain-authelia@file" + published_ports: ### local testing + - "9441:8080" + - "9442:9090" # /metrics + - "50443:50443" # /headscale grpc API + exposed_ports: + - "50443" + +# (If a given namespace already exists, the command issues an error +# message but the exit code is 0 (successful).) +- name: namespaces aka tailnets + community.docker.docker_container_exec: + container: headscale + argv: ['headscale', 'namespaces', 'create', "{{ item }}"] + register: result + changed_when: "'Namespace created' in result.stdout_lines" + loop: "{{ headscale_namespaces }}" + +# https://github.com/gurucomputing/headscale-ui +- name: headscale UI + docker_container: + name: headscale-ui + image: ghcr.io/gurucomputing/headscale-ui:latest + pull: "{{ headscale_pull }}" + restart_policy: unless-stopped + labels: + traefik.enable: "true" + traefik.http.routers.headscale.entryPoints: "web_https" + traefik.http.routers.headscale.rule: "Host(`{{ headscale_domain }}`) && PathPrefix(`/web`)" + traefik.http.routers.headscale.middlewares: "chain-authelia@file" + exposed_ports: + - "80" diff --git a/templates/config-example.yaml b/templates/config-example.yaml new file mode 100644 index 0000000..2019a13 --- /dev/null +++ b/templates/config-example.yaml @@ -0,0 +1,268 @@ +--- +# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: +# +# - `/etc/headscale` +# - `~/.headscale` +# - current working directory + +# The url clients will connect to. +# Typically this will be a domain like: +# +# https://myheadscale.example.com:443 +# +server_url: http://127.0.0.1:8080 + +# Address to listen to / bind to on the server +# +listen_addr: 0.0.0.0:8080 + +# Address to listen to /metrics, you may want +# to keep this endpoint private to your internal +# network +# +metrics_listen_addr: 127.0.0.1:9090 + +# Address to listen for gRPC. +# gRPC is used for controlling a headscale server +# remotely with the CLI +# Note: Remote access _only_ works if you have +# valid certificates. +grpc_listen_addr: 0.0.0.0:50443 + +# Allow the gRPC admin interface to run in INSECURE +# mode. This is not recommended as the traffic will +# be unencrypted. Only enable if you know what you +# are doing. +grpc_allow_insecure: false + +# Private key used encrypt the traffic between headscale +# and Tailscale clients. +# The private key file which will be +# autogenerated if it's missing +private_key_path: /var/lib/headscale/private.key + +# The Noise section includes specific configuration for the +# TS2021 Noise procotol +noise: + # The Noise private key is used to encrypt the + # traffic between headscale and Tailscale clients when + # using the new Noise-based protocol. It must be different + # from the legacy private key. + private_key_path: /var/lib/headscale/noise_private.key + +# List of IP prefixes to allocate tailaddresses from. +# Each prefix consists of either an IPv4 or IPv6 address, +# and the associated prefix length, delimited by a slash. +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 + +# DERP is a relay system that Tailscale uses when a direct +# connection cannot be established. +# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp +# +# headscale needs a list of DERP servers that can be presented +# to the clients. +derp: + server: + # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config + # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place + enabled: false + + # Region ID to use for the embedded DERP server. + # The local DERP prevails if the region ID collides with other region ID coming from + # the regular DERP config. + region_id: 999 + + # Region code and name are displayed in the Tailscale UI to identify a DERP region + region_code: "headscale" + region_name: "Headscale Embedded DERP" + + # Listens in UDP at the configured address for STUN connections to help on NAT traversal. + # When the embedded DERP server is enabled stun_listen_addr MUST be defined. + # + # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ + stun_listen_addr: "0.0.0.0:3478" + + # List of externally available DERP maps encoded in JSON + urls: + - https://controlplane.tailscale.com/derpmap/default + + # Locally available DERP map files encoded in YAML + # + # This option is mostly interesting for people hosting + # their own DERP servers: + # https://tailscale.com/kb/1118/custom-derp-servers/ + # + # paths: + # - /etc/headscale/derp-example.yaml + paths: [] + + # If enabled, a worker will be set up to periodically + # refresh the given sources and update the derpmap + # will be set up. + auto_update_enabled: true + + # How often should we check for DERP updates? + update_frequency: 24h + +# Disables the automatic check for headscale updates on startup +disable_check_updates: false + +# Time before an inactive ephemeral node is deleted? +ephemeral_node_inactivity_timeout: 30m + +# Period to check for node updates in the tailnet. A value too low will severily affect +# CPU consumption of Headscale. A value too high (over 60s) will cause problems +# to the nodes, as they won't get updates or keep alive messages in time. +# In case of doubts, do not touch the default 10s. +node_update_check_interval: 10s + +# SQLite config +db_type: sqlite3 +db_path: /var/lib/headscale/db.sqlite + +# # Postgres config +# If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. +# db_type: postgres +# db_host: localhost +# db_port: 5432 +# db_name: headscale +# db_user: foo +# db_pass: bar +# db_ssl: false + +### TLS configuration +# +## Let's encrypt / ACME +# +# headscale supports automatically requesting and setting up +# TLS for a domain with Let's Encrypt. +# +# URL to ACME directory +acme_url: https://acme-v02.api.letsencrypt.org/directory + +# Email to register with ACME provider +acme_email: "" + +# Domain name to request a TLS certificate for: +tls_letsencrypt_hostname: "" + +# Client (Tailscale/Browser) authentication mode (mTLS) +# Acceptable values: +# - disabled: client authentication disabled +# - relaxed: client certificate is required but not verified +# - enforced: client certificate is required and verified +tls_client_auth_mode: relaxed + +# Path to store certificates and metadata needed by +# letsencrypt +tls_letsencrypt_cache_dir: /var/lib/headscale/cache + +# Type of ACME challenge to use, currently supported types: +# HTTP-01 or TLS-ALPN-01 +# See [docs/tls.md](docs/tls.md) for more information +tls_letsencrypt_challenge_type: HTTP-01 +# When HTTP-01 challenge is chosen, letsencrypt must set up a +# verification endpoint, and it will be listning on: +# :http = port 80 +tls_letsencrypt_listen: ":http" + +## Use already defined certificates: +tls_cert_path: "" +tls_key_path: "" + +log_level: info + +# Path to a file containg ACL policies. +# ACLs can be defined as YAML or HUJSON. +# https://tailscale.com/kb/1018/acls/ +acl_policy_path: "" + +## DNS +# +# headscale supports Tailscale's DNS configuration and MagicDNS. +# Please have a look to their KB to better understand the concepts: +# +# - https://tailscale.com/kb/1054/dns/ +# - https://tailscale.com/kb/1081/magicdns/ +# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ +# +dns_config: + # List of DNS servers to expose to clients. + nameservers: + - 1.1.1.1 + + # Split DNS (see https://tailscale.com/kb/1054/dns/), + # list of search domains and the DNS to query for each one. + # + # restricted_nameservers: + # foo.bar.com: + # - 1.1.1.1 + # darp.headscale.net: + # - 1.1.1.1 + # - 8.8.8.8 + + # Search domains to inject. + domains: [] + + # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). + # Only works if there is at least a nameserver defined. + magic_dns: true + + # Defines the base domain to create the hostnames for MagicDNS. + # `base_domain` must be a FQDNs, without the trailing dot. + # The FQDN of the hosts will be + # `hostname.namespace.base_domain` (e.g., _myhost.mynamespace.example.com_). + base_domain: example.com + +# Unix socket used for the CLI to connect without authentication +# Note: for local development, you probably want to change this to: +# unix_socket: ./headscale.sock +unix_socket: /var/run/headscale.sock +unix_socket_permission: "0770" +# +# headscale supports experimental OpenID connect support, +# it is still being tested and might have some bugs, please +# help us test it. +# OpenID Connect +# oidc: +# issuer: "https://your-oidc.issuer.com/path" +# client_id: "your-oidc-client-id" +# client_secret: "your-oidc-client-secret" +# +# Customize the scopes used in the OIDC flow, defaults to "openid", "profile" and "email" and add custom query +# parameters to the Authorize Endpoint request. Scopes default to "openid", "profile" and "email". +# +# scope: ["openid", "profile", "email", "custom"] +# extra_params: +# domain_hint: example.com +# +# List allowed principal domains and/or users. If an authenticated user's domain is not in this list, the +# authentication request will be rejected. +# +# allowed_domains: +# - example.com +# allowed_users: +# - alice@example.com +# +# If `strip_email_domain` is set to `true`, the domain part of the username email address will be removed. +# This will transform `first-name.last-name@example.com` to the namespace `first-name.last-name` +# If `strip_email_domain` is set to `false` the domain part will NOT be removed resulting to the following +# namespace: `first-name.last-name.example.com` +# +# strip_email_domain: true + +# Logtail configuration +# Logtail is Tailscales logging and auditing infrastructure, it allows the control panel +# to instruct tailscale nodes to log their activity to a remote server. +logtail: + # Enable logtail for this headscales clients. + # As there is currently no support for overriding the log server in headscale, this is + # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. + enabled: false + +# Enabling this option makes devices prefer a random port for WireGuard traffic over the +# default static port 41641. This option is intended as a workaround for some buggy +# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. +randomize_client_port: false diff --git a/templates/config.yaml.j2 b/templates/config.yaml.j2 new file mode 100644 index 0000000..c99dc68 --- /dev/null +++ b/templates/config.yaml.j2 @@ -0,0 +1,69 @@ +--- +# see: https://github.com/juanfont/headscale/blob/main/docs/running-headscale-container.md + +server_url: "https://{{ headscale_domain }}" +listen_addr: 0.0.0.0:8080 +metrics_listen_addr: 0.0.0.0:9090 + +grpc_listen_addr: 0.0.0.0:50443 +grpc_allow_insecure: false + +private_key_path: /var/lib/headscale/private.key + +noise: + private_key_path: /var/lib/headscale/noise_private.key + +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 + +derp: + server: + enabled: false + region_id: 999 + region_code: "headscale" + region_name: "Headscale Embedded DERP" + stun_listen_addr: "0.0.0.0:3478" + urls: + - https://controlplane.tailscale.com/derpmap/default + paths: [] + auto_update_enabled: true + update_frequency: 24h + +disable_check_updates: false + +ephemeral_node_inactivity_timeout: 30m + +node_update_check_interval: 10s + +db_type: sqlite3 +db_path: /var/lib/headscale/db.sqlite + +acme_url: https://acme-v02.api.letsencrypt.org/directory +acme_email: "" +tls_letsencrypt_hostname: "" +tls_client_auth_mode: relaxed +tls_letsencrypt_cache_dir: /var/lib/headscale/cache +tls_letsencrypt_challenge_type: HTTP-01 +tls_letsencrypt_listen: ":http" +tls_cert_path: "" +tls_key_path: "" + +log_level: info + +acl_policy_path: "" + +dns_config: + nameservers: + - 1.1.1.1 + domains: [] + magic_dns: true + base_domain: "{{ headscale_domain }}" + +unix_socket: /var/run/headscale.sock +unix_socket_permission: "0770" + +logtail: + enabled: false + +randomize_client_port: false