Getting started

The debops.tinc role by itself only defines the connections between hosts, posing as Ethernet tunnels. To make a proper network, you need a defined subnet and a way to assign IP addresses to hosts in the network. You can use the debops.ifupdown role to define an internal subnet for the hosts in the VPN, and debops.dnsmasq to provide DHCP and DNS services inside the network.

Example inventory

Hosts added to the [debops_service_tinc] inventory group will have the tinc daemon installed and configured.

Here is a example inventory that defines a Tinc mesh0 VPN with one of the hosts acting as the DHCP/DNS server and optionally a gateway:

[debops_service_ifupdown]
gateway

[debops_service_dnsmasq]
gateway

[debops_service_tinc]
gateway
hostname1
hostname2

If you don't want the hosts to be included by default in any Tinc mesh networks, you can put them in the [debops_service_tinc_aux] inventory group instead.

The gateway needs some additional configuration which should be placed in the Ansible inventory of the host:

tinc__host_networks:
  'mesh0':
    bridge: 'br2'

ifupdown__host_interfaces:
  'br2':
    type: 'bridge'
    inet: 'static'
    inet6: 'static'
    nat: True
    addresses:
      - '2001:db8::23/64'
      - '192.0.2.23/24'

By default Tinc configures host configuration to contain the primary FQDN address of a given host, so that when its IP address changes, Tinc will query the DNS to get the current IP address. In addition, all publicly routable IP addresses will be added to the host configuration file as well.

However, the FQDN will only be added, if a given host has a publicly routable IP address. This means that hosts without public IPs won't have their addresses mentioned in their host configuration file. This allows these hosts to still connect to a public gateway with access to the Tinc network.

If you want to test the Tinc VPN only on a private network, or allow VPN connections between hosts, you can tell the debops.tinc role to add the private IP addresses of the hosts to their host configuration files by adding in the Ansible inventory:

tinc__host_addresses: '{{ tinc__host_addresses_fqdn +
                          tinc__host_addresses_ip_public +
                          tinc__host_addresses_ip_private }}'

Example playbook

The debops.tinc role uses other roles present in the DebOps project to configure certain aspects of a host, like firewall, installation of the tinc package from Debian Backports repository on certain OS releases, and so on. To do that, a special Ansible playbook is used to access other roles. A "mini role" called debops.tinc/env is used to pass variable data generated from templates to other roles.

If you are using this role without DebOps, here's an example Ansible playbook that uses the debops.tinc role:

---

- name: Configure Tinc VPN
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_service_tinc', 'debops_service_tinc_aux' ]
  become: True

  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'

  pre_tasks:

    - name: Prepare tinc environment
      import_role:
        name: 'tinc'
        tasks_from: 'main_env'
      tags: [ 'role::tinc', 'role::tinc:secret', 'role::secret', 'role::ferm' ]

  roles:

    - role: secret
      tags: [ 'role::secret', 'role::tinc:secret' ]
      secret__directories: '{{ tinc__env_secret__directories }}'

    - role: apt_preferences
      tags: [ 'role::apt_preferences', 'skip::apt_preferences' ]
      apt_preferences__dependent_list: '{{ tinc__apt_preferences__dependent_list }}'

    - role: etc_services
      tags: [ 'role::etc_services', 'skip::etc_services' ]
      etc_services__dependent_list: '{{ tinc__env_etc_services__dependent_list }}'

    - role: ferm
      tags: [ 'role::ferm', 'skip::ferm' ]
      ferm__dependent_rules: '{{ tinc__env_ferm__dependent_rules }}'

    - role: tinc
      tags: [ 'role::tinc', 'skip::tinc' ]

If you are using this role without DebOps, here's an example Ansible playbook that uses debops.tinc together with the debops.persistent_paths:

---

- name: Configure Tinc VPN and ensure persistence
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_service_tinc_persistent_paths', 'debops_service_tinc_aux' ]
  become: True

  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'

  pre_tasks:

    - name: Prepare tinc environment
      import_role:
        name: 'tinc'
        tasks_from: 'main_env'
      tags: [ 'role::tinc', 'role::tinc:secret', 'role::secret', 'role::ferm' ]

  roles:

    - role: secret
      tags: [ 'role::secret', 'role::tinc:secret' ]
      secret__directories: '{{ tinc__env_secret__directories }}'

    - role: apt_preferences
      tags: [ 'role::apt_preferences', 'skip::apt_preferences' ]
      apt_preferences__dependent_list: '{{ tinc__apt_preferences__dependent_list }}'

    - role: etc_services
      tags: [ 'role::etc_services', 'skip::etc_services' ]
      etc_services__dependent_list: '{{ tinc__env_etc_services__dependent_list }}'

    - role: ferm
      tags: [ 'role::ferm', 'skip::ferm' ]
      ferm__dependent_rules: '{{ tinc__env_ferm__dependent_rules }}'

    - role: tinc
      tags: [ 'role::tinc', 'skip::tinc' ]

    - role: persistent_paths
      tags: [ 'role::persistent_paths', 'skip::persistent_paths' ]
      persistent_paths__dependent_paths: '{{ tinc__persistent_paths__dependent_paths }}'

