I have an old ReadyNAS Duo 2120 (confusingly it says 2120v2 on the bottom, although it is RND2000v1 generation hardware) which I have turned into a webserver to provide a mirror service for my new air gapped home lab network. This is a precursor to setting up Proxmox VE, with both read-only package mirrors and ISO repository on the NAS. It is updated via a USB hard disk which I sync on my home network then physically move to the lab environment, mount read-only to update the mirrors from.

Reset, create share and enable anonymous http access

I started with a factory reset of the NAS by holding paper clip in reset hole for 30s while powering on until all Disk LEDs flash twice. Wait 15 minutes for the reset to happen (“The system performs the disk test, which takes about five minutes, waits 10 minutes, and then restores the system to its factory settings.”).

Next I had a fight to find a web-browser to access the NAS’ admin interface, which must be accessed over https (http redirects to https) but uses a version of TLS disabled by all modern web-browsers. When connected (I managed it by going to about:config and setting security.tls.version.min to 1 in Firefox which brought back the option to by-pass the security error, I had a complete failure to find a way to by-pass with Chrome and Safari), I was able to login with the default username and password (admin/netgear1) and started going through the setup wizard.

In the fileshare protocols, I disabled everything except HTTP and Rsync. I deleted both of the default shares and created one new one, mirrors (“Software mirrors” as the description). Once setup, in the mirrors share I enabled HTTP/S with read-only default access and Rsync with read/write default access but restricted access to the host I will use to update the mirrors from. I then went back into Services and Standard File Protocols and set Redirect default web access to this share: to mirrors (leaving Login authentication on this share: set to Disabled) so the NAS will be accessible as a source for Linux package managers.

Later I went back and enabled NFS for the ISOs share that I added at the end of this post - so if you are following along to replicate something similar to my setup you might want to do that now.

Disable https

Once setup, I downloaded and install the Enable Root SSH Access add-on. After rebooting, which the update will ask you to do, I used SSH to login to the device and disabled https - a downgrade in security but since this is in my air-gapped home lab network (where there is nothing of value or consequence) it is an acceptable risk and removes the inability to manage the device due to the version of TLS being blocked.

Note that in order to SSH I had to add a defunct (and presumably insecure) key-exchange algorithm at the client end:

ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -l root 192.168.100.200

The edits to disable https are:

  1. In /etc/frontview/apache/Virtual.conf comment these two lines:
RewriteRule ^/admin/(.*)$ https://%{SERVER_NAME}/admin/$1 [R,L]
RewriteRule ^/admin$ https://%{SERVER_NAME}/admin
  1. In /etc/frontview/apache/httpd.conf comment these lines:
Listen 443
SSLEngine On
SSLSessionCache dbm:/ramfs/gcache.db
SSLSessionCacheTimeout 600
SSLCACertificatePath /etc/frontview/apache
SSLCertificateFile /etc/frontview/apache/apache.pem
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384Smiley Very HappyHE-RSA-AES128-GCM-SHA256Smiley Very HappyHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHASmiley Very HappyHE-RSA-AES128-SHA256Smiley Very HappyHE-RSA-AES128-SHASmiley Very HappyHE-DSS-AES128-SHA256Smiley Very HappyHE-RSA-AES256-SHA256Smiley Very HappyHE-DSS-AES256-SHASmiley Very HappyHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA:AES256-SHA:AES:CAMELLIASmiley Very HappyES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
SSLHonorCipherOrder on

Restart the webserver with (I tried /etc/init.d/frontview restart but it failed to start it up again):

/etc/init.d/frontview stop
/etc/init.d/frontview start

Do not forget to set Firefox’s minimum TLS version back to default, now we can connect.

Creating the mirrors

Syncing from internet to USB drive

On a Debian Linux system with an internet connection, I installed the tools to create Debian and EL repositories:

apt-get install debmirror yum-utils

I then synced the mirrors I needed for ProxMox as well as CentOS and Rocky Linux to an ext4 formatted USB hard disk - to help make this painless in future, I created a little script called mirrors-fetch.bash on the USB disk:

#!/bin/bash

# Basic scripting safety
set -euo pipefail

