Bootstrapping Boxes Into Tailscale With 1Password

This is a follow-on from Disposable Cloud Environments With Vagrant and Tailscale. The summary is that I've worked out how to get new boxes up and integrated with Tailscale with a small bootstrap Ansible playbook and some 1Password integration.

This is going to be short and direct, with the goal of showing how to repeat this and never have to think about how to manage secrets. Credit to kaushikchandrashekar/developer-vagrant for shortcutting much of this process with a github project showing Vagrant leveraging Ansible Galaxy.

The source code is available at https://github.com/wsargent/vagrant-tailscale-example.

The Problem

The previous post used Vagrant's inline script to set up Tailscale and everything else:

Vagrant.configure("2") do |config|
  config.env.enable

  # vm parameters
 
  config.vm.provision "tailscale-install", type: "shell" do |s|
    s.inline = "curl -fsSL https://tailscale.com/install.sh | sh"
  end
   
  config.vm.provision "tailscale-up", type: "shell" do |s|
    s.inline = "tailscale up --ssh --operator=vagrant --authkey #{ENV['TAILSCALE_AUTHKEY']}"
  end

  # ...yet more script...
end 

Inline scripts in Vagrant aren't great. Every Vagrantfile is different, and the failure behavior is unpredictable. In addition, doing the work of op run -- vagrant up to integrate with 1Password CLI was awkward.

Let's take the opposite approach. Bootstrap Vagrant into a Tailscale host with as little manual work as possible, then provision using Ansible through Tailscale, bypassing Vagrant.

Vagrant with Ansible

The first step was to use the Ansible Provisioner in Vagrant, and put as few things into the Vagrant file as possible.

Vagrant.configure("2") do |config|   
    config.vm.box = "ubuntu/jammy64"
    config.vm.hostname = "vagrant-docker"
    config.vm.provision :ansible do |ansible|
        ansible.compatibility_mode = "2.0"
        ansible.playbook = "playbook.yml"
        ansible.galaxy_role_file = "requirements.yml"
        ansible.galaxy_roles_path = "/etc/ansible/roles"
        ansible.galaxy_command = "sudo ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force"
    end

    config.trigger.before :destroy do |trigger|
        trigger.run_remote = {inline: "tailscale logout"}
        trigger.on_error = :continue
    end
end

Python package management is slightly terrifying, so I opted for apt to install Ansible:

$ sudo add-apt-repository --yes --update ppa:ansible/ansible
$ sudo apt install ansible

Using Ansible Galaxy

There are two Ansible packages I needed to get Tailscale set up: artis3n.tailscale and community.general.onepassword.

These can be set up in requirements.yml:

---
roles:
  - name: artis3n.tailscale    

collections:
  - name: community.general

It's easiest to install these pre-emptively rather than have it pop up in the middle of the install:

$ ansible-galaxy install artis3n.tailscale
$ ansible-galaxy collection install community.general

Running the Playbook

After the packages are installed, the only thing needed in the playbook is to set up the tailscale role and look up the authkey from 1Password using community.general.onepassword:

---
- hosts: all
  become: true
  tasks:
    - name: install-tailscale
      import_role: 
        name: artis3n.tailscale
      vars:
        tailscale_authkey: "{{ lookup('community.general.onepassword', 'vagrant-tailscale', field='credential', vault='will-connect-vault') }}"
        tailscale_args: "--ssh"

I did have to go into Tailscale and change the SSH Check mode from action: check to action: accept so it didn't keep asking me to click on URLs.

The only thing I need to do is make sure I'm signed into 1Password, and then after that the host will pop up:

$ eval $(op signin) # sign into 1p CLI
$ vagrant up
# ...tailscale status shows new host on tailnet...

Integrating Tailscale with Ansible

From there, it's now a question of how to install software other than Tailscale on the box. Ansible can do it, but first Ansible has to know about it.

The first thing to do is set up dynamic inventory with Tailscale using the Tailscale Inventory Plugin:

$ ansible-galaxy collection install freeformz.ansible

And then the configuration in ansible.cfg:

[inventory]
enable_plugins = freeformz.ansible.tailscale

[defaults]
inventory = $HOME/tailscale.yaml
remote_user = vagrant
host_key_checking = False

[ssh_connection]
pipelining=true
retries=10

And now the vagrant boxes can get installs the same way that any other host would. For example, to install Docker:

$ ansible-playbook playbooks/docker.yml

Where docker.yml contains:

- name: Configure with Docker
  hosts: vagrant-docker
  become: true
  tasks:
    - name: apt-update
      apt:
        update_cache: yes
    - name: install-docker
      import_role:
        name: geerlingguy.docker
      vars:
        docker_edition: 'ce'
        docker_package: "docker-"
        docker_package_state: present
        docker_install_compose: true
        docker_users:
          - vagrant

Next Steps

I am aware of Ansible Vault but want to use 1Password in part because the next step is to extend it to 1Password Connect for use with Kubernetes and Terraform. With any luck, this should make working with API keys and tokens much easier – copy and paste them into 1Password and be done. And then, just maybe, never think about secrets management again.