1.0.0 Release IaaS

This commit is contained in:
2026-03-15 04:41:02 +09:00
commit a7365da431
292 changed files with 36059 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
Tags: #os, #configuration, #network, #virtualization
## Preparation
### Create VM template
- ~/data/config/scripts/net.sh
```bash
virt-install \
--boot uefi \
--name net \
--os-variant debian13 \
--vcpus 1 \
--memory 2048 \
--location /var/lib/libvirt/images/debian-13.0.0-amd64-netinst.iso \ # For serial installing, use `--location` instead of `--cdrom`
--disk pool=vm-images,size=34,format=qcow2,discard=unmap \
--network network=ovs-lan-net,portgroup=vlan10-access,model=virtio,mac=0A:49:6E:4D:01:00 \ # Use designated ovs port group
--graphics none \
--console pty,target_type=serial \
--extra-args "console=ttyS0,115200"
# After enter this command, then the console starts automatically
# Remove all annotation before you make the sh file.
```
### Debian installation
- Following [here](../03_common/03_01_debian_configuration.md) to install Debian.
- Debian installer supports serial mode regardless getty@ttyS0 service is enabled or not.
- Following [here](../03_common/03_02_iptables.md) to set iptables.
- Following [here](../03_common/03_04_crowdsec.md) to set CrowdSec
#### Serial console setting
After installation, use `ctrl + ]` to exit console. Before setting getty@ttyS0, you can't use serial console to access VM. Therefore, use IP address set on installation, and connect net server via ssh first, following the step to enable the getty.
### Modify VM template settings
After getty setting, shutdown net vm with `shutdown` in VM or `sudo virsh shutdown net` in hypervisor to turn off vm first.
```bash
virsh edit net
```
```xml
<!-- net -->
...
</vcpu>
<cputune>
<shares>512</shares>
</cputune>
<!-- cpu priority - 1024: default/2048: high/512: low -->
<!--
<disk type='file' device='cdrom'>
...
</disk>
# Remove booting disk
-->
```
```bash
virsh dumpxml net > ~/data/config/vms/dumps/net.xml
# Start net server with console
```
### Common setting
- net.service
```ini
# ~/data/config/services/net.service
# ~/.config/systemd/user/net.service
[Unit]
Description=net Auto Booting
After=network-online.target
Wants=network-online.target
Requires=opnsense.service
[Service]
Type=oneshot
# Maintain status as active
RemainAfterExit=yes
# CrowdSec should be set
ExecStartPre=%h/data/config/scripts/wait-for-it.sh 192.168.10.1:8080 -t 0
ExecStartPre=/bin/bash -c "sleep 15"
# Run the service
ExecStart=/usr/bin/virsh -c qemu:///system start net
# Stop the service
ExecStop=/usr/bin/virsh -c qemu:///system shutdown net
[Install]
WantedBy=default.target
```
```bash
ln -s ~/data/config/services/net.service ~/.config/systemd/user/net.service
systemctl --user daemon-reload
systemctl --user enable net.service
systemctl --user start net.service
```

View File

