{"id":323,"date":"2025-10-25T09:44:05","date_gmt":"2025-10-25T16:44:05","guid":{"rendered":"https:\/\/www.cmsws.com\/blog\/?p=323"},"modified":"2025-10-25T09:44:05","modified_gmt":"2025-10-25T16:44:05","slug":"self-hosted-bind-9-dynamic-dns-ddns-how-to","status":"publish","type":"post","link":"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/","title":{"rendered":"Self-Hosted Bind 9 Dynamic DNS (DDNS) &#8211; How\u2011To"},"content":{"rendered":"\n<p>This guide shows how to run your own dynamic DNS using <strong>BIND 9<\/strong>, authenticated with <strong>TSIG<\/strong> and updated from a cron\u2011driven script on a Linux host. It includes two safe filesystem layouts, a production\u2011ready updater script, cron setup, and troubleshooting for common errors (like <code>.jnl: create: permission denied<\/code>).<\/p><div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 ez-toc-wrap-right counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<span class=\"ez-toc-title-toggle\"><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#Audience_Assumptions\" >Audience &amp; Assumptions<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#1_Create_a_TSIG_key_on_the_BIND_server\" >1) Create a TSIG key on the BIND server<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#2_Choose_a_writable_zone_file_location\" >2) Choose a writable zone file location<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#3_Configure_the_zone_for_dynamic_updates\" >3) Configure the zone for dynamic updates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#4_Install_the_updater_script_on_your_client_home_network\" >4) Install the updater script on your client (home network)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#5_Add_a_cron_job\" >5) Add a cron job<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#Updating_multiple_hostnames\" >Updating multiple hostnames<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#6_Verification_initial_run\" >6) Verification &amp; initial run<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#7_Troubleshooting\" >7) Troubleshooting<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#Error_jnl_create_permission_denied_journal_open_failed\" >Error: ... .jnl: create: permission denied \/ journal open failed<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#Updates_succeed_but_resolvers_see_old_IP\" >Updates succeed but resolvers see old IP<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#Script_logs_say_%E2%80%9CIP_unchanged%E2%80%9D_but_you_expect_a_change\" >Script logs say \u201cIP unchanged\u201d but you expect a change<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#IPv6_updates_dont_appear\" >IPv6 updates don\u2019t appear<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/www.cmsws.com\/blog\/self-hosted-bind-9-dynamic-dns-ddns-how-to\/#8_Security_operational_tips\" >8) Security &amp; operational tips<\/a><\/li><\/ul><\/nav><\/div>\n\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Audience_Assumptions\"><\/span>Audience &amp; Assumptions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You run an authoritative BIND 9 for your domain (example: my-domain<code>.com<\/code>).<\/li>\n\n\n\n<li>You want to update a hostname (example: <code>home.my-domain.com<\/code>) to follow your changing public IP.<\/li>\n\n\n\n<li>Your BIND server and the client host run Linux (Debian\/Ubuntu examples shown, but adapt as needed).<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"1_Create_a_TSIG_key_on_the_BIND_server\"><\/span>1) Create a TSIG key on the BIND server<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Generate a key that the client will use to authenticate dynamic updates.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tsig-keygen -a hmac-sha256 ddns-home &gt; \/etc\/bind\/ddns-home.key\nsudo chown root:bind \/etc\/bind\/ddns-home.key\nsudo chmod 640 \/etc\/bind\/ddns-home.key\n<\/code><\/pre>\n\n\n\n<p>You will paste the <strong>secret<\/strong> from this file into the BIND config and use the same key file on the client.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"2_Choose_a_writable_zone_file_location\"><\/span>2) Choose a writable zone file location<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>BIND must be able to create a journal file next to the zone (e.g., <code>my-domain.com.zone.jnl<\/code>) for dynamic updates. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chown -R bind:bind \/etc\/bind\/zones\nsudo chmod 750 \/etc\/bind\/zones\n\n# Allow writes via AppArmor (Debian\/Ubuntu)\necho '\/etc\/bind\/zones\/** rw,' | sudo tee -a \/etc\/apparmor.d\/local\/usr.sbin.named\nsudo apparmor_parser -r \/etc\/apparmor.d\/usr.sbin.named || sudo systemctl reload apparmor\n<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>If a stale or root\u2011owned <code>.jnl<\/code> exists, remove it so BIND can recreate it next to the zone.<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo rm -f \/etc\/bind\/zones\/my-domain.com.zone.jnl 2&gt;\/dev\/null || true\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"3_Configure_the_zone_for_dynamic_updates\"><\/span>3) Configure the zone for dynamic updates<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Edit your BIND config (e.g., <code>named.conf.local<\/code>). Replace the file path with the one you chose in Step 2.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>key \"ddns-home\" {\n  algorithm hmac-sha256;\n  secret \"PASTE_THE_SECRET_FROM_\/etc\/bind\/ddns-home.key\";\n};\n\nzone \"my-domain.com\" {\n  type master;\n  file \"\/etc\/bind\/my-domain.com.zone\";\n  \/\/ If you use DNSSEC inline signing, keep these (otherwise you may omit):\n  inline-signing yes;\n  auto-dnssec maintain;\n\n  \/\/ Allow this key to update ONLY the records you intend (least privilege):\n  update-policy {\n    grant ddns-home name \"home.my-domain.com\" A AAAA;\n  };\n  \/\/ Alternative (broader): allow-update { key ddns-home; };\n};\n<\/code><\/pre>\n\n\n\n<p>Reload\/start BIND:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl start named || sudo systemctl start bind9\n# or if it was running already\nsudo rndc reload\n<\/code><\/pre>\n\n\n\n<p>Sanity check:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo named-checkconf -z\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"4_Install_the_updater_script_on_your_client_home_network\"><\/span>4) Install the updater script on your client (home network)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Create <code>\/usr\/local\/sbin\/ddns-update.sh<\/code> on the client machine that will detect the public IP and push an update via <code>nsupdate + TSIG<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tee \/usr\/local\/sbin\/ddns-update.sh > \/dev\/null &lt;&lt;'EOF'\n#!\/usr\/bin\/env bash\nset -euo pipefail\n\n# ======= CONFIG =======\nZONE=\"my-domain.com\"            # your DNS zone\nHOST=\"home\"                 # host label to update (FQDN = HOST.ZONE)\nDNS_SERVER=\"ns0.my-domain.com\"  # your authoritative BIND server (IP or name)\nTTL=120                      # TTL for updated record(s)\nKEYFILE=\"\/root\/ddns-home.key\"   # TSIG key file (copy from server or generate separately)\nSTATE_DIR=\"\/var\/lib\/ddns\"   # cache last-seen IPs to avoid redundant updates\nUPDATE_IPV4=true\nUPDATE_IPV6=false            # set true if you want AAAA updates too\n# =======================\n\nFQDN=\"${HOST}.${ZONE}.\"\nmkdir -p \"${STATE_DIR}\"\n\nlog() { printf '%s %s\\n' \"$(date '+%F %T')\" \"$*\" >&amp;2; }\n\nget_public_ipv4() {\n  # Prefer DNS-based methods; force IPv4. Each probe has a short timeout.\n  # 1) OpenDNS \"myip\" service (anycast IP instead of hostname)\n  ip=\"$(dig -4 +time=2 +tries=1 +short A myip.opendns.com @208.67.222.222 2>\/dev\/null | head -n1)\"\n  &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n\n  # 2) Cloudflare \"whoami\" (CH\/TXT)\n  ip=\"$(dig -4 +time=2 +tries=1 +short TXT CH whoami.cloudflare @1.1.1.1 2>\/dev\/null \\\n        | tr -d '\"' | awk '{print $1}' | head -n1)\"\n  &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n\n  # 3) Google DNS \"o-o.myaddr\"\n  ip=\"$(dig -4 +time=2 +tries=1 +short TXT o-o.myaddr.l.google.com @ns1.google.com 2>\/dev\/null \\\n        | tr -d '\"' | awk -F'\"' '{print $1}' | awk '{print $1}' | head -n1)\"\n  &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n\n  # 4) HTTPS fallback (if curl exists)\n  if command -v curl >\/dev\/null 2>&amp;1; then\n    ip=\"$(curl -4 -fsS --max-time 3 https:\/\/api.ipify.org || true)\"\n    &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n    ip=\"$(curl -4 -fsS --max-time 3 https:\/\/ipv4.icanhazip.com || true)\"\n    &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n  fi\n\n  # Give up\n  return 1\n}\n\nget_public_ipv6() {\n  # Only useful if you actually have IPv6 egress; force IPv6 lookups.\n  ip=\"$(dig -6 +time=2 +tries=1 +short AAAA myip.opendns.com @2620:119:35::35 2>\/dev\/null | head -n1)\"\n  &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n\n  ip=\"$(dig -6 +time=2 +tries=1 +short TXT CH whoami.cloudflare @2606:4700:4700::1111 2>\/dev\/null \\\n        | tr -d '\"' | awk '{print $1}' | head -n1)\"\n  &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n\n  if command -v curl >\/dev\/null 2>&amp;1; then\n    ip=\"$(curl -6 -fsS --max-time 3 https:\/\/api64.ipify.org || true)\"\n    &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n    ip=\"$(curl -6 -fsS --max-time 3 https:\/\/ipv6.icanhazip.com || true)\"\n    &#91;&#91; -n \"$ip\" ]] &amp;&amp; printf '%s' \"$ip\" &amp;&amp; return 0\n  fi\n\n  return 1\n}\n\nupdate_record() {\n  local rtype=\"$1\" new_ip=\"$2\" state_file=\"$3\"\n\n  if &#91;&#91; -z \"$new_ip\" ]]; then\n    log \"No ${rtype} address detected; skipping ${rtype} update.\"\n    return 0\n  fi\n\n  local old_ip=\"\"\n  &#91;&#91; -f \"$state_file\" ]] &amp;&amp; old_ip=\"$(cat \"$state_file\" || true)\"\n\n  if &#91;&#91; \"$new_ip\" == \"$old_ip\" &amp;&amp; -n \"$old_ip\" ]]; then\n    log \"${rtype}: IP unchanged (${new_ip}); no update needed.\"\n    return 0\n  fi\n\n  log \"${rtype}: updating ${FQDN} -> ${new_ip}\"\n  nsupdate -k \"$KEYFILE\" &lt;&lt;NSU\nserver ${DNS_SERVER}\nzone ${ZONE}\nupdate delete ${FQDN} ${rtype}\nupdate add ${FQDN} ${TTL} ${rtype} ${new_ip}\nsend\nNSU\n\n  printf '%s' \"$new_ip\" > \"$state_file\"\n}\n\nmain() {\n  &#91;&#91; -r \"$KEYFILE\" ]] || { log \"Key file not readable: $KEYFILE\"; exit 1; }\n\n  if &#91;&#91; \"${UPDATE_IPV4}\" == true ]]; then\n    ip4=\"$(get_public_ipv4)\"\n    update_record \"A\" \"${ip4}\" \"${STATE_DIR}\/current_ipv4\"\n  fi\n\n  if &#91;&#91; \"${UPDATE_IPV6}\" == true ]]; then\n    ip6=\"$(get_public_ipv6)\"\n    update_record \"AAAA\" \"${ip6}\" \"${STATE_DIR}\/current_ipv6\"\n  fi\n}\n\nmain \"$@\"\nEOF\n\nsudo chmod 750 \/usr\/local\/sbin\/ddns-update.sh\nsudo mkdir -p \/var\/lib\/ddns\nsudo chown root:bind \/var\/lib\/ddns\n<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Key file on the client:<\/strong> copy <code>\/etc\/bind\/ddns-home.key<\/code> from the server over a secure channel (or create a separate key with the same name\/secret). Keep it root\u2011readable only.<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code\"><code># On the client\nsudo scp root@my-domain.com:\/etc\/bind\/ddns-home.key \/root\/ddns-home.key\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"5_Add_a_cron_job\"><\/span>5) Add a cron job<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Run the updater every 5 minutes (common choice):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo crontab -e\n<\/code><\/pre>\n\n\n\n<p>Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/5 * * * * \/usr\/local\/sbin\/ddns-update.sh\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Updating_multiple_hostnames\"><\/span>Updating multiple hostnames<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Reuse the same script by overriding <code>HOST<\/code> via environment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/5 * * * * HOST=home \/usr\/local\/sbin\/ddns-update.sh\n*\/5 * * * * HOST=vpn  \/usr\/local\/sbin\/ddns-update.sh\n<\/code><\/pre>\n\n\n\n<p>Remember to authorize each name in the zone\u2019s <code>update-policy<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>update-policy {\n  grant ddns-home name \"home.my-domain.com\" A AAAA;\n  grant ddns-home name \"vpn.my-domain.com\"  A AAAA;\n};\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"6_Verification_initial_run\"><\/span>6) Verification &amp; initial run<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Validate BIND config and zones:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo named-checkconf -z\n<\/code><\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Kick the updater manually and watch logs:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/sbin\/ddns-update.sh\nsudo journalctl -u bind9 -n 100 --no-pager   # or: sudo tail -n 100 \/var\/log\/syslog\n<\/code><\/pre>\n\n\n\n<p>You should see lines indicating an authorized update and <strong>no<\/strong> <code>journal open failed<\/code> errors.<\/p>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>Check the live record from a resolver (allow a short TTL):<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A home.my-domain.com @ns0.my-domain.com\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"7_Troubleshooting\"><\/span>7) Troubleshooting<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Error_jnl_create_permission_denied_journal_open_failed\"><\/span>Error: <code>... .jnl: create: permission denied<\/code> \/ <code>journal open failed<\/code><span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure the zone file\u2019s <strong>directory<\/strong> and the zone file itself are writable by the <code>bind<\/code> user.<\/li>\n\n\n\n<li>If using <code>\/etc\/bind\/...<\/code>, add the AppArmor write rule and reload AppArmor.<\/li>\n\n\n\n<li>Delete any stale <code>.jnl<\/code> next to the zone so BIND can recreate it.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Updates_succeed_but_resolvers_see_old_IP\"><\/span>Updates succeed but resolvers see old IP<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Check TTL on the RR; if very low, upstream caches may still hold the old value briefly.<\/li>\n\n\n\n<li>Verify you updated the <strong>authoritative<\/strong> server you publish at your NS records.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Script_logs_say_%E2%80%9CIP_unchanged%E2%80%9D_but_you_expect_a_change\"><\/span>Script logs say \u201cIP unchanged\u201d but you expect a change<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm your WAN actually has a new public IP (some ISPs CGNAT or keep it stable).<\/li>\n\n\n\n<li>The script intentionally suppresses redundant updates to reduce journal churn.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"IPv6_updates_dont_appear\"><\/span>IPv6 updates don\u2019t appear<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Set <code>UPDATE_IPV6=true<\/code> and confirm your network has working v6 egress.<\/li>\n\n\n\n<li>Verify <code>update-policy<\/code> grants <code>AAAA<\/code> updates for the name.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"8_Security_operational_tips\"><\/span>8) Security &amp; operational tips<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Least privilege<\/strong>: use <code>update-policy<\/code> to scope updates to specific names and RR types.<\/li>\n\n\n\n<li><strong>Protect TSIG secrets<\/strong>: key files should be readable only by the process\/user that needs them.<\/li>\n\n\n\n<li><strong>Network exposure<\/strong>: if possible, point <code>DNS_SERVER<\/code> at a private\/VPN\u2011reachable address and firewall off the update port from the Internet.<\/li>\n\n\n\n<li><strong>Reasonable TTLs<\/strong>: 60\u2013300 seconds balances agility and cache thrash.<\/li>\n\n\n\n<li><strong>Monitoring<\/strong>: alert on repeated failed updates and on journal write errors.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>You\u2019re done.<\/strong> Your <code>home.my-domain.com<\/code> will now track your changing WAN IP, fully under your control, no third\u2011party DDNS service required.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This guide shows how to run your own dynamic DNS using BIND 9, authenticated with TSIG and updated from a cron\u2011driven script on a Linux host. It includes two safe filesystem layouts, a production\u2011ready updater script, cron setup, and troubleshooting for common errors (like .jnl: create: permission denied). Audience &amp; Assumptions 1) Create a TSIG key on the BIND server [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40,4,25,1,44],"tags":[],"class_list":["post-323","post","type-post","status-publish","format-standard","hentry","category-cloud-computing","category-education","category-linux","category-misc","category-network-services"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/posts\/323","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/comments?post=323"}],"version-history":[{"count":2,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/posts\/323\/revisions"}],"predecessor-version":[{"id":331,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/posts\/323\/revisions\/331"}],"wp:attachment":[{"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/media?parent=323"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/categories?post=323"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cmsws.com\/blog\/wp-json\/wp\/v2\/tags?post=323"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}