# Make sure to include Proxmox's base suite
DEBIAN_SUITES=bullseye,bullseye-updates
DEBIAN_SECURITY_SUITES=bullseye-security
CENTOS_VERSIONS=7
CENTOS_VAULT_VERSIONS=6.10
ROCKY_VERSIONS=8
PROXMOX_SUITES=bullseye
PROXMOX_CEPH_VERSION=pacific
# Taken from https://pve.proxmox.com/pve-docs/pve-admin-guide.html#repos_secure_apt
PROXMOX_KEYRING_SOURCE="https://enterprise.proxmox.com/debian/proxmox-release-bullseye.gpg"
PROXMOX_KEYRING_SHA512="7fb03ec8a1675723d2853b84aa4fdb49a46a3bb72b9951361488bfd19b29aab0a789a4f8c7406e71a69aabbc727c936d3549731c4659ffa1a08f44db8fdcebfa"

# Find where to sync to
my_path="$( dirname "$( realpath "$0" )" )"
if ! [ -d "$my_path/mirrors" ]
then
    echo "$my_path/mirrors does not exist - are you running this script from the location with the mirrors directory?" >&2
    echo "(N.B. mirrors directory needs to be pre-created manually)" >&2
    exit 1
else
    TARGET="$my_path/mirrors"
fi

# Local keyring filename for Proxmox
PROXMOX_KEYRING="$TARGET/$( basename "$PROXMOX_KEYRING_SOURCE" )"

# Check pre-requisite commands exist (fail early)
if ! command -v debmirror &>/dev/null || ! command -v reposync &>/dev/null
then
    echo "Unable to locate both debmirror and reposync commands." >&2
    echo "(on Debian, are debmirror and yum-utils packages installed?)" >&2
    exit 1
fi

# Create Debian mirrors
# ...main mirror
debmirror -v -p -d $DEBIAN_SUITES -s main,contrib,non-free -a amd64 --keyring=/usr/share/keyrings/debian-archive-keyring.gpg --method=rsync -h rsync.mirrorservice.org -r ftp.debian.org/debian "$TARGET"/debian
# ...security mirror
debmirror -v -p -d $DEBIAN_SECURITY_SUITES -s main,contrib,non-free -a amd64 --keyring=/usr/share/keyrings/debian-archive-keyring.gpg --method=http -h security.debian.org -r debian-security "$TARGET"/debian-security

# Create Proxmox mirrors
if ! [ -e "$PROXMOX_KEYRING" ]
then
    curl -o "$PROXMOX_KEYRING" "$PROXMOX_KEYRING_SOURCE"
fi

if [ "$( sha512sum "$PROXMOX_KEYRING" | awk '{print $1}' )" != "$PROXMOX_KEYRING_SHA512" ]
then
    echo "ERROR: Proxmox keyring on disk ($PROXMOX_KEYRING) sha512sum does not match configured value ($PROXMOX_KEYRING_SHA512):" >&2
    sha512sum "$PROXMOX_KEYRING" >&2
    rm -f "$PROXMOX_KEYRING"
    exit 1
fi

# ...main Proxmox repository
debmirror -v -p -d $PROXMOX_SUITES -s pve-no-subscription -a amd64 --keyring="$PROXMOX_KEYRING" --rsync-extra=none --method=http -h download.proxmox.com -r debian/pve "$TARGET"/pve-no-subscription
# ...Proxmox Ceph repository
debmirror -v -p -d $PROXMOX_SUITES -s main -a amd64 --keyring="$PROXMOX_KEYRING" --rsync-extra=none --method=http -h download.proxmox.com -r debian/ceph-$PROXMOX_CEPH_VERSION "$TARGET"/ceph-$PROXMOX_CEPH_VERSION

# Create yum repository config for reposync
[ -e "$TARGET/yum.conf" ] && rm -f "$TARGET/yum.conf"  # Remove old config, if it exists

for centos_version in $( echo "$CENTOS_VERSIONS" | sed -e 's/,/ /g' )
do
    cat - >> "$TARGET/yum.conf" <<EOF
[centos-$centos_version-os]
name=CentOS $centos_version OS
baseurl=http://mirror.centos.org/centos/$centos_version/os/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$centos_version

[centos-$centos_version-updates]
name=CentOS $centos_version updates
baseurl=http://mirror.centos.org/centos/$centos_version/updates/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$centos_version

[centos-$centos_version-extras]
name=CentOS $centos_version extras
baseurl=http://mirror.centos.org/centos/$centos_version/extras/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$centos_version
EOF
done

