diff --git a/scripts/firefox-sync-exceptions.nu b/scripts/firefox-sync-exceptions.nu
new file mode 100755
index 0000000..9e3e0be
--- /dev/null
+++ b/scripts/firefox-sync-exceptions.nu
@@ -0,0 +1,120 @@
+#! /usr/bin/env nu
+
+print "This script synchronizes site data & cookie exceptions for Firefox-based browsers with a TOML config file."
+
+let browsers = [
+  { name: "Mullvad Browser" path: "~/.mullvad/mullvadbrowser" }
+  { name: "Tor Browser" path: "~/.tor project/firefox" }
+  { name: "Firefox" path: "~/.mozilla/firefox" }
+]
+let browser = $browsers | input list --display name --fuzzy "Select your browser"
+
+print $"Looking for profiles in ($browser.path)"
+let profiles = ls --short-names ...(glob $browser.path) | where type == "dir" | get name
+let profile = $profiles | input list --fuzzy "Select the profile you want to change"
+
+let db_file = $"($browser.path)/($profile)/permissions.sqlite"
+
+let config_file = "/etc/nixos/home/browsers/site-data-exceptions.toml"
+let config_file = input --default $config_file "Select your config file"
+
+# get origin & origin attributes separately from single origin string
+def split_origin []: string -> record {
+  let x = $in | split column '^' origin attrs | into record
+  let attrs = if $x.attrs? != null {
+    $x.attrs | split row '&' | each {|attr| $attr | split row '='} | into record
+  } else { {} }
+  { origin: $in origin_short: $x.origin } | merge $attrs
+}
+
+let db = $db_file | open
+let config = $config_file | open
+
+let exceptions_db = $db | get moz_perms
+  | where type == "cookie" and permission == 1 and expireType == 0 and expireTime == 0
+  | each {|x| $x | merge ($x.origin | split_origin) }
+let exceptions_config = $config | get $profile | each {|origin| $origin | split_origin }
+
+let exceptions_to_insert = $exceptions_config
+  | filter {|x| ($exceptions_db | where origin_short == $x.origin_short | length) == 0 }
+  | each {|x| { origin: $x.origin } }
+
+let exceptions_to_update = $exceptions_config
+  | each {|x|
+    let results = $exceptions_db | where origin_short == $x.origin_short and origin != $x.origin
+    let result = $results.0?
+    if $result != null {
+      { id: $result.id origin: $x.origin origin_old: $result.origin }
+    } else { null }
+  }
+
+let exceptions_to_delete = $exceptions_db
+  | filter {|x| ($exceptions_config | where origin_short == $x.origin_short | length) == 0 }
+  | each {|x| { id: $x.id origin: $x.origin } }
+
+let needs_insert = ($exceptions_to_insert | length) > 0
+let needs_update = ($exceptions_to_update | length) > 0
+let needs_delete = ($exceptions_to_delete | length) > 0
+let needs_changes = $needs_insert or $needs_update or $needs_delete
+
+if $needs_changes == false {
+  print "No changes neccessary"
+  exit
+}
+
+mut available_actions = []
+
+if $needs_insert {
+  print "Exceptions to insert:"
+  print $exceptions_to_insert
+  $available_actions = $available_actions | append "insert"
+}
+
+if $needs_update {
+  print "Exceptions to update:"
+  print $exceptions_to_update
+  $available_actions = $available_actions | append "update"
+}
+
+if $needs_delete {
+  print "Exceptions to delete:"
+  print $exceptions_to_delete
+  $available_actions = $available_actions | append "delete"
+}
+
+let prompt = "Which of the changes above should be applied?"
+let actions = $available_actions | input list --multi $prompt
+
+if ($actions | length) > 0 {
+  let time = (date now | into int) / 1000000 | into int
+
+  if $needs_insert and "insert" in $actions {
+    $exceptions_to_insert | each {|x|
+      $db | query db '
+        INSERT INTO moz_perms (origin, type, permission, expireType, expireTime, modificationTime)
+        VALUES (:origin, "cookie", 1, 0, 0, :time)
+      ' --params { origin: $x.origin time: $time }
+    }
+    print "Successfully inserted new records"
+  }
+
+  if $needs_update and "update" in $actions {
+    $exceptions_to_update | each {|x|
+      $db | query db '
+        UPDATE moz_perms
+        SET origin = :origin, modificationTime = :time
+        WHERE id = :id
+      ' --params { id: $x.id origin: $x.origin time: $time }
+    }
+    print "Successfully updated changed records"
+  }
+
+  if $needs_delete and "delete" in $actions {
+    $exceptions_to_delete | each {|x|
+      $db | query db 'DELETE FROM moz_perms WHERE id = :id' --params { id: $x.id }
+    }
+    print "Successfully deleted missing records"
+  }
+} else {
+  print "Not applying any changes"
+}
diff --git a/scripts/tailscale-lock-sign-mullvad-exit-nodes.nu b/scripts/tailscale-lock-sign-mullvad-exit-nodes.nu
new file mode 100755
index 0000000..ea7c0c0
--- /dev/null
+++ b/scripts/tailscale-lock-sign-mullvad-exit-nodes.nu
@@ -0,0 +1,26 @@
+#! /usr/bin/env nu
+
+let $status = tailscale lock status --json | from json
+
+let $nodes = $status | get FilteredPeers
+let $nodes_mullvad = $nodes | where Name =~ ".mullvad.ts.net"
+
+let count_total = $nodes | length
+let count_mullvad = $nodes_mullvad | length
+
+print $"unsigned nodes: ($count_total) total, ($count_mullvad) Mullvad"
+
+if ($nodes_mullvad | length) == 0 {
+  print "no Mullvad nodes need to be signed"
+  return
+}
+
+print "signing Mullvad nodes..."
+
+$nodes_mullvad | each { |node|
+  print $"signing ($node.Name)"
+  tailscale lock sign $node.NodeKey
+  sleep 0.1sec
+}
+
+print "all Mullvad nodes successfully signed"
diff --git a/system/vpn.nix b/system/vpn.nix
index c3d3afb..c8543b4 100644
--- a/system/vpn.nix
+++ b/system/vpn.nix
@@ -56,4 +56,9 @@ in
     sslCertificate = "/var/lib/tailscale/certs/${tailnetHost}.crt";
     sslCertificateKey = "/var/lib/tailscale/certs/${tailnetHost}.key";
   };
+
+  # TODO Tailscale Mullvad exit nodes currently don't support IPv6 and this is
+  # causing issues with nginx (proxy pass) requests timing out and high CPU load.
+  # Until Mullvad exit nodes support IPv6, we'll just disable IPv6 for nginx.
+  services.nginx.resolver.ipv6 = false;
 }