Static vs DHCP connection type

By default, the debops.tinc role configures a node to start its VPN interface in a "DHCP" mode, without connecting to any other bridge interface, and ask the mesh network for an IP address.

To have properly configured networking in the mesh, you need to configure at least one VPN host to work in a "static" mode and preferably connect it to a bridge which connects to a network with DHCP/DNS server. If the bridge parameter is specified without the link_type, role will assume that the host should be configured as static and enable this automatically.

Example network configuration:

tinc__host_networks:
  'mesh0':
    link_type: 'static'
    bridge: 'br2'

In this mode, hosts will be configured to start their VPN interface with a dummy 0.0.0.0 IP address and connect it to a specified bridge. This bridge can be created by the debops.ifupdown.

In "static" mode, the VPN interface will act as another layer 2 connection on the bridge and DHCP requests from the VPN will be passed along to a suitable server. You can configure a DHCP/DNS server using debops.dnsmasq.

Host configuration exchange

The debops.tinc role uses directories created in the secret/tinc/ directory on the Ansible Controller to exchange host configuration files which contain the RSA public keys between hosts in a given VPN. Each network has its own directory tree:

secret/tinc/
└── networks/
    └── mesh0/
        ├── by-group/
        │   ├── all/
        │   │   └── hosts/
        │   └── debops_service_tinc_mesh0/
        │       └── hosts/
        ├── by-host/
        │   ├── gateway/
        │   │   └── hosts/
        │   ├── hostname1/
        │   │   └── hosts/
        │   └── hostname/
        │       └── hosts/
        └── by-network/
            └── mesh0/
                └── hosts/
                    ├── gateway
                    ├── hostname1
                    └── hostname2

By default all host configuration files in a given mesh network will be stored in:

secret/tinc/networks/<mesh>/by-network/<mesh>/hosts/

The by-group/all/hosts/ directory can be used to distribute public keys to all hosts in a given mesh network. You can also distribute the keys only to hosts in a particular Ansible group, or even to a specific host.

Only the hosts in the current ansible-playbook run will get the keys present in the hosts/ directories. This means that when you add a new host to the mesh, you will need to run the role against all of the hosts of the mesh, otherwise the new host won't be accepted by the mesh due to unknown public keys.

Support for systemd tinc@.service instances

On a legacy systems without systemd, you can manage Tinc VPN networks using the /etc/init.d/tinc init script.

If systemd is detected as the current init process, debops.tinc will configure a set of systemd unit files:

tinc.service

This is the main unit that manages all of the Tinc VPN networks and propagates start/stop/restart events.

tinc@.service

This unit can be used to manage individual Tinc networks. The unit argument is the name of the VPN.

With systemd, you can manage each Tinc network separately by issuing commands:

systemctl status tinc@mesh0
systemctl start tinc@mesh0
systemctl stop tinc@mesh0

debops.persistent_paths support

In case the host in question happens to be a TemplateBasedVM on Qubes OS or another system where persistence is not the default, it should be absent in debops_service_tinc and instead be added to the debops_service_tinc_persistent_paths Ansible inventory group so that the changes can be made persistent:

[debops_service_tinc_persistent_paths]
hostname

Note that the tinc__user (tinc-vpn by default) created by the role is not made persistent because making /etc/passwd and related files persistent might interfere with template changes.

You will need to ensure that the user exists by one of the following ways:

  • Create the user in the template using useradd --system tinc-vpn --comment 'tinc VPN service' --home-dir '/etc/tinc' --shell '/bin/false'

  • Running the above command on start in the TemplateBasedVM

  • Run the role against your template with the role configured in such a way that it only creates the user. Note that this is normally discouraged on Qubes OS.

Besides that, the tinc__base_packages are expected to be present (typically installed in the TemplateVM).

Also note that you will need to set core__unsafe_writes to True when you attempt to update the configuration on a system that uses bind mounts for persistence. You can set core__unsafe_writes directly in your inventory without the need to run the debops.core role for this special case. Refer to Templating or updating persistent files for details.