@@ -0,0 +1,387 @@
Tags: #os, #configuration, #network
## DDNS
### Secret management
- File:
- ~/data/config/secrets/.secret.yaml
- /etc/secrets/2001/ddns.env
- Edit `.secret.yaml` with `edit_secret.sh`
```yaml
# ~/data/config/secrets/.secret.yaml
# DDNS
DDNS:
ZONE_ID: 'encrypted value'
API_KEY: 'encrypted value'
```
```bash
extract_secret.sh ~/data/config/secrets/.secret.yaml -e "DDNS" > /run/user/$UID/tmp.env && sudo mv /run/user/$UID/tmp.env /etc/secrets/$UID/ddns.env && sudo chown $UID:root /etc/secrets/$UID/ddns.env && sudo chmod 400 /etc/secrets/$UID/ddns.env
```
### ddns.sh
- File: ~/data/config/scripts/ddns/ddns.sh
```bash
#!/bin/bash
# ~/data/config/scripts/ddns.sh
# Designate directory
# DIRECTORY="$HOME/data/config/scripts"
# Information
DOMAIN=""
TTL=180
C_TTL=86400
PROXIED="false"
DELETE_FLAG="false"
CURRENT_IP=""
# These will be injected by systemd
# ZONE_ID='.secret'
# API_KEY='.secret'
# usage() function
usage() {
echo "Usage: $0 -d \"domain\" [-t \"ttl\"] [-p] [-r] [-c]"
echo "-d <domain>: Specify the domain to update"
echo "-t <ttl>: Specify the TTL(Time to live)"
echo "-p: Specify the cloudflare proxy to use"
echo "-r: Delete the DNS record"
exit 1
}
# getopts to get arguments
while getopts "d:t:pr" opt; do
case $opt in
d)
DOMAIN="$OPTARG"
;;
t)
TTL="$OPTARG"
;;
p)
PROXIED="true"
;;
r)
DELETE_FLAG="true"
;;
\?) # unknown options
echo "Invalid option: -$OPTARG" >&2
usage
;;
:) # parameter required option
echo "Option -$OPTARG requires an argument." >&2
usage
;;
esac
done
# Get option and move to parameters - This has no functional thing, because it only use arguments with parameters
shift $((OPTIND - 1))
# Check necessary options
if [ -z "$DOMAIN" ]; then
echo "Error: -d option is required" >&2
usage
fi
if ! [[ "$TTL" =~ ^[0-9]+$ ]] || [ "$TTL" -le 0 ]; then
echo "Error: -t option (ttl) requires a number above 0." >&2
usage
fi
# log() function
log()
{
local text="$1"
echo -e "$(date "+%Y-%m-%d %H:%M:%S"): [ddns] $text"
}
# Make log directory
# if [ ! -d "$DIRECTORY/log" ]; then
# mkdir "$DIRECTORY/log"
# fi
# Check and create log file
# LOG_FILE="$DIRECTORY/log/ddns_$(date "+%Y-%m-%d").log"
# if [ ! -f "$LOG_FILE" ]; then
# log "Notice: log file is created"
# fi
# Check package
if ! command -v curl &> /dev/null; then
log "Error: curl package is needed"
exit
fi
if ! command -v jq &> /dev/null; then
log "Error: jq package is needed"
exit
fi
# API options
URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"
CONTENT_TYPE="Content-Type: application/json"
AUTHORIZATION="Authorization: Bearer $API_KEY"
# Current IP check
CURRENT_IP=$(curl -sf "https://ifconfig.me") ||\
CURRENT_IP=$(curl -sf "https://ifconfig.kr") ||\
CURRENT_IP=$(curl -sf "https://api.ipify.org")
if [ "$CURRENT_IP" == "" ]; then
log "Error: Can't get an IP"
exit
fi
# DNS functions
# get_dns_record() function
get_dns_record()
{
local type="$1"
local name="$2"
local response="$(
curl -s "$URL?type=$type&name=$name"\
-H "$CONTENT_TYPE"\
-H "$AUTHORIZATION")"
if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then
log "Error: Can't get dns record\"Reason: $response"
exit
else
echo "$response"
fi
}
# create_dns_record() function
create_dns_record()
{
local type="$1"
local name="$2"
local ttl="$3"
local comment="$4"
local content="$5"
local response="$(
curl -s "$URL"\
-X POST\
-H "$CONTENT_TYPE"\
-H "$AUTHORIZATION"\
-d "{
\"name\": \"$name\",
\"ttl\": $ttl,
\"type\": \"$type\",
\"comment\": \"$comment\",
\"content\": \"$content\",
\"proxied\": $PROXIED
}")"
if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then
log "Error: Can't create dns record\"Reason: $response"
exit
else
echo "$response"
fi
}
# update_dns_record() function
update_dns_record()
{
local type="$1"
local name="$2"
local ttl="$3"
local comment="$4"
local content="$5"
local id="$6"
local response=$(
curl -s "$URL/$id"\
-X PUT\
-H "$CONTENT_TYPE"\
-H "$AUTHORIZATION"\
-d "{
\"name\": \"$name\",
\"ttl\": $ttl,
\"type\": \"$type\",
\"comment\": \"$comment\",
\"content\": \"$content\",
\"proxied\": $PROXIED
}")
if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then
log "Error: Can't update dns record\"Reason: $response"
exit
else
echo "$response"
fi
}
# delete_dns_record() function
delete_dns_record()
{
local type="$1"
local id="$2"
local response=$(
curl -s "$URL/$id"\
-X DELETE\
-H "$CONTENT_TYPE"\
-H "$AUTHORIZATION"
)
if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then
log "Error: Can't delete dns record\"Reason: $response"
exit
else
echo "$response"
fi
}
# Get DNS A, and CNAME record
A_DNS_RECORD=$(get_dns_record "A" "$DOMAIN")
S_DNS_RECORD=$(get_dns_record "cname" "*.$DOMAIN")
W_DNS_RECORD=$(get_dns_record "cname" "www.$DOMAIN")
# Delete DNS record with Delete flag
if [ "$DELETE_FLAG" == "true" ]; then
FLAG="false"
if [ "$(echo $A_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then
A_DNS_ID="$(echo $A_DNS_RECORD | jq -r '.result[0].id')"
delete_dns_record "A" "$A_DNS_ID"
log "Delete: root DNS record is deleted"
FLAG="true"
fi
if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then
S_DNS_ID="$(echo $S_DNS_RECORD | jq -r '.result[0].id')"
delete_dns_record "cname" "$S_DNS_ID"
log "Delete: sub DNS record is deleted"
FLAG="true"
fi
if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then
W_DNS_ID="$(echo $W_DNS_RECORD | jq -r '.result[0].id')"
delete_dns_record "cname" "$W_DNS_ID"
log "Delete: www DNS record is deleted"
FLAG="true"
fi
if [ "$FLAG" == "false" ]; then
log "Notice: Nothing is Deleted. There are no DNS records"
fi
exit
fi
# Create or update DNS A record
if [ "$(echo $A_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # root DNS record exist
A_DNS_ID="$(echo $A_DNS_RECORD | jq -r '.result[0].id')"
A_DNS_CONTENT="$(echo $A_DNS_RECORD | jq -r '.result[0].content')"
A_DNS_TTL="$(echo $A_DNS_RECORD | jq -r '.result[0].ttl')"
A_DNS_PROXIED="$(echo $A_DNS_RECORD | jq -r '.result[0].proxied')"
if [ "$A_DNS_CONTENT" != $CURRENT_IP -o "$A_DNS_TTL" != "$TTL" -o "$A_DNS_PROXIED" != "$PROXIED" ]; then
update_dns_record "A" "$DOMAIN" "$TTL" "$(date "+%Y-%m-%d %H:%M:%S"): root domain from ddns.sh" "$CURRENT_IP" "$A_DNS_ID"
log "Update: Root DNS record is successfully changed\nDomain: $DOMAIN\nIP: $A_DNS_CONTENT to $CURRENT_IP\nTTL: $A_DNS_TTL to $TTL\nproxied: $A_DNS_PROXIED to $PROXIED"
else
log "Notice: Root DNS record is not changed\nDomain: $DOMAIN\nIP: $CURRENT_IP\nTTL: $TTL\nproxied: $PROXIED"
fi
else # root DNS record does not exist
create_dns_record "A" "$DOMAIN" "$TTL" "$(date "+%Y-%m-%d %H:%M:%S"): root domain from ddns.sh" "$CURRENT_IP"
log "Create: Root DNS record is successfully created\nDomain: $DOMAIN\nIP: $CURRENT_IP\nTTL: $TTL\nproxied: $PROXIED"
fi
# Create or update DNS CNAME records
if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # sub DNS record exist
S_DNS_ID="$(echo $S_DNS_RECORD | jq -r '.result[0].id')"
S_DNS_CONTENT="$(echo $S_DNS_RECORD | jq -r '.result[0].content')"
S_DNS_TTL="$(echo $S_DNS_RECORD | jq -r '.result[0].ttl')"
S_DNS_PROXIED="$(echo $S_DNS_RECORD | jq -r '.result[0].proxied')"
if [ "$S_DNS_CONTENT" != "$DOMAIN" -o "$S_DNS_TTL" != "$C_TTL" -o "$S_DNS_PROXIED" != "$PROXIED" ]; then
update_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" "$S_DNS_ID"
log "Update: Sub DNS record is successfully changed\nDomain: $S_DNS_CONTENT to *.$DOMAIN\ncname: $DOMAIN \nTTL: $S_DNS_TTL to $C_TTL\nproxied: $S_DNS_PROXIED to $PROXIED"
else
log "Notice: Sub DNS record is not changed\nDomain: *.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED"
fi
else # sub DNS record does not exist
create_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN"
log "Create: Sub DNS record is successfully created\nDomain: *.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED"
fi
if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # www DNS record exist
W_DNS_ID="$(echo $W_DNS_RECORD | jq -r '.result[0].id')"
W_DNS_CONTENT="$(echo $W_DNS_RECORD | jq -r '.result[0].content')"
W_DNS_TTL="$(echo $W_DNS_RECORD | jq -r '.result[0].ttl')"
W_DNS_PROXIED="$(echo $W_DNS_RECORD | jq -r '.result[0].proxied')"
if [ "$W_DNS_CONTENT" != "$DOMAIN" -o "$W_DNS_TTL" != "$C_TTL" -o "$W_DNS_PROXIED" != "$PROXIED" ]; then
update_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" "$W_DNS_ID"
log "Update: www DNS record is successfully changed\nDomain: $W_DNS_CONTENT to www.$DOMAIN\ncname: $DOMAIN\nTTL: $W_DNS_TTL to $C_TTL\nproxied: $W_DNS_PROXIED to $PROXIED"
else
log "Notice: www DNS record is not changed\nDomain: www.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED"
fi
else # www DNS record does not exist
create_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN"
log "Create: www DNS record is successfully created\nDomain: www.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED"
fi
# Remove old backup file (7days before)
# find "$DIRECTORY/log" -maxdepth 1 -type f -mtime +7 -delete
```
### Systemd
- File:
- ~/data/config/services/ddns/ddns.service
- ~/data/config/services/ddns/ddns.timer
- /etc/secrets/2001/ddns.env
```ini
# ~/data/config/services/ddns/ddns.service
# ~/.config/systemd/user/ddns.service
[Unit]
Description=DDNS Update Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
# logging
StandardOutput=journal
StandardError=journal
# EnvironmentFile
EnvironmentFile=/etc/secrets/%U/ddns.env
# Run the script
ExecStart=/bin/bash -c '%h/data/config/scripts/ddns/ddns.sh -d "ilnmors.com"'
```
```ini
# ~/data/config/services/ddns/ddns.timer
# ~/.config/systemd/user/ddns.timer
[Unit]
Description=Run DDNS update service every 5 minutes
[Timer]
# Execute service after 1 min on booting
OnBootSec=1min
# Execute service every 5mins
OnUnitActiveSec=5min
# When timer is activated, Service also starts.
Persistent=true
[Install]
WantedBy=timers.target
```
```bash
# Register service
mkdir -p ~/.config/systemd/user && chmod -R 700 ~/.config
ln -s ~/data/config/services/ddns/ddns.service ~/.config/systemd/user/ddns.service
ln -s ~/data/config/services/ddns/ddns.timer ~/.config/systemd/user/ddns.timer
systemctl --user daemon-reload
# Start timer and enable
systemctl --user enable --now ddns.timer
```

View File

@@ -0,0 +1,338 @@
Tags: #os, #configuration, #network, #virtualization, #container
## BIND9
BIND9 is an open source authoritative DNS software of ISC. It can work as a personal authoritative in private network environment.
### Secret management
- File:
- ~/data/config/secrets/.secret.yaml
- Edit `.secret.yaml` with `edit_secret.sh`
#### Generate TSIG key as ACME key
```bash
podman run --rm --entrypoint /bin/sh internetsystemsconsortium/bind9:9.20 -c "tsig-keygen -a hmac-sha256 acme-key"
# Paste and add on .secret.yaml
# key "acme-key" {
#     algorithm hmac-sha256;
#     secret "your_base64_tsig_secret_here";
# };'
```
```yaml
# ~/data/config/secrets/.secret.yaml
BIND9_ACME_KEY: |
key "acme-key" {
    algorithm hmac-sha256;
    secret "your_base64_tsig_secret_here";
};
```
```bash
# Copy the secret value from .secret.yaml
# Podman secret
extract_secret.sh ~/data/config/secrets/.secret.yaml -f "BIND9_ACME_KEY" | podman secret create "acme-key" -
```
### Preparation
#### iptables and firewall rules
- Set iptables first, following [here](../03_common/03_02_iptables.md).
- Set firewall rules first, following [here](Latest/05_firewall/05_04_opnsense_rules.md).
#### Create directory for container
```bash
mkdir -p ~/data/containers/bind
chmod 700 ~/data/containers/bind
sudo setfacl -m d:g::0 ~/data/containers/bind
sudo setfacl -m d:o::0 ~/data/containers/bind
sudo setfacl -m u:net:rwx ~/data/containers/bind
sudo setfacl -m u:100052:rwx ~/data/containers/bind
sudo setfacl -d -m u:net:rwx ~/data/containers/bind
sudo setfacl -d -m u:100052:rwx ~/data/containers/bind
mkdir -p ~/data/containers/bind/{cache,etc,lib,log}
nano ~/data/containers/bind/{etc,lib}/configuration_files
```
> BIND9 container executes as 53:53(bind:bind) permission in container. It is mapped host's 100052. Therefore, directories have to have ACL via `setfacl`
### Podman Image
```bash
podman pull internetsystemsconsortium/bind9:9.20 # Do not use latest version to management
```
### Configuration files
> If `named.conf` file didn't exist in `/etc/bind` or it had error in it, the container would be terminated without any logs. Before starting BIND container, you should makes all configuration file already.
#### named.conf
- file: ~/data/containers/bind/etc/named.conf
```ini
include "/run/secrets/keys/acme-key";
include "/run/secrets/keys/ddns-key";
options {
directory "/var/cache/bind";
listen-on { any; };
listen-on-v6 { ::1; };
// Authoritative DNS setting
allow-recursion { none; };
allow-transfer { none; };
allow-update { none; };
dnssec-validation no;
check-names master warn;
};
zone "ilnmors.internal." {
type primary;
file "/var/lib/bind/db.ilnmors.internal";
notify yes;
// ACME-01 challenge policy. It allows only subdomain's TXT record update.
update-policy {
grant acme-key subdomain ilnmors.internal. TXT;
grant ddns-key subdomain ilnmors.internal. A AAAA DHCID;
};
};
zone "1.168.192.in-addr.arpa" {
type primary;
file "/var/lib/bind/db.1.168.192.in-addr.arpa";
notify yes;
update-policy {
grant ddns-key subdomain 1.168.192.in-addr.arpa PTR DHCID;
};
};
zone "10.168.192.in-addr.arpa" {
type primary;
file "/var/lib/bind/db.10.168.192.in-addr.arpa";
notify yes;
update-policy {
grant ddns-key subdomain 10.168.192.in-addr.arpa PTR DHCID;
};
};
zone "ilnmors.com." {
//split horizon dns
type primary;
file "/var/lib/bind/db.ilnmors.com";
notify yes;
};
logging {
channel default_log {
stderr;
severity info;
};
category default { default_log; };
category config { default_log; };
category queries { default_log; };
};
```
>If ddns function were required, generate tsig key for ddns and give update policy for each zone like below.
>
> - include "/etc/bind/ddns-key";
> - update-policy { grant ddns-key subdomain \[zone_domain\] ANY A AAAA TXT; };
- Verify the named.conf with the command `named-checkconf`
#### Zone files
> When you add the record, you should use `.` at the end of the domain. i.e. mydomain.internal.
- file:
- ~/data/containers/bind/lib/db.ilnmors.internal
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
2025113001 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
bind IN A 192.168.10.11
opnsense IN A 192.168.10.1
vmm IN A 192.168.10.10
net IN A 192.168.10.11
crowdsec IN CNAME opnsense.ilnmors.internal.
adguard IN CNAME net.ilnmors.internal.
step-ca IN CNAME auth.ilnmors.internal.
caddy IN CNAME auth.ilnmors.internal.
ldap IN CNAME auth.ilnmors.internal.
authelia IN CNAME auth.ilnmors.internal.
code-server IN CNAME dev.ilnmors.internal.
postgresql IN CNAME dev.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.1.168.192.in-addr.arpa
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
1 IN PTR opnsense.ilnmors.internal.
11 IN PTR console.ilnmors.internal.
30 IN PTR printer.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.10.168.192.in-addr.arpa
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
1 IN PTR opnsense.ilnmors.internal.
10 IN PTR vmm.ilnmors.internal.
11 IN PTR net.ilnmors.internal.
12 IN PTR auth.ilnmors.internal.
13 IN PTR dev.ilnmors.internal.
14 IN PTR app.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.ilnmors.com
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
* IN A 192.168.10.12
```
```bash
# Adguard container has Requires=bind.service. When it restarted, then Adguard also restarted.
systemctl --user restart bind
```
### Quadlet
- File:
- ~/data/config/containers/bind/bind.container
```ini
# ~/data/config/containers/bind/bind.container
[Quadlet]
DefaultDependencies=false
[Unit]
Description=BIND9 DNS
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/internetsystemsconsortium/bind9:9.20
ContainerName=bind
PublishPort=2253:53/tcp
PublishPort=2253:53/udp
Volume=%h/data/containers/bind/etc:/etc/bind:rw
Volume=%h/data/containers/bind/lib:/var/lib/bind:rw
Volume=%h/data/containers/bind/cache:/var/cache/bind:rw
Volume=%h/data/containers/bind/log:/var/log:rw
Environment="TZ=Asia/Seoul"
Secret=acme-key,target=/run/secrets/key/acme-key
Secret=ddns-key,target=/run/secrets/key/ddns-key
Label=diun.enable=true
Label=diun.watch_repo=true
[Install]
WantedBy=default.target
```
#### Create systemd `.service` file
```bash
# linger has to be activated
mkdir -p ~/.config/containers/systemd
ln -s ~/data/config/containers/bind/bind.container ~/.config/containers/systemd/bind.container
# This command makes bind.service
systemctl --user daemon-reload
```
#### Enable and start service
```bash
systemctl --user start bind.service
```
### nsupdate (RFC 2136) verification
```bash
# Update query at net server
# Before test, create temp file .bind.acme-key
nsupdate -k /run/user/$UID/.bind.acme-key
> server 127.0.0.1 2253
> zone ilnmors.internal
> update add _acme-challenge.ilnmors.internal. 60 TXT "validation-test"
> send
> `ctrl+c`
# Verify
dig @127.0.0.1 -p 2253 _acme-challenge.ilnmors.internal. TXT
# Print
;; QUESTION SECTION:
;_acme-challenge.ilnmors.internal. IN TXT
;; ANSWER SECTION:
_acme-challenge.ilnmors.internal. 60 IN TXT "validation-test"
# Delete query at net server
nsupdate -k /run/user/$UID/.bind.acme-key
> server 127.0.0.1 2253
> zone ilnmors.internal
> update delete _acme-challenge.ilnmors.internal. 60 TXT "validation-test"
> send
> `ctrl+c`
# Verify
dig @127.0.0.1 -p 2253 _acme-challenge.ilnmors.internal. TXT
# Print
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 17572
```

View File

@@ -0,0 +1,293 @@
Tags: #os, #configuration, #network, #virtualization, #container
## AdGuard Home
AdGuard Home is one of open sourced recursive DNS resolver which can block malicious domain. It supports powerful DNS query filter based on ruleset and split horizon DNS service, plus recursive query on DoH and DoT towards public or internal authoritative DNS server.
### Secret
Adgaurd Home container doesn't need any secret value.
### Preparation
#### Add new domain in BIND
Following [here](../06_network/06_03_net_bind.md).
- net server
- file: ~/data/containers/bind/lib/db.ilnmors.internal
```ini
# ...
adguard IN CNAME net.ilnmors.internal.
# ...
```
```bash
# Adguard container has Requires=bind.service. When it restarted, then Adguard also restarted.
systemctl --user restart bind
```
#### iptables and firewall rules
- Set iptables first, following [here](../03_common/03_02_iptables.md).
- Set firewall rules first, following [here](Latest/05_firewall/05_04_opnsense_rules.md).
#### Create directory for container
```bash
mkdir -p ~/data/containers/adguard
chmod 700 ~/data/containers/adguard
sudo setfacl -m d:g::0 ~/data/containers/adguard
sudo setfacl -m d:o::0 ~/data/containers/adguard
mkdir -p ~/data/containers/adguard/{work,config,certs}
```
> AdGuard Home container executes as root permission in container. It is not necessary to give `setfacl`. Because the container's root account is mapped as host's uid.
### Podman Image
```bash
podman pull adguard/adguardhome:v0.107.68 # Do not use latest version to management
```
### Quadlet
- File:
- ~/data/config/containers/adguard/adguard.container
```ini
# ~/data/config/containers/adguard/adguard.container
[Quadlet]
DefaultDependencies=false
[Unit]
Description=AdGuard Home DNS
After=bind.service
Requires=bind.service
[Container]
Image=docker.io/adguard/adguardhome:v0.107.68
ContainerName=adguard
AddHost=bind.ilnmors.internal:host-gateway
PublishPort=2053:53/tcp
PublishPort=2053:53/udp
PublishPort=2443:443/tcp
PublishPort=2443:443/udp
PublishPort=3000:3000/tcp
# 3000 is temporary port, After TLS setting, delete it.
Volume=%h/data/containers/adguard/work:/opt/adguardhome/work:rw
Volume=%h/data/containers/adguard/config:/opt/adguardhome/conf:rw
Volume=%h/data/containers/adguard/certs:/etc/ssl/adguard:ro
Environment="TZ=Asia/Seoul"
Label=diun.enable=true
Label=diun.watch_repo=true
[Install]
WantedBy=default.target
```
#### Create systemd `.service` file
```bash
ln -s ~/data/config/containers/adguard/adguard.container ~/.config/containers/systemd/adguard.container
systemctl --user daemon-reload
```
#### Enable and start service
```bash
systemctl --user start adguard.service
```
### Web UI configuration
#### Access web UI and initial setting
- URL: http://192.168.10.11:3000
> After TLS certificate setting, access to adguard via https `https://192.168.10.11` or `https://adguard.ilnmors.internal`
#### Initial setting wizard
- Administrator interface
- Network interface: all
- Interface port: 3000
- DNS server
- Network interface: all
- Interface port: 53
- Authentication
- User: adguard
- Password: Password
### AdGuard Configuration
#### General setting
- Filter update interval: 1 hour
- \[\*\] Use AdGuard browsing security web service
- \[\*\] Enable log
- Query logs rotation - 90 days
- \[\*\] Enable statistics
- `Save`
#### DNS settings
- Upstream DNS servers
```text
# Cloudflare DNS
tls://security.cloudflare-dns.com
# Internal DNS bind.ilnmors.internal:2253
[/ilnmors.internal/]udp://bind.ilnmors.internal:2253
# Split Horizon DNS
[/*.ilnmors.com/]udp://bind.ilnmors.internal:2253
```
> Internal authoritative DNS will be BIND. BIND will use 2253 port. `bind.ilnmors.internal` is defined on container's `/etc/hosts`, it allows communication with host system.
- \[\*\] Parallel requests
- Private reverse DNS servers
```text
udp://bind.ilnmors.internal:2253
```
> Set this option after BIND prepared
- \[\*\] Use private reverse DNS resolvers
- \[\*\] Enable reverse resolving of client's IP addresses
- `Apply` and `Test upstreams`
- \[ \] Enable cache
- `Save`
- Disallowed domains
```text
...
wpad.ilnmors.internal
_ldap._tcp.dc._msdcs.ilnmors.internal
```
- `Save configuration`
#### Client settings
- Add Client
| Client | Name |
| :----------------------------------------------: | :-------: |
| 169.254.1.2, 192.168.10.11 | localhost |
| 10.10.10.2, 10.10.10.3, 10.10.10.4, 192.168.1.11 | console |
#### Filters - DNS blocklists
- Add blocklist - Choose from the list
- 1Hosts (Lite)
- AdGuard DNS filter
- AdAway DNS Popup Hosts filter
- HaGeZi's Ultimate Blocklist
- OISD Blocklist Big
- KOR: List-KR DNS
#### DNS rewrites
- Filters:DNS rewrites
- bind.ilnmors.internal - 192.168.10.11
#### Clients' DNS setting
##### OPNsense
- System:Settings:General
- DNS server: 192.168.10.11
- Services:KEA DHCP:KEA DHCPv4:Subnets
- DNS server: 192.168.10.11
##### Hypervisor
- /etc/resolv.conf
- nameserver 192.168.10.11
##### net
- /etc/resolv.conf
- nameserver 192.168.10.11
##### VPN client
```ini
[Interface]
DNS = 192.168.10.11
```
---
#### Encryption setting
> It requires `BIND` and `Step-CA`
- \[\*\] Enable Encryption (HTTPS, DNS-over_HTTPS, and DNS-over-TLS)
- \[\*\] Enable plain DNS
- \[\*\] Redirect to HTTPS automatically
- HTTPS port: 443
##### Certificates
> AdGuard home doesn't support ACME protocol by itself. It is necessary to use OPNsense's ACME client function to automate adguard home certificates.
- Add new domain in BIND, Following [here](../06_network/06_03_net_bind.md).
- net server
- file: ~/data/containers/bind/lib/db.ilnmors.internal
```text
...
adguard IN CNAME net.ilnmors.internal.
...
```
- ACME setting (OPNsense)
- Services:ACME Client:Certificates - Certificates - \[+\]
- Common Name: adguard.ilnmors.internal
- Description: adguard
- ACME Account: acme.ilnmors.internal
> Even though provisioner's name includes `@`, it has to use as `.`.
>
> i.e. `acme@ilnmors.internal` > `acme.ilnmors.internal`
- Challenge Type: ilnmors.internal-dns-01-challenge
- \[\*\] Auto Renewal
- Automations: adguard-auto-acme, adguard-auto-restart
- Automations (OPNsense)
- Services:ACME Client:Automations - Automation - \[+\]
- Name: adguard-auto-acme / adguard-auto-restart
- Description: adguard acme crt issue / restart adguard after crt is issued
- Run Command: Upload certificate via SFTP / Remote command via SSH
- SFTP Host: adguard.ilnmors.internal
- Username: net
- Identity Type: ed25519
- Remote Path(SFTP): /home/net/data/containers/adguard/certs
- Command(SSH): systemctl --user restart adguard
- `Show Identity`
> Copy Required parameters `ssh-ed25519 ~~~ root@opnsense.ilnmors.internal`
>
> Add parameters in net server's ~/.ssh/authorized_keys
- `Test Connect` and `Save`
- \[\*\] Set a certificates file path: `/etc/ssl/adguard/adguard.ilnmors.internal/fullchain.pem`
- \[\*\] Set a private key file: `/etc/ssl/adguard/adguard.ilnmors.internal/key.pem`
- \[Save configuration\]
##### Modify container file
- Delete `PublishPort=3000:3000/tcp` part
- `systemctl --user daemon-reload`
- `systemctl --user start adguard`

View File

@@ -0,0 +1,630 @@
Tags: #os, #configuration, #network, #virtualization, #container
# KEA
#### Generate TSIG key as DDNS key
```bash
podman run --rm --entrypoint /bin/sh internetsystemsconsortium/bind9:9.20 -c "tsig-keygen -a hmac-sha256 acme-key"
# Paste and add on .secret.yaml
# key "acme-key" {
#     algorithm hmac-sha256;
#     secret "your_base64_tsig_secret_here";
# };'
```
### DHCPv4
#### Add user to group
```bash
sudo apt install kea-dhcp4-server kea-dhcp-ddns-server
sudo usermod -aG _kea net
sudo chmod 770 /etc/kea /var/lib/kea
sudo chmod 660 /etc/kea/kea-dhcp4.conf
sudo mkdir /etc/secrets/$(id -u _kea)
sudo chown _kea:root /etc/secrets/$(id -u _kea)
sudo chmod 500 /etc/secrets/$(id -u _kea)
sudo touch /etc/secrets/$(id -u _kea)/ddns-key
sudo nano /etc/secrets/$(id -u _kea)/ddns-key
sudo chown _kea:root /etc/secrets/$(id -u _kea)/ddns-key
sudo chmod 400 /etc/secrets/$(id -u _kea)/ddns-key
```
iptables
```
*mangle
*filter
-A INPUT -p udp -m udp --dport 67 -m comment --comment "allow upd DHCPv4 connection" -j ACCEPT
# check
sudo bash -c 'iptables-restore --test < /etc/iptables/rules.v4'
```
kea-dhcp4.conf
```json
{
"Dhcp4": {
"subnet4": [
{
"subnet": "192.168.10.0/24",
"pools" : [
{
"pool": "192.168.10.254-192.168.10.254"
}
],
"option-data": [
{
"name": "routers",
"data": "192.168.10.1"
},
{
"name": "ntp-servers",
"data": "192.168.10.1"
},
{
"name": "domain-name-servers",
"data": "192.168.10.11"
},
{
"name": "domain-name",
"data": "ilnmors.internal"
}
],
"reservations": [
{
"hw-address": "0a:49:6e:4d:02:00",
"ip-address": "192.168.10.12",
"hostname": "auth"
},
{
"hw-address": "0a:49:6e:4d:03:00",
"ip-address": "192.168.10.13",
"hostname": "dev"
},
{
"hw-address": "0a:49:6e:4d:04:00",
"ip-address": "192.168.10.14",
"hostname": "app"
}
],
"id": 1,
"interface": "enp1s0"
},
{
"subnet": "192.168.1.0/24",
"relay": {
"ip-addresses": [ "192.168.1.1" ]
},
"pools": [
{
"pool": "192.168.1.100-192.168.1.254"
}
],
"option-data": [
{
"name": "routers",
"data": "192.168.1.1"
},
{
"name": "ntp-servers",
"data": "192.168.1.1"
},
{
"name": "domain-name-servers",
"data": "192.168.10.11"
},
{
"name": "domain-name",
"data": "ilnmors.internal"
}
],
"reservations": [
{
"hw-address": "d8:e2:df:ff:1b:d5",
"ip-address": "192.168.1.11",
"hostname": "surface"
},
{
"hw-address": "38:ca:84:94:5e:06",
"ip-address": "192.168.1.30",
"hostname": "printer"
}
],
"id": 2,
"interface": "enp1s0"
}
],
"interfaces-config": {
"interfaces": [
"enp1s0"
],
"dhcp-socket-type": "raw",
"service-sockets-max-retries": 5,
"service-sockets-require-all": true
},
"renew-timer": 1000,
"rebind-timer": 2000,
"valid-lifetime": 4000,
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "stdout"
}
],
"severity": "INFO"
}
],
"lease-database": {
"type": "memfile",
"persist": true,
"name": "/var/lib/kea/kea-leases4.csv",
"lfc-interval": 3600
},
"dhcp-ddns": {
"enable-updates": false,
"server-ip": "127.0.0.1",
"server-port": 53001
"ncr-protocol": "UDP"
"ncr-format": "JSON"
},
"ddns-send-updates": true,
"ddns-update-on-renew": true,
"ddns-qualifying-suffix": "ilnmors.internal",
"ddns-override-no-update": true,
"ddns-override-client-update": true,
"ddns-replace-client-name": "when-present",
"ddns-generated-prefix": "host"
"hostname-char-set": "[^a-zA-Z0-9.-]",
"hostname-char-replacement": "-"
}
}
// There is one interface for DHCP host. In this case, the subnet which has dhcp server should be `id=1`. Because Kea DHCP allocate IP from the `id=1` subnet when it recieve pacekt has no giaddr. When Kea DHCP is running on Router, then id is not important because DHCP allocate IP based on their interface.
```
#### OPNsense
- Services:Kea DHCP:Kea DHCPv4
- \[ \] Enabled
- Services:DHCP Relay:Configuration
- Destination - \[+\]
- Name: kea-dhcp-v4
- Server: 192.168.10.11
- Relays - \[+\]
- \[\*\] Enabled
- Interface: LAN
- Desitination kea-dhcp-v4
- \[ \]Agent Information
- Status `green box` check
### DDNS
```bash
sudo nano /etc/apparmor.d/local/usr.sbin.kea-dhcp-ddns
# /etc/secrets/** r,
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.kea-dhcp-ddns
sudo systemctl restart kea-dhcp-ddns-server.service
```
/etc/secrets/102/ddns-key
```json
[
{
"name": "ddns-key",
"algorithm": hmac-sha256;
"secret": "secret_value"
}
]
```
/etc/kea/kea-dhcp-ddns.conf
```json
{
"DhcpDdns":
{
"ip-address": "127.0.0.1",
"port": 53001,
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea-ddns-ctrl-socket"
},
"tsig-keys": <?include "/etc/secrets/102/ddns-key"?>,
"forward-ddns" : {
"ddns-domains": [
{
"name": "ilnmors.internal.",
"key-name": "ddns-key",
"dns-servers": [
{
"ip-address": "127.0.0.1",
"port": 2053
}
]
}
]
},
"reverse-ddns" : {
"ddns-domains": [
{
"name": "10.168.192.in-addr.arpa.",
"key-name": "ddns-key",
"dns-servers": [
{
"ip-address": "127.0.0.1",
"port": 2053
}
]
},
{
"name": "1.168.192.in-addr.arpa.",
"key-name": "ddns-key",
"dns-servers": [
{
"ip-address": "127.0.0.1",
"port": 2053
}
]
}
]
},
"loggers": [
{
"name": "kea-dhcp-ddns",
"output-options": [
{
"output": "stdout"
}
],
"severity": "INFO",
}
]
}
}
```
kea-dhcp4.conf
``` json
// ...
"dhcp-ddns": {
"enable-updates": true,
// ...
}
```
---
## BIND9
BIND9 is an open source authoritative DNS software of ISC. It can work as a personal authoritative in private network environment.
### Secret management
- File:
- ~/data/config/secrets/.secret.yaml
- Edit `.secret.yaml` with `edit_secret.sh`
#### Generate TSIG key as ACME key
```bash
podman run --rm --entrypoint /bin/sh internetsystemsconsortium/bind9:9.20 -c "tsig-keygen -a hmac-sha256 acme-key"
# Paste and add on .secret.yaml
# key "acme-key" {
#     algorithm hmac-sha256;
#     secret "your_base64_tsig_secret_here";
# };'
```
```yaml
# ~/data/config/secrets/.secret.yaml
BIND9_ACME_KEY: |
key "acme-key" {
    algorithm hmac-sha256;
    secret "your_base64_tsig_secret_here";
};
```
```bash
# Copy the secret value from .secret.yaml
# Podman secret
extract_secret.sh ~/data/config/secrets/.secret.yaml -f "BIND9_ACME_KEY" | podman secret create "acme-key" -
```
### Preparation
#### iptables and firewall rules
- Set iptables first, following [here](../03_common/03_02_iptables.md).
- Set firewall rules first, following [here](Latest/05_firewall/05_04_opnsense_rules.md).
#### Create directory for container
```bash
mkdir -p ~/data/containers/bind
chmod 700 ~/data/containers/bind
sudo setfacl -m d:g::0 ~/data/containers/bind
sudo setfacl -m d:o::0 ~/data/containers/bind
sudo setfacl -m u:net:rwx ~/data/containers/bind
sudo setfacl -m u:100052:rwx ~/data/containers/bind
sudo setfacl -d -m u:net:rwx ~/data/containers/bind
sudo setfacl -d -m u:100052:rwx ~/data/containers/bind
mkdir -p ~/data/containers/bind/{cache,etc,lib,log}
nano ~/data/containers/bind/{etc,lib}/configuration_files
```
> BIND9 container executes as 53:53(bind:bind) permission in container. It is mapped host's 100052. Therefore, directories have to have ACL via `setfacl`
### Podman Image
```bash
podman pull internetsystemsconsortium/bind9:9.20 # Do not use latest version to management
```
### Configuration files
> If `named.conf` file didn't exist in `/etc/bind` or it had error in it, the container would be terminated without any logs. Before starting BIND container, you should makes all configuration file already.
#### named.conf
- file: ~/data/containers/bind/etc/named.conf
```ini
include "/etc/bind/key/acme-key";
options {
directory "/var/cache/bind";
listen-on { any; };
listen-on-v6 { ::1; };
// Authoritative DNS setting
allow-recursion { none; };
allow-transfer { none; };
allow-update { none; };
dnssec-validation no;
check-names master warn;
};
zone "ilnmors.internal." {
type primary;
file "/var/lib/bind/db.ilnmors.internal";
notify yes;
// ACME-01 challenge policy. It allows only subdomain's TXT record update.
update-policy {
grant acme-key subdomain ilnmors.internal. TXT;
};
};
zone "1.168.192.in-addr.arpa" {
type primary;
file "/var/lib/bind/db.1.168.192.in-addr.arpa";
notify yes;
};
zone "10.168.192.in-addr.arpa" {
type primary;
file "/var/lib/bind/db.10.168.192.in-addr.arpa";
notify yes;
};
zone "ilnmors.com." {
//split horizon dns
type primary;
file "/var/lib/bind/db.ilnmors.com";
notify yes;
};
logging {
channel default_log {
stderr;
severity info;
};
category default { default_log; };
category config { default_log; };
category queries { default_log; };
};
```
>If ddns function were required, generate tsig key for ddns and give update policy for each zone like below.
>
> - include "/etc/bind/ddns-key";
> - update-policy { grant ddns-key subdomain \[zone_domain\] ANY A AAAA TXT; };
- Verify the named.conf with the command `named-checkconf`
#### Zone files
> When you add the record, you should use `.` at the end of the domain. i.e. mydomain.internal.
- file:
- ~/data/containers/bind/lib/db.ilnmors.internal
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
bind IN A 192.168.10.11
opnsense IN A 192.168.10.1
vmm IN A 192.168.10.10
net IN A 192.168.10.11
auth IN A 192.168.10.12
dev IN A 192.168.10.13
app IN A 192.168.10.14
console IN A 192.168.1.11
printer IN A 192.168.1.30
crowdsec IN CNAME opnsense.ilnmors.internal.
adguard IN CNAME net.ilnmors.internal.
step-ca IN CNAME auth.ilnmors.internal.
caddy IN CNAME auth.ilnmors.internal.
ldap IN CNAME auth.ilnmors.internal.
postgresql IN CNAME dev.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.1.168.192.in-addr.arpa
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
1 IN PTR opnsense.ilnmors.internal.
11 IN PTR console.ilnmors.internal.
30 IN PTR printer.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.10.168.192.in-addr.arpa
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
1 IN PTR opnsense.ilnmors.internal.
10 IN PTR vmm.ilnmors.internal.
11 IN PTR net.ilnmors.internal.
12 IN PTR auth.ilnmors.internal.
13 IN PTR dev.ilnmors.internal.
14 IN PTR app.ilnmors.internal.
```
- ~/data/containers/bind/lib/db.ilnmors.com
```ini
$TTL 86400
@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. (
yyyymmdd01 ; serial
3600 ; refresh (1 hour)
1800 ; retry (30 minutes)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
IN NS bind.ilnmors.internal.
* IN A 192.168.10.12
```
```bash
# Adguard container has Requires=bind.service. When it restarted, then Adguard also restarted.
systemctl --user restart bind
```
### Quadlet
- File:
- ~/data/config/containers/bind/bind.container
```ini
# ~/data/config/containers/bind/bind.container
[Quadlet]
DefaultDependencies=false
[Unit]
Description=BIND9 DNS
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/internetsystemsconsortium/bind9:9.20
ContainerName=bind
PublishPort=2253:53/tcp
PublishPort=2253:53/udp
Volume=%h/data/containers/bind/etc:/etc/bind:rw
Volume=%h/data/containers/bind/lib:/var/lib/bind:rw
Volume=%h/data/containers/bind/cache:/var/cache/bind:rw
Volume=%h/data/containers/bind/log:/var/log:rw
Environment="TZ=Asia/Seoul"
Secret=acme-key,target=/etc/bind/key/acme-key
Label=diun.enable=true
Label=diun.watch_repo=true
[Install]
WantedBy=default.target
```
#### Create systemd `.service` file
```bash
# linger has to be activated
mkdir -p ~/.config/containers/systemd
ln -s ~/data/config/containers/bind/bind.container ~/.config/containers/systemd/bind.container
# This command makes bind.service
systemctl --user daemon-reload
```
#### Enable and start service
```bash
systemctl --user start bind.service
```
### nsupdate (RFC 2136) verification
```bash
# Update query at net server
# Before test, create temp file .bind.acme-key
nsupdate -k /run/user/$UID/.bind.acme-key
> server 127.0.0.1 2253
> zone ilnmors.internal
> update add _acme-challenge.ilnmors.internal. 60 TXT "validation-test"
> send
> `ctrl+c`
# Verify
dig @127.0.0.1 -p 2253 _acme-challenge.ilnmors.internal. TXT
# Print
;; QUESTION SECTION:
;_acme-challenge.ilnmors.internal. IN TXT
;; ANSWER SECTION:
_acme-challenge.ilnmors.internal. 60 IN TXT "validation-test"
# Delete query at net server
nsupdate -k /run/user/$UID/.bind.acme-key
> server 127.0.0.1 2253
> zone ilnmors.internal
> update delete _acme-challenge.ilnmors.internal. 60 TXT "validation-test"
> send
> `ctrl+c`
# Verify
dig @127.0.0.1 -p 2253 _acme-challenge.ilnmors.internal. TXT
# Print
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 17572
```