for centos_vault_version in $( echo "$CENTOS_VAULT_VERSIONS" | sed -e 's/,/ /g' )
do
    cat - >> "$TARGET/yum.conf" <<EOF
[centos-vault-$centos_vault_version-os]
name=CentOS $centos_vault_version OS
baseurl=http://vault.centos.org/$centos_vault_version/os/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$( echo $centos_vault_version | sed 's/\.[0-9]*$//' )

[centos-vault-$centos_vault_version-updates]
name=CentOS $centos_vault_version updates
baseurl=http://vault.centos.org/$centos_vault_version/updates/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$( echo $centos_vault_version | sed 's/\.[0-9]*$//' )

[centos-vault-$centos_vault_version-extras]
name=CentOS $centos_vault_version extras
baseurl=http://vault.centos.org/$centos_vault_version/extras/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-$( echo $centos_vault_version | sed 's/\.[0-9]*$//' )
EOF
done

for rocky_version in $( echo "$ROCKY_VERSIONS" | sed -e 's/,/ /g' )
do
    cat - >> "$TARGET/yum.conf" <<EOF
[rocky-$rocky_version-baseos]
name=Rocky Linux $rocky_version BaseOS
baseurl=http://rockylinux.mirrorservice.org/$rocky_version/BaseOS/x86_64/os/
enabled=1
gpgcheck=1
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-rockyofficial

[rocky-$rocky_version-appstream]
name=Rocky Linux $centos_version AppStream
baseurl=http://rockylinux.mirrorservice.org/$rocky_version/AppStream/x86_64/os/
enabled=1
gpgcheck=1
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-rockyofficial

[rocky-$rocky_version-extras]
name=Rocky Linux $centos_version extras
baseurl=http://rockylinux.mirrorservice.org/$rocky_version/extras/x86_64/os/
enabled=1
gpgcheck=1
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-rockyofficial
EOF
done

reposync -a x86_64 -c "$TARGET/yum.conf" -p "$TARGET" --delete --newest-only --downloadcomps --download-metadata

# Run createrepo on RPM repositories
for file in "$TARGET"/*
do
  if [ -d "$file/Packages" ]
  then
    pushd "$file"
    CREATE_REPO_OPTS="--update -v"
    if [ -e comps.xml ]
    then
      CREATE_REPO_OPTS="$CREATE_REPO_OPTS -g comps.xml"
    fi
    createrepo $CREATE_REPO_OPTS "$file"
    popd
  fi
done

Syncing from USB drive to NAS

To get the data into the air-gapped lab environment, I took the USB disk and plugged it into the host I had configured the NAS to allow write access via Rsync. I then mounted it and used Rsync to upload the mirrors to the NAS (which is configured to receive the IP address 192.168.100.200 on the switch):

mkdir /media/usb
mount -o ro /dev/sdb1 /media/usb
rsync -avP --delete /media/usb/mirrors/ 192.168.100.200::mirrors
umount /mnt

The same process and commands can be used to efficiently update the mirrors in future.

Using the mirrors

Once synced, the mirrors are accessible below /mirrors on the NAS (navigating to the device now redirects to that path anyway). Helpfully folder listings are on by default so it is easy to see what is going on with it. Apt/Yum etc. can now be configured to use the NAS as a source.

Install images - a new share called isos

To make life easy with setting up VMs I added a new share, isos, with network install iso files (since I now have local mirrors, network install makes sense) for Debian, CentOS, Rocky, Windows Server 2019 180 day evaluation(the latter to create a Domain Controller for testing Active Directory integrations) and enabled read-only NFS access (and the same read/write access via Rsync to update).

In order to make this work with Proxmox, I had to put the files into the path template/iso within the share (or Proxmox will complain that it cannot create them as the NFS share is read-only). Presuming the iso files are in the folder isos on the USB drive and the drive mounted as above for syncing the mirrors, I synced them with this command:

# ReadyNAS rsync daemon does not support --mkpath, so have to pre-create parent directory by syncing an empty directory to it
mkdir /tmp/empty && rsync -r /tmp/empty/ 192.168.100.200::isos/template && rmdir /tmp/empty
rsync -avP --delete --mkpath /media/usb/isos/ 192.168.100.200::isos/template/iso