This repository provides a tutorial‑style guide to simulate how software updates are securely distributed in an air‑gapped environment. The walkthrough demonstrates how to set up virtual machines, configure networking, verify and transfer packages, sign repositories with GPG, and consume updates on client machines — all without internet access on the internal network.
Secure.Offline.Software.Update.Distribution.System.mp4
The project simulates how software updates are securely distributed in an internal network without internet access.
-
Gateway Server
- Receives .deb packages from external sources.
- Performs integrity checks(e.g. ClamAV scan, SHA256).
- Transfers verified packages into the internal network via SSH
-
Bastion Host
- Acts as a jump-only SSH gateway
- Enforces controlled access to the repository server.
-
Repository Server
- Uses aptly to index incoming packages.
- Generates signed metadata(Release, Packages.gz)
- Publishes the repository via Lighttpd
-
Client Machine
- Run apt update to fetch metadata from the internal repo.
- Verify GPG signatures before accepting updates.
- Install packages using apt install, pulling only from the internal source
-
Create four Ubuntu VMs:
- Update VM – NAT + host‑only, IP
192.168.182.10 - Bastion Host – host‑only, IP
192.168.182.50 - Repository Server – host‑only, IP
192.168.182.20 - Client – host‑only, DHCP
- Update VM – NAT + host‑only, IP
-
Configure networking:
- NAT → Update VM only (for external package downloads).
- Host‑only (vmnet10) → all VMs (isolated subnet, no internet).
| VM Name | IP Address | Network Adapter | Purpose |
|---|---|---|---|
| Gateway Server | 192.168.226.130 | NAT | External access for package ingestion |
| 192.168.182.10 | VMNet10 | Internal communication | |
| Bastion Host | 192.168.182.50 | VMNet10 | SSH jump enforcement |
| Repository Server | 192.168.182.20 | VMNet10 | Aptly repository hosting via Lighttpd |
| Client | DHCP | VMNet10 | Consumes updates via apt |
Install and enable SSH:
sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now sshapt update→ refreshes package metadata.apt install→ installs the OpenSSH daemon.systemctl enable --now→ ensures SSH starts immediately and on boot.
Install required tools:
sudo apt update
sudo apt install -y clamav gnupg rsync openssh-client- ClamAV → malware scanning.
- GnuPG → key generation and signing.
- rsync → secure file transfer.
- OpenSSH client → provides
scpand jump‑host support.
You’ll use the following pipeline script to automate package verification and transfer.
#!/bin/bash
set -euo pipefail
PKG_NAME=$1
WORKDIR=/tmp/package-check
VERIFIED_DIR=/opt/verified
REJECTED_DIR=/opt/rejected
REPO_USER=reposrvadmin
REPO_HOST=192.168.182.20
REPO_PATH=/opt/repo/incoming
LOGFILE=/var/log/package-pipeline.log
mkdir -p $WORKDIR $VERIFIED_DIR $REJECTED_DIR
touch $LOGFILE
cd $WORKDIR
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') | $1" | tee -a $LOGFILE
}
log "[0] Updating ClamAV signatures..."
sudo freshclam >> $LOGFILE 2>&1 || log "⚠️ ClamAV update failed, continuing with existing database."
log "[1] Downloading package: $PKG_NAME"
apt download $PKG_NAME >> $LOGFILE 2>&1
PKG_FILE=$(ls ${PKG_NAME}_*.deb)
log "[2] Calculating SHA256"
sha256sum $PKG_FILE > ${PKG_FILE}.sha256
sha256sum -c ${PKG_FILE}.sha256 >> $LOGFILE 2>&1 || {
log "❌ SHA mismatch! Moving to rejected."
mv $PKG_FILE $REJECTED_DIR/
exit 1
}
log "[3] Running ClamAV scan"
clamscan $PKG_FILE >> $LOGFILE 2>&1 || {
log "❌ ClamAV flagged! Moving to rejected."
mv $PKG_FILE $REJECTED_DIR/
exit 1
}
log "[4] Package clean. Moving to verified."
mv $PKG_FILE $VERIFIED_DIR/
log "[5] Sending package to Repository via Bastion jump"
rsync -avz -e "ssh -i /home/gatewaysrvadmin/.ssh/id_ed25519 -J bastionadmin@192.168.182.50" \
$VERIFIED_DIR/$PKG_FILE \
reposrvadmin@192.168.182.20:/opt/repo/incoming/
log "✅ Workflow complete: $PKG_FILE delivered to Repository"Explanation:
set -euo pipefail→ strict error handling.freshclam→ updates ClamAV signatures.apt download→ fetches.debwithout installing.sha256sum→ verifies file integrity.clamscan→ malware scan.rsync -avz -e "ssh -J"→ transfers via Bastion jump host.
Install Aptly:
sudo apt install -y aptlyCreate and populate repository:
aptly repo create offline-repo
aptly repo add offline-repo /opt/repo/incoming/Generate and export GPG key:
gpg --full-generate-key
gpg --export -a "Offline Repo" > /opt/repo/repo-public.keySend public key to Bastion Host:
scp /opt/repo/repo-public.key bastionadmin@192.168.182.50:/incoming/repo-public.keyPublish repository:
aptly publish repo -gpg-key="YOUR_KEY_ID" offline-repoAddtionally, you can use the update script that I have used in the demo video.
#!/bin/bash
set -euo pipefail
REPO_NAME=internal-repository
DIST=stable
GPG_KEY="Internal Repository"
INCOMING=/opt/repo/incoming
echo "[`date`] Adding new packages..."
aptly repo add $REPO_NAME $INCOMING
echo "[`date`] Updating published repo..."
aptly publish update -gpg-key="$GPG_KEY" $DISTServe via Lighttpd:
sudo apt install -y lighttpd
sudo systemctl enable --now lighttpd
sudo ln -s /var/lib/aptly/public /var/www/html/repoRepository URL:
http://192.168.182.20/repo/
Copy public key from Bastion:
scp bastionadmin@192.168.182.50:/incoming/repo-public.key ~/repo-public.keyTrust the key:
sudo mkdir -p /etc/apt/trusted.gpg.d/
sudo cp ~/repo-public.key /etc/apt/trusted.gpg.d/Add repository to sources:
sudo nano /etc/apt/sources.list
deb http://192.168.182.20/repo/ stable mainUpdate and install packages:
sudo apt update
sudo apt install <package>