← Blog

Task 2.2 - Dynamic DNS

TLDR

  • Created a Mythic Beasts DNS API key (“Dynamic DNS”) with permits to update @ and www A records for cainappleby.net.
  • Stored the API credentials in ~/.netrc, then confirmed curl -n -X POST correctly updated the @ record via Mythic Beasts’ DDNS endpoint.
  • Wrapped the curl call in a dynamicDns.sh script and exposed it via a mythic-ddns.service oneshot unit.
  • Added a mythic-ddns.timer unit to run once a minute after boot and then every 5 minutes.
  • Hit a “No credentials supplied” error because systemd was running as root and looking at /root/.netrc, then fixed it by running the service as my user with the correct working directory.
  • Enabled the timer, confirmed the timer → service → script chain works, and verified each run and API response via journalctl.

Task Requirements

  • Use Mythic Beasts (current DNS provider) to keep the domain cainappleby.net pointing at my current public IPv4 address.
  • Specifically allow DDNS updates for the @ and www A records without deleting the existing records.
  • Automate the DDNS API call on the Pi using systemd timers and services rather than cron.
  • Ensure logs for the DDNS updates are accessible via journalctl, relying on Ubuntu Server’s built-in log rotation and caps.
  • Keep the setup simple: one shell script, one service unit, one timer unit.

Goal

I wanted the A records for cainappleby.net (both @ and www) to track my ISP’s changing IPv4 automatically so the site stays reachable, with the Pi periodically calling the Mythic Beasts DDNS API instead of me manually editing DNS or living with stale records.

Implementation Steps

  1. I logged into Mythic Beasts (which already hosts my domain) and created a DNS API key named Dynamic DNS. I added two permits: one for hostname @ in zone cainappleby.net of type A, and one for hostname www in the same zone and type, so the key can update both relevant A records.

  2. From the Mythic Beasts documentation I confirmed that their Dynamic DNS API updates existing records rather than creating new ones, so I left my current A records in place instead of deleting anything.

  3. Following the docs, I created a .netrc file in my home directory with nano .netrc, using the precise format they specify so curl can pick up the credentials from $HOME:

    machine ipv4.api.mythic-beasts.com
    login KEYID
    password SECRET
    

    The key point here is that it lives in my home directory because curl looks there by default when using -n.

  4. I initially referenced the documentation example:

    curl -n -X POST https://ipv4.api.mythic-beasts.com/dns/v2/dynamic/myhost.example.com
    

    then made the real call for my domain as recommended:

    curl -n -X POST https://ipv4.api.mythic-beasts.com/dns/v2/dynamic/cainappleby.net
    

    The response came back as JSON:

    {"message":"Dynamic DNS for @ set to IPv4 REDACTED"}
    

    which confirmed that .netrc was wired correctly, the API key had the right permits, and a single POST updates the @ A record to my current public IPv4.

  5. To avoid typing the curl command manually, I placed it into a small shell script in my home directory. The script is just a file of terminal commands, but I explicitly declared bash so the system knows how to execute it:

    #!/bin/bash
    curl -n -X POST https://ipv4.api.mythic-beasts.com/dns/v2/dynamic/cainappleby.net
    

    I saved this as dynamicDns.sh (camel casing just for readability).

  6. I decided to use systemd on the Pi (the system’s service manager) as an easy and reliable way to auto-call the API. The basic structure I wanted was: systemd timer → systemd service → shell script → DDNS API.

  7. For the service unit, I went into /etc/systemd/system/ (where services live) and created mythic-ddns.service using nano mythic-ddns.service. The initial unit looked like this:

    [Unit]
    Description=Service to run dynamicDns.sh in home directory.
    [Service]
    Type=oneshot
    ExecStart=/home/santigold/dynamicDns.sh
    [Install]
    WantedBy=multi-user.target
    

    Type=oneshot fits because it’s a single POST that starts and finishes with a clear return, and ExecStart points at the shell script with the curl command.

  8. I knew systemd services log to the journal, so whatever the script prints (including the JSON from curl) ends up accessible via journalctl. For example, I can view logs for the service with:

    journalctl -u servicename
    

    and Ubuntu Server will handle log rotation and caps, so log bloat isn’t a concern here.

  9. With the service sketched out, I created the matching timer. Still in /etc/systemd/system/, I ran nano mythic-ddns.timer and used:

    [Unit]
    Description=Timer to run service mythic-ddns.service
    [Timer]
    OnBootSec=1min
    OnUnitActiveSec=5min
    [Install]
    WantedBy=multi-user.target
    

    The key settings are OnBootSec=1min for the first run one minute after boot, and OnUnitActiveSec=5min to rerun the service every five minutes after a successful run.

  10. Before trusting the whole chain, I tested the basic setup. First I ran the .sh file manually to confirm it still worked. Then I reloaded systemd so it would see the new service and timer:

    sudo systemctl daemon-reload
    

    After that I manually started the service:

    sudo systemctl start mythic-ddns.service
    

    and checked the logs with:

    journalctl -u mythic.ddns.service
    

    The response from the service was:

    {"error":"No credentials supplied","status":"Error"}
    

    which meant the script wasn’t finding the .netrc credentials.

  11. I traced the issue to how systemd was running the service: because it was running as root, it looked for /root/.netrc instead of the .netrc in my home directory. To fix this cleanly, I made the service run as my user and set the working directory to my home:

    [Service]
    Type=oneshot
    User=santigold
    WorkingDirectory=/home/santigold
    ExecStart=/home/santigold/dynamicDns.sh
    

    I edited the unit with nano, then ran:

    sudo systemctl daemon-reload
    sudo systemctl start mythic-ddns.service
    journalctl -u mythic-ddns.service -n 10
    

    I ignored the older failed entries and just checked the latest log lines, which now showed a successful run.

  12. With a working service, the final step was to enable the timer so it actually fires on schedule. I ran:

    systemctl enable mythic-ddns.timer
    

    and, because I wasn’t rebooting the Pi, I started it immediately:

    systemctl enable --now mythic-ddns.timer
    

    After waiting five minutes, I confirmed the timer had triggered, the service had run, and the A record update via the DDNS API worked perfectly. The final flow is:

    Timer -> Service -> dynamicDns.sh -> curl -> Mythic Beasts DDNS
    

    with everything logged in the journal.

Notes & Decisions

  • I stuck with Mythic Beasts’ own Dynamic DNS API because they already host my DNS and provide clear guidance on using .netrc and the DDNS endpoint.
  • I deliberately left the existing @ and www A records in place because the DDNS API updates them; deleting them would have broken this behaviour.
  • I chose systemd (service + timer) over cron so I get nicer integration with the Pi’s service manager and easy log access via journalctl.
  • Relying on .netrc instead of passing credentials on the command line keeps secrets out of the unit files and logs while matching Mythic Beasts’ recommendations.
  • Running the service as my own user with a proper WorkingDirectory solved the .netrc lookup problem and avoids unnecessary root access for this task.

Next Ideas / Follow-ups

None captured yet.