diff --git a/backend/index.js b/backend/index.js
index 551378251..0e4c5ce2b 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -9,6 +9,7 @@ async function appStart () {
 	const app                 = require('./app');
 	const internalCertificate = require('./internal/certificate');
 	const internalIpRanges    = require('./internal/ip_ranges');
+	const ddnsUpdater         = require('./lib/ddns_resolver/ddns_updater');
 
 	return migrate.latest()
 		.then(setup)
@@ -17,6 +18,7 @@ async function appStart () {
 		.then(() => {
 			internalCertificate.initTimer();
 			internalIpRanges.initTimer();
+			ddnsUpdater.initTimer();
 
 			const server = app.listen(3000, () => {
 				logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js
index f6043e18b..97d0087b4 100644
--- a/backend/internal/access-list.js
+++ b/backend/internal/access-list.js
@@ -2,6 +2,7 @@ const _                     = require('lodash');
 const fs                    = require('fs');
 const batchflow             = require('batchflow');
 const logger                = require('../logger').access;
+const ddnsUpdater           = require('../lib/ddns_resolver/ddns_updater');
 const error                 = require('../lib/error');
 const utils                 = require('../lib/utils');
 const accessListModel       = require('../models/access_list');
@@ -97,6 +98,10 @@ const internalAccessList = {
 					.then(() => {
 						return internalAccessList.maskItems(row);
 					});
+			})
+			.then((result) => {
+				// Call the DDNS updater after the access list update process is complete
+				return ddnsUpdater.updateDynamicDnsRecords().then(() => result);
 			});
 	},
 
@@ -230,6 +235,10 @@ const internalAccessList = {
 					.then(() => {
 						return internalAccessList.maskItems(row);
 					});
+			})
+			.then((result) => {
+				// Call the DDNS updater after the access list update process is complete
+				return ddnsUpdater.updateDynamicDnsRecords().then(() => result);
 			});
 	},
 
diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js
index 5f802c004..b65cde486 100644
--- a/backend/internal/nginx.js
+++ b/backend/internal/nginx.js
@@ -1,9 +1,10 @@
-const _      = require('lodash');
-const fs     = require('fs');
-const logger = require('../logger').nginx;
-const config = require('../lib/config');
-const utils  = require('../lib/utils');
-const error  = require('../lib/error');
+const _            = require('lodash');
+const fs           = require('fs');
+const logger       = require('../logger').nginx;
+const config       = require('../lib/config');
+const utils        = require('../lib/utils');
+const error        = require('../lib/error');
+const ddnsResolver = require('../lib/ddns_resolver/ddns_resolver');
 
 const internalNginx = {
 
@@ -131,6 +132,37 @@ const internalNginx = {
 		return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf';
 	},
 
+	/**
+	 * Resolves any ddns addresses that need to be resolved for clients in the host's access list.
+	 * Defines a new property 'resolvedAddress' on each client in `host.access_list.clients` that uses a ddns address.
+	 * @param {Object} host 
+	 * @returns {Promise}
+	 */
+	resolveDDNSAddresses: (host) => {
+		const promises = [];
+		if (typeof host.access_list !== 'undefined' && host.access_list && typeof host.access_list.clients !== 'undefined' && host.access_list.clients) {
+			for (const client of host.access_list.clients) {
+				const address = client.address;
+				if (ddnsResolver.ddnsRegex.test(address)) {
+					const p = ddnsResolver.resolveAddress(address)
+						.then((resolvedIP) => {
+							if (resolvedIP !== address) {
+								Object.defineProperty(client, 'resolvedAddress', {value: resolvedIP});
+							} else {
+								delete client.resolvedAddress;
+							}
+							return Promise.resolve();
+						});
+					promises.push(p);
+				}
+			}
+		}
+		if (promises.length) {
+			return Promise.all(promises);
+		}
+		return Promise.resolve();
+	},
+
 	/**
 	 * Generates custom locations
 	 * @param   {Object}  host
@@ -203,6 +235,12 @@ const internalNginx = {
 				return;
 			}
 
+			// Resolve ddns addresses if needed
+			let resolverPromise = Promise.resolve();
+			if (host_type === 'proxy_host') {
+				resolverPromise = internalNginx.resolveDDNSAddresses(host);
+			}
+
 			let locationsPromise;
 			let origLocations;
 
@@ -217,8 +255,10 @@ const internalNginx = {
 			if (host.locations) {
 				//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
 				origLocations    = [].concat(host.locations);
-				locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
-					host.locations = renderedLocations;
+				locationsPromise = resolverPromise.then(() => {
+					return internalNginx.renderLocations(host).then((renderedLocations) => {
+						host.locations = renderedLocations;
+					});
 				});
 
 				// Allow someone who is using / custom location path to use it, and skip the default / location
@@ -229,7 +269,7 @@ const internalNginx = {
 				});
 
 			} else {
-				locationsPromise = Promise.resolve();
+				locationsPromise = resolverPromise;
 			}
 
 			// Set the IPv6 setting for the host
diff --git a/backend/lib/ddns_resolver/ddns_resolver.js b/backend/lib/ddns_resolver/ddns_resolver.js
new file mode 100644
index 000000000..26d367a31
--- /dev/null
+++ b/backend/lib/ddns_resolver/ddns_resolver.js
@@ -0,0 +1,70 @@
+const error  = require('../error');
+const logger = require('../../logger').ddns;
+const utils  = require('../utils');
+
+const ddnsResolver = {
+	/** Pattern to match any valid domain/subdomain */
+	ddnsRegex: /^((?!-)[A-Za-z\d-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$/,
+
+	/**
+     * Resolves the given address to its IP
+     * @param {String} domainName domain name of the dynamic DNS record
+     * @param {boolean} forceUpdate option to force resolution instead of using the cached value
+     */
+	resolveAddress: (domainName, forceUpdate=false) => {
+		if (!forceUpdate && ddnsResolver._cache.has(domainName)) {
+			// Check if it is still valid
+			const value       = ddnsResolver._cache.get(domainName);
+			const ip          = value[0];
+			const lastUpdated = value[1];
+			const nowSeconds  = Date.now();
+			const delta       = nowSeconds - lastUpdated;
+			if (delta < ddnsResolver._updateIntervalMs) {
+				return Promise.resolve(ip);
+			}
+		}
+		ddnsResolver._cache.delete(domainName);
+		// Reach here only if cache value doesn't exist or needs to be updated 
+		let host = domainName.toLowerCase();
+		return ddnsResolver._queryHost(host)
+			.then((resolvedIP) => {
+				ddnsResolver._cache.set(domainName, [resolvedIP, Date.now()]);
+				return resolvedIP;
+			})
+			.catch((/*error*/) => {
+				// return input address in case of failure
+				logger.error(`Failed to resolve IP for ${host}`);
+				return domainName;
+			});
+	},
+
+    
+	/** Cache mapping host to (ip address, last updated time) */
+	_cache: new Map(),
+
+	/**
+     * Uses execSafe to query the IP address of the given host
+     * @param {String} host host to query
+     * @returns {Promise} resolves to the IPV4 address of the host
+     */
+	_queryHost: (host) => {
+		return utils.execSafe('getent', ['ahostsv4', host])
+			.then((result) => {
+				const ipv4Regex = /(\d{1,3}\.){3}\d{1,3}/;
+				const match     = result.match(ipv4Regex);
+				
+				if (!match) {
+					logger.error(`IPV4 lookup for ${host} returned invalid output: ${result}`);
+					throw error.ValidationError('Invalid output from getent hosts');
+				}
+				
+				return match[0];
+			},
+			(error) => {
+				logger.error('Error looking up IP for ' + host + ': ', error);
+				throw error; 
+			});
+	},
+};
+
+module.exports = ddnsResolver;
diff --git a/backend/lib/ddns_resolver/ddns_updater.js b/backend/lib/ddns_resolver/ddns_updater.js
new file mode 100644
index 000000000..c63fc2f57
--- /dev/null
+++ b/backend/lib/ddns_resolver/ddns_updater.js
@@ -0,0 +1,170 @@
+const internalNginx      = require('../../internal/nginx');
+const logger             = require('../../logger').ddns;
+const internalAccessList = require('../../internal/access-list');
+const ddnsResolver       = require('./ddns_resolver');
+
+const ddnsUpdater = {
+	/**
+     * Starts a timer to periodically check for ddns updates
+     */
+	initTimer: () => {
+		ddnsUpdater._initialize();
+		ddnsUpdater._interval = setInterval(ddnsUpdater.updateDynamicDnsRecords, ddnsUpdater._updateIntervalMs);
+		logger.info(`DDNS Update Timer initialized (interval: ${Math.floor(ddnsUpdater._updateIntervalMs / 1000)}s)`);
+		// Trigger a run so that initial cache is populated and hosts can be updated - delay by 10s to give server time to boot up
+		setTimeout(ddnsUpdater.updateDynamicDnsRecords, 10 * 1000);
+	},
+
+	/** Private **/
+	// Properties
+	_initialized:          false,
+	_updateIntervalMs:     60 * 60 * 1000, // 1 hr default (overridden with $DDNS_UPDATE_INTERVAL env var)
+	_interval:             null, // reference to created interval id
+	_processingDDNSUpdate: false,
+
+	// Methods
+
+	_initialize: () => {
+		if (ddnsUpdater._initialized) {
+			return;
+		}
+		// Init the resolver
+		// Read and set custom update interval from env if needed
+		if (typeof process.env.DDNS_UPDATE_INTERVAL !== 'undefined') {
+			const interval = Number(process.env.DDNS_UPDATE_INTERVAL.toLowerCase());
+			if (!isNaN(interval)) {
+				// Interval value from env is in seconds. Set min to 60s.
+				ddnsUpdater._updateIntervalMs = Math.max(interval * 1000, 60 * 1000);
+			} else {
+				logger.warn(`[DDNS] invalid value for update interval: '${process.env.DDNS_UPDATE_INTERVAL}'`);
+			}
+		}
+		ddnsUpdater._initialized = true;
+	},
+
+	/**
+     * Triggered by a timer, will check for and update ddns hosts in access list clients
+    */
+	updateDynamicDnsRecords: () => {
+		logger.info('Checking for DDNS updates...');
+		if (!ddnsUpdater._processingDDNSUpdate) {
+			ddnsUpdater._processingDDNSUpdate = true;
+            
+			const updatedAddresses = new Map();
+
+			// Get all ddns hostnames in use
+			return ddnsUpdater._getAccessLists()
+				.then((rows) => {
+					// Build map of used addresses that require resolution
+					const usedAddresses = new Map();
+					for (const row of rows) {
+						if (!row.proxy_host_count) {
+							// Ignore rows (access lists) that are not associated to any hosts
+							continue;
+						}
+						for (const client of row.clients) {
+							if (!ddnsResolver.ddnsRegex.test(client.address)) {
+								continue;
+							}
+							if (!usedAddresses.has(client.address)) {
+								usedAddresses.set(client.address, [row]);
+							} else {
+								usedAddresses.get(client.address).push(row);
+							}
+						}
+					}
+					logger.info(`Found ${usedAddresses.size} address(es) in use.`);
+					// Remove unused addresses
+					const addressesToRemove = [];
+					for (const address of ddnsResolver._cache.keys()) {
+						if (!usedAddresses.has(address)) {
+							addressesToRemove.push(address);
+						}
+					}
+					addressesToRemove.forEach((address) => { ddnsResolver._cache.delete(address); });
+
+					const promises = [];
+
+					for (const [address, rows] of usedAddresses) {
+						let oldIP = '';
+						if (ddnsResolver._cache.has(address)) {
+							oldIP = ddnsResolver._cache.get(address)[0];
+						}
+						const p = ddnsResolver.resolveAddress(address, true)
+							.then((resolvedIP) => {
+								if (resolvedIP === address) {
+									updatedAddresses.delete(address);
+								}
+								if (resolvedIP !== address && resolvedIP !== oldIP) {
+									// Mark this as an updated address
+									updatedAddresses.set(address, rows);
+								}
+							});
+						promises.push(p);
+					}
+
+					if (promises.length) {
+						return Promise.all(promises);
+					}
+					return Promise.resolve();
+				})
+				.then(() => {
+					logger.info(`${updatedAddresses.size} DDNS IP(s) updated.`);
+					const updatedRows = new Map();
+					const proxy_hosts = [];
+					for (const rows of updatedAddresses.values()) {
+						for (const row of rows) {
+							if (!updatedRows.has(row.id)) {
+								updatedRows.set(row.id, 1);
+								for (const host of row.proxy_hosts) {
+									if (host.enabled) {
+										proxy_hosts.push(host);
+									}
+								}
+							}
+						}
+					}
+					if (proxy_hosts.length) {
+						logger.info(`Updating ${proxy_hosts.length} proxy host(s) affected by DDNS changes`);
+						return internalNginx.bulkGenerateConfigs('proxy_host', proxy_hosts)
+							.then(internalNginx.reload);
+					}
+					return Promise.resolve();
+				})
+				.then(() => {
+					logger.info('Finished checking for DDNS updates');
+					ddnsUpdater._processingDDNSUpdate = false;
+				});
+		} else {
+			logger.info('Skipping since previous DDNS update check is in progress');
+		}
+	},
+
+	_getAccessLists: () => {
+		const fakeAccess = {
+			can: (/*role*/) => {
+				return Promise.resolve({
+					permission_visibility: 'all'
+				});
+			}
+		};
+        
+		return internalAccessList.getAll(fakeAccess)
+			.then((rows) => {
+				const promises = [];
+				for (const row of rows) {
+					const p = internalAccessList.get(fakeAccess, {
+						id:     row.id,
+						expand: ['owner', 'items', 'clients', 'proxy_hosts.[certificate,access_list.[clients,items]]']
+					}, true /* <- skip masking */);
+					promises.push(p);
+				}
+				if (promises.length) {
+					return Promise.all(promises);
+				}
+				return Promise.resolve([]);
+			});
+	}
+};
+
+module.exports = ddnsUpdater;
\ No newline at end of file
diff --git a/backend/lib/utils.js b/backend/lib/utils.js
index bcdb3341c..26a0ffdc1 100644
--- a/backend/lib/utils.js
+++ b/backend/lib/utils.js
@@ -4,6 +4,7 @@ const execFile   = require('child_process').execFile;
 const { Liquid } = require('liquidjs');
 const logger     = require('../logger').global;
 const error      = require('./error');
+const spawn      = require('child_process').spawn;
 
 module.exports = {
 
@@ -26,6 +27,37 @@ module.exports = {
 		return stdout;
 	},
 
+	/**
+	 * Run the given command. Safer than using exec since args are passed as a list instead of in shell mode as a single string.
+	 * @param {string} cmd The command to run
+	 * @param {string} args The args to pass to the command
+	 * @returns Promise that resolves to stdout or an object with error code and stderr if there's an error
+	 */
+	execSafe: (cmd, args) => {
+		return new Promise((resolve, reject) => {
+			let stdout = '';
+			let stderr = '';
+			const proc = spawn(cmd, args);
+			proc.stdout.on('data', (data) => {
+				stdout += data;
+			});
+			proc.stderr.on('data', (data) => {
+				stderr += data;
+			});
+
+			proc.on('close', (exitCode) => {
+				if (!exitCode) {
+					resolve(stdout.trim());
+				} else {
+					reject({
+						exitCode: exitCode,
+						stderr:   stderr
+					});
+				}
+			});
+		});
+	},
+
 	/**
 	 * @param   {String} cmd
 	 * @param   {Array}  args
@@ -96,6 +128,9 @@ module.exports = {
 		 */
 		renderEngine.registerFilter('nginxAccessRule', (v) => {
 			if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) {
+				if (typeof v.resolvedAddress !== 'undefined' && v.resolvedAddress) {
+					return `${v.directive} ${v.resolvedAddress}; # ${v.address}`;
+				}
 				return `${v.directive} ${v.address};`;
 			}
 			return '';
diff --git a/backend/logger.js b/backend/logger.js
index 0ebb07c58..78553f515 100644
--- a/backend/logger.js
+++ b/backend/logger.js
@@ -10,5 +10,6 @@ module.exports = {
 	certbot:   new Signale({scope: 'Certbot  '}),
 	import:    new Signale({scope: 'Importer '}),
 	setup:     new Signale({scope: 'Setup    '}),
-	ip_ranges: new Signale({scope: 'IP Ranges'})
+	ip_ranges: new Signale({scope: 'IP Ranges'}),
+	ddns:      new Signale({scope: 'DDNS     '})
 };
diff --git a/backend/schema/components/access-list-object.json b/backend/schema/components/access-list-object.json
index cd0218d72..91c220e63 100644
--- a/backend/schema/components/access-list-object.json
+++ b/backend/schema/components/access-list-object.json
@@ -28,15 +28,19 @@
 			"oneOf": [
 				{
 					"type": "string",
-					"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+					"pattern": "^([\\d]{1,3}\\.){3}[\\d]{1,3}(/([\\d]|[1-2][\\d]|3[0-2]))?$"
 				},
 				{
 					"type": "string",
-					"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+					"pattern": "^s*((([\\dA-Fa-f]{1,4}:){7}([\\dA-Fa-f]{1,4}|:))|(([\\dA-Fa-f]{1,4}:){6}(:[\\dA-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){5}(((:[\\dA-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){4}(((:[\\dA-Fa-f]{1,4}){1,3})|((:[\\dA-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){3}(((:[\\dA-Fa-f]{1,4}){1,4})|((:[\\dA-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){2}(((:[\\dA-Fa-f]{1,4}){1,5})|((:[\\dA-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){1}(((:[\\dA-Fa-f]{1,4}){1,6})|((:[\\dA-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[\\dA-Fa-f]{1,4}){1,7})|((:[\\dA-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([\\d]|[1-9][\\d]|1[0-1][\\d]|12[0-8]))?$"
 				},
 				{
 					"type": "string",
 					"pattern": "^all$"
+				},
+				{
+					"type": "string",
+					"pattern": "^((?!-)[A-Za-z\\d-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$"
 				}
 			]
 		},
diff --git a/backend/schema/paths/nginx/access-lists/listID/put.json b/backend/schema/paths/nginx/access-lists/listID/put.json
index 7f887dad6..e80b1e89b 100644
--- a/backend/schema/paths/nginx/access-lists/listID/put.json
+++ b/backend/schema/paths/nginx/access-lists/listID/put.json
@@ -64,15 +64,19 @@
 										"oneOf": [
 											{
 												"type": "string",
-												"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+												"pattern": "^([\\d]{1,3}\\.){3}[\\d]{1,3}(/([\\d]|[1-2][\\d]|3[0-2]))?$"
 											},
 											{
 												"type": "string",
-												"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+												"pattern": "^s*((([\\dA-Fa-f]{1,4}:){7}([\\dA-Fa-f]{1,4}|:))|(([\\dA-Fa-f]{1,4}:){6}(:[\\dA-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){5}(((:[\\dA-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){4}(((:[\\dA-Fa-f]{1,4}){1,3})|((:[\\dA-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){3}(((:[\\dA-Fa-f]{1,4}){1,4})|((:[\\dA-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){2}(((:[\\dA-Fa-f]{1,4}){1,5})|((:[\\dA-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){1}(((:[\\dA-Fa-f]{1,4}){1,6})|((:[\\dA-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[\\dA-Fa-f]{1,4}){1,7})|((:[\\dA-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([\\d]|[1-9][\\d]|1[0-1][\\d]|12[0-8]))?$"
 											},
 											{
 												"type": "string",
 												"pattern": "^all$"
+											},
+											{
+												"type": "string",
+												"pattern": "^((?!-)[A-Za-z\\d-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$"
 											}
 										]
 									},
diff --git a/backend/schema/paths/nginx/access-lists/post.json b/backend/schema/paths/nginx/access-lists/post.json
index 4c5a4edd2..bee464e12 100644
--- a/backend/schema/paths/nginx/access-lists/post.json
+++ b/backend/schema/paths/nginx/access-lists/post.json
@@ -53,15 +53,19 @@
 										"oneOf": [
 											{
 												"type": "string",
-												"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$"
+												"pattern": "^([\\d]{1,3}\\.){3}[\\d]{1,3}(/([\\d]|[1-2][\\d]|3[0-2]))?$"
 											},
 											{
 												"type": "string",
-												"pattern": "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"
+												"pattern": "^s*((([\\dA-Fa-f]{1,4}:){7}([\\dA-Fa-f]{1,4}|:))|(([\\dA-Fa-f]{1,4}:){6}(:[\\dA-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){5}(((:[\\dA-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([\\dA-Fa-f]{1,4}:){4}(((:[\\dA-Fa-f]{1,4}){1,3})|((:[\\dA-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){3}(((:[\\dA-Fa-f]{1,4}){1,4})|((:[\\dA-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){2}(((:[\\dA-Fa-f]{1,4}){1,5})|((:[\\dA-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([\\dA-Fa-f]{1,4}:){1}(((:[\\dA-Fa-f]{1,4}){1,6})|((:[\\dA-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[\\dA-Fa-f]{1,4}){1,7})|((:[\\dA-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([\\d]|[1-9][\\d]|1[0-1][\\d]|12[0-8]))?$"
 											},
 											{
 												"type": "string",
 												"pattern": "^all$"
+											},
+											{
+												"type": "string",
+												"pattern": "^((?!-)[A-Za-z\\d-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$"
 											}
 										]
 									},
diff --git a/frontend/js/app/nginx/access/form.ejs b/frontend/js/app/nginx/access/form.ejs
index 79220b14b..ab4cb8aaf 100644
--- a/frontend/js/app/nginx/access/form.ejs
+++ b/frontend/js/app/nginx/access/form.ejs
@@ -78,6 +78,10 @@
                         <a target="_blank" href="https://nginx.org/en/docs/http/ngx_http_access_module.html">
                             Nginx HTTP Access
                         </a>
+                        or 
+                        <a target="_blank" href="https://en.wikipedia.org/wiki/Dynamic_DNS">
+                            DDNS
+                        </a>
                     </p>
                     <div class="clients"><!-- clients --></div>
                     <div class="row">