Skip to main content

This is a new website theme. Help me improve it and give your feedback (opens in a new tab).

Hetzner Dedicated Server Reverse DNS + Ansible

Published:

Tags:

Sysadmin Hetzner Ansible Openbsd Planet FSFE Planet Debian
This blog post is more than two years old. It is preserved here in the hope that it is useful to someone, but please be aware that links may be broken and that opinions expressed here may not reflect my current views. If this is a technical article, it may no longer reflect current best practice.

Continuing on the path towards all my stuff being managed by Ansible, I’ve figured out a method of managing the reverse DNS entries for subnets on the Hetzner Dedicated Server.

There’s a bunch of Ansible modules for handling Hetzner Cloud, but these servers are managed in Robot which the Cloud API doesn’t cover. Instead, you need to use the Robot Webservice.

Ansible does have a module for doing pretty arbitrary things with web APIs though, so using that I’ve got the following playbook figured out to keep the reverse DNS entries in sync:

---
- hosts:
  - vmhost_vm1
  gather_facts: False
  tasks:
  - name: import hetzner webservice credentials
    include_vars:
      file: "hetzner_ws.yml"
  - name: set rdns for hetzner hosts
    block:
    - name: get current rdns entry
      uri:
        user: "{{ hetzner_ws_user }}"
        password: "{{ hetzner_ws_password }}"
        url: "https://robot-ws.your-server.de/rdns/{{ vmip4 }}"
        status_code: [200, 404]
      register: rdns_result
    - name: update incorrect rdns entry
      uri:
        user: "{{ hetzner_ws_user }}"
        password: "{{ hetzner_ws_password }}"
        url: "https://robot-ws.your-server.de/rdns/{{ vmip4 }}"
        method: "POST"
        body_format: form-urlencoded
        body:
          ptr: "{{ inventory_hostname }}"
        status_code: [200, 201]
      when: '"rdns" not in rdns_result.json or inventory_hostname != rdns_result.json.rdns.ptr'
      changed_when: True
    delegate_to: localhost

The host groups this runs on are currently hardcoded as the VMs that live in the Hetzner Dedicated Server. A future iteration of this might use some inventory plugin to look up the subnets that are managed on Hetzner and create a group for all of those. Right now it won’t be setting the reverse DNS for the “router” interface on that subnet, and won’t automatically include new server’s subnets.

Gathering facts is disabled because all of these run locally. There is at least one VM running on this server that I can’t log in to because I host it for someone else, so running locally is a necessity.

The webservice credentials are stored in an Ansible Vault encrypted YAML file and loaded explicitly. An important note: the webservice username and password is not the same as your regular Robot username and password. You need to create a webservice user in Robot under “Settings; Webservice and app settings”.

If you attempt to authenticate with an incorrect username and password 3 times in a row, your IP address will be blocked for 10 minutes. There are 6 hosts in this group, so I did this a few times before I realised there was a different user account I needed to create. I’d suggest limiting to a single host while you’re testing to get the authentication figured out.

The actual calls to the webservice take place in a block just to avoid having to specify the delegate_to: localhost twice. The first step looks up the current PTR record, and accepts success if it gives either a 200 or 404 status code (it will be 404 if there is no existing pointer record). The result of this is used to conditionally create/update the PTR record in the next task.

If nothing needs to be done, nothing will be changed and the second task will be skipped. If a change is needed, the second step is successful if either the PTR record is updated (status 200) or created (status 201).

This was actually a lot easier than I thought it would be, and the uri module looks to be really flexible, so I bet there are other things that I could easily integrate into my Ansible playbooks.