TT Lab
Get started
Learn Learning paths Courses

Air-Gapped Mirrors and a Private CA

Stand up internal DNS and a time server in an air gap

Continue in TT Lab

This lab runs on a real VM

It is a single Ubuntu 24.04 VM. The preparation step has created a network namespace client — inside the same VM but separate as far as the network goes, it is "another server inside the air-gapped network", connected to the VM (10.203.0.1) by a veth (10.203.0.2). To type commands inside it, use ip netns exec client <명령> (the placeholder is the command). dnsmasq, chrony, and dig are installed (the dnsmasq service is left failed because of a port 53 conflict — that diagnosis is covered in the network-basics course). Grading is done by an agent that comes in from outside to port 8899 of the VM, so blocking already-established connections cuts off grading.

Goal

Stand up an internal DNS and time server in a network that cannot reach the internet, make another server use them, and finish with a handover check script.

Why it matters

The internal mirror must be called by name to match the certificate, and there must be a DNS that resolves that name. But if an air-gapped DNS goes out to ask the outside about names it does not know, it waits for a timeout every time. You have to make the internal zone answer immediately with authority. Time is the same. The clocks of servers that cannot reach internet time servers move apart from each other, and even a few minutes of drift breaks the certificate validity check and log comparison. An air-gapped network must have one reference clock.

Estimated time is 50 minutes. The VM disappears when the session ends, so keep separately, before ending, any files you want to retain.

Steps

  1. Block outbound traffic with /root/infra/egress.sh, which leaves only one set of rules even if run again (the chain AIRGAP-EGRESS, jumped to once from OUTPUT). Keep loopback, established connections, and the internal range 10.203.0.0/24. Measure curl -s -o /dev/null -w '%{http_code}' https://ubuntu.com before and after blocking, and write the results in /root/infra/egress-proof.txt as before= and after=.
  2. With /etc/dnsmasq.d/airgap.conf, make dnsmasq listen only on 10.203.0.1, with no upstream, and answer for the airgap.internal zone authoritatively. Keep the name list in /etc/airgap/hosts (three names, registry, pypi, and ntp → 10.203.0.1) and read it with addn-hosts. Enable the service and make it start at boot.
  3. Write /etc/netns/client/resolv.conf so that client uses that DNS and the search domain airgap.internal. The short name registry must resolve from client.
  4. Add nexus.airgap.internal → 10.203.0.1 to /etc/airgap/hosts and apply it without restarting dnsmasq. Write the PID of the dnsmasq main process before and after applying it in /root/infra/reload.txt as pid_before= and pid_after=.
  5. With /etc/chrony/conf.d/airgap-server.conf, make chrony serve time to the internal range and act as the reference at stratum 8 even without an upstream.
  6. From client, without changing the clock, query 10.203.0.1 for the time and save the output in /root/infra/ntp-query.txt.
  7. Write the handover check script /root/infra/check.sh <네임스페이스> (the placeholder is the namespace). It exits with 0 if registry, pypi, and ntp resolve in that namespace and the time server answers, and with a nonzero value otherwise.

Notes

Block outbound traffic to make an air-gapped network

Block outbound traffic with /root/infra/egress.sh (the chain AIRGAP-EGRESS), and write the status codes of https://ubuntu.com before and after blocking in /root/infra/egress-proof.txt as before= and after=.

If you create a new chain and jump to it at the very front of OUTPUT, then a flush-and-refill approach gives the same shape however many times you run it. Write what to keep (loopback, established connections, the grading agent, the internal range) first as RETURN, and block at the end.

Internal DNS with no upstream

With /etc/dnsmasq.d/airgap.conf, make dnsmasq listen only on 10.203.0.1 and answer for airgap.internal authoritatively with no upstream, and have it read the name list kept in /etc/airgap/hosts. Enable the service and make it start at boot.

You need two lines to narrow the listening address to one, one line to cut the upstream, one line to declare the zone as local, and one line to read the name-list file. Use the status in dig to check whether an internal name that does not exist is answered immediately as "does not exist".

Make another server use the internal DNS

Write /etc/netns/client/resolv.conf so that client uses 10.203.0.1 and the search domain airgap.internal.

ip netns exec mounts /etc/netns//resolv.conf, if it exists, in the place of /etc/resolv.conf inside that namespace. Two lines, nameserver and search, are enough. To check, use dig +search with the short name inside client.

Add a name without a restart

Add nexus.airgap.internal → 10.203.0.1 to /etc/airgap/hosts and apply it without restarting dnsmasq. Write the main process PID before and after applying it in /root/infra/reload.txt as pid_before= and pid_after=.

When dnsmasq receives SIGHUP, it clears its cache and rereads /etc/hosts and the addn-hosts files (it does not reread the configuration file itself). Also check what systemd's reload sends.

Internal time server with no upstream

With /etc/chrony/conf.d/airgap-server.conf, make chrony serve time to 10.203.0.0/24 and act as the reference at stratum 8 even without an upstream.

By default chrony accepts no clients. You need a directive that permits the range to accept, and a directive that serves its own clock as the reference with no upstream. Ubuntu's chrony.conf reads conf.d. After changing it, restart the service and look at the Stratum in tracking.

Ask for the time from another server

From client, without changing the clock, query 10.203.0.1 for the time and save the output in /root/infra/ntp-query.txt.

chronyd has a mode in which, instead of a configuration file, it asks once with a command-line directive and only prints the difference and exits without changing the clock. Run it inside the namespace, and give a timeout so that it waits a few seconds.

Handover check script

Write /root/infra/check.sh <네임스페이스> (the placeholder is the namespace). It exits with 0 if registry, pypi, and ntp resolve in that namespace and the time server answers, and with a nonzero value otherwise.

The grader runs this script in client and in an empty namespace that is not connected to the internal network. The former must give 0 and the latter must be nonzero. Give dig and chronyd -Q a short timeout.