#!/usr/bin/env bash set -Eeuo pipefail umask 077 RELEASE=1.1.1 APP=/opt/netmarga-sftp WEB=/var/lib/netmarga-sftp-web PRIVATE=/var/lib/netmarga-sftp CONFIG=/etc/netmarga-sftp STORAGE=/srv/netmarga-sftp [[ $EUID -eq 0 ]] || { echo 'Run with sudo.' >&2; exit 1; } . /etc/os-release [[ "$ID:$VERSION_ID" == ubuntu:26.04 ]] || { echo 'Use a clean Ubuntu 26.04 server.' >&2; exit 1; } [[ -d /run/systemd/system ]] || { echo 'A running systemd server/VM is required.' >&2; exit 1; } [[ ! -f /etc/isp-platform/hub.env && ! -f /opt/isp-core/INSTALLER-MANAGED ]] || { echo 'Use a separate clean server, not Hub or Core.' >&2; exit 1; } for path in "$APP" "$WEB" "$PRIVATE" "$CONFIG" "$STORAGE"; do [[ ! -L "$path" ]] || { echo "Refusing linked path: $path" >&2; exit 1; }; done if [[ -f $APP/INSTALLER-MANAGED ]]; then [[ -f $PRIVATE/accounts.json ]] || { echo 'Managed account state is missing. Restore it; automatic account resets are disabled.' >&2; exit 1; } [[ -f $WEB/admin.json || ( -f $WEB/bootstrap.json && -f $WEB/setup-token ) ]] || { echo 'Administrator state is missing. Restore it; automatic password resets are disabled.' >&2; exit 1; } fi exec 9>/run/lock/netmarga-sftp-install.lock flock -n 9 || { echo 'Another SFTP installer is running.' >&2; exit 1; } if [[ -d $APP && ! -f $APP/INSTALLER-MANAGED && ! -f $APP/INSTALL-IN-PROGRESS ]]; then echo 'An unmanaged application directory exists.' >&2; exit 1; fi if [[ ! -f $APP/INSTALLER-MANAGED && ! -f $APP/INSTALL-IN-PROGRESS ]]; then for path in "$WEB" "$PRIVATE" "$CONFIG" "$STORAGE"; do [[ ! -e $path ]] || { echo "Fresh installation requires an unused path: $path" >&2; exit 1; } done for port in 2222 9443; do port_hex=$(printf '%04X' "$port") if awk -v port="$port_hex" '$4 == "0A" {split($2,a,":"); if (a[2] == port) found=1} END {exit !found}' /proc/net/tcp /proc/net/tcp6; then echo "Port $port is already in use." >&2; exit 1; fi done fi mkdir -p "$APP" [[ -f $APP/INSTALLER-MANAGED ]] || touch "$APP/INSTALL-IN-PROGRESS" printf 'Netmarga SFTP Server %s — installation in progress.\n' "$RELEASE" exec 3>&1 4>&2 exec >>/var/log/netmarga-sftp-installer.log 2>&1 tmp=$(mktemp -d) cleanup() { result=$?; rm -rf -- "$tmp"; if [[ $result -ne 0 ]]; then printf 'Installation failed. See /var/log/netmarga-sftp-installer.log. Existing administrator SSH is preserved.\n' >&4; fi; } trap cleanup EXIT export DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 update -qq apt-get -o Acquire::ForceIPv4=true -o Acquire::Retries=2 -o Acquire::http::Timeout=20 install -y -qq --no-install-recommends python3 openssh-server openssh-client openssl ca-certificates curl util-linux quota for spec in 'common.py:23f91cb666281bea748f49482b2d387c1dfe5afdfd74316d214f356a50faa061' 'panel.py:529c1a5dd309fbd7c1b871fe23f5a24390b104eab3b56a52c29d82dde1185f71' 'worker.py:fbc904dedb2693d45bf6e475a5206482d2e307bb6c00b13269824171d9c4dab6' 'ui.py:09e97674455c550348049fe9bbe911e11bf66cf7c929588590783188c2ee51c6' 'storage.py:f18f37a544855731c37ec005f745fc8326f2b776d03fc13cf1dc12aca989ebcf' 'quota.py:fbedfdae92fd0d007098c78309be61092357a5c6ab53c12d319d5e669a399d1c'; do name=${spec%%:*};expected=${spec#*:} curl --proto '=https' --tlsv1.2 --retry 2 --connect-timeout 15 --max-time 120 -fsS "https://hub.netmarga.com/downloads/sftp/$RELEASE/$name" -o "$tmp/$name" printf '%s %s\n' "$expected" "$tmp/$name" | sha256sum --check --status python3 -m py_compile "$tmp/$name" done # Grow only a supported dedicated root disk; never format or shrink storage. python3 -S "$tmp/storage.py" >"$tmp/storage-report" 2>&1 cat "$tmp/storage-report" grep -E "Root storage:|Disk size:|Automatic expansion skipped:" "$tmp/storage-report" >&3 || true getent group netmarga-sftp-users >/dev/null || groupadd --system netmarga-sftp-users if getent passwd netmarga-sftp-web >/dev/null; then [[ "$(getent passwd netmarga-sftp-web | cut -d: -f6)" == "$WEB" ]] || { echo 'Conflicting web service account.'; exit 1; } else useradd --system --user-group --home-dir "$WEB" --shell /usr/sbin/nologin netmarga-sftp-web fi install -d -m 0755 -o root -g root "$APP" "$CONFIG" "$CONFIG/keys" "$STORAGE" /run/sshd install -d -m 0700 -o root -g root "$PRIVATE" install -d -m 0700 -o netmarga-sftp-web -g netmarga-sftp-web "$WEB" if [[ -f $APP/INSTALLER-MANAGED ]]; then systemctl stop netmarga-sftp-worker.timer netmarga-sftp-worker.service netmarga-sftp.service netmarga-sftp-panel.service; fi for name in common.py panel.py worker.py ui.py storage.py quota.py; do install -m 0644 -o root -g root "$tmp/$name" "$APP/$name"; done if [[ ! -f $CONFIG/ssh_host_ed25519_key ]]; then ssh-keygen -q -t ed25519 -N '' -f "$CONFIG/ssh_host_ed25519_key"; fi if [[ ! -f $CONFIG/ssh_host_rsa_key ]]; then ssh-keygen -q -t rsa -b 3072 -N '' -f "$CONFIG/ssh_host_rsa_key"; fi PYTHONPATH="$APP" python3 -S - <<'PY' import common as c,secrets,hashlib,os if not (c.PRIVATE/'accounts.json').exists():c.atomic(c.PRIVATE/'accounts.json',{}) accounts=c.read(c.PRIVATE/'accounts.json') (c.CONFIG/'sshd_config').write_text(c.sshd_config(accounts));(c.CONFIG/'sshd_config').chmod(0o600) if not (c.WEB/'admin.json').exists(): if not (c.WEB/'bootstrap.json').exists(): token=secrets.token_urlsafe(32) c.atomic(c.WEB/'bootstrap.json',{'digest':hashlib.sha256(token.encode()).hexdigest()}) (c.WEB/'setup-token').write_text(token);(c.WEB/'setup-token').chmod(0o600) elif not (c.WEB/'setup-token').exists():raise ValueError('Incomplete setup state: restore the original bootstrap state. No password was reset.') PY if [[ ! -f $WEB/panel.crt || ! -f $WEB/panel.key ]]; then python3 - "$tmp/tls.conf" <<'PY' import ipaddress,subprocess,sys ips=['127.0.0.1'] for item in subprocess.check_output(['hostname','-I'],text=True).split(): try: ipaddress.ip_address(item) if item not in ips:ips.append(item) except ValueError:pass config='[req]\ndistinguished_name=dn\nx509_extensions=ext\nprompt=no\n[dn]\nCN=Netmarga SFTP Admin\n[ext]\nsubjectAltName=@names\n[names]\nDNS.1=localhost\n' config+=''.join('IP.'+str(i+1)+'='+ip+'\n' for i,ip in enumerate(ips)) open(sys.argv[1],'w').write(config) PY openssl req -x509 -newkey rsa:2048 -nodes -days 365 -config "$tmp/tls.conf" -keyout "$WEB/panel.key" -out "$WEB/panel.crt" fi find "$WEB" -maxdepth 1 -type f -exec chown netmarga-sftp-web:netmarga-sftp-web {} + -exec chmod 0600 {} + cat > /etc/systemd/system/netmarga-sftp-storage.service <<'UNIT' [Unit] Description=Non-executable Netmarga SFTP storage mount Before=netmarga-sftp.service netmarga-sftp-worker.service [Service] Type=oneshot ExecStart=/bin/sh -c 'mountpoint -q /srv/netmarga-sftp || mount --bind /srv/netmarga-sftp /srv/netmarga-sftp; mount -o remount,bind,noexec,nodev,nosuid /srv/netmarga-sftp' RemainAfterExit=yes [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/netmarga-sftp-quota.service <<'UNIT' [Unit] Description=Enable kernel-enforced SFTP account storage limits Requires=netmarga-sftp-storage.service After=netmarga-sftp-storage.service Before=netmarga-sftp.service netmarga-sftp-worker.service [Service] Type=oneshot ExecStart=/usr/bin/python3 -S /opt/netmarga-sftp/quota.py --setup RemainAfterExit=yes [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/netmarga-sftp.service <<'UNIT' [Unit] Description=Netmarga isolated SFTP-only server Requires=netmarga-sftp-storage.service netmarga-sftp-quota.service After=network.target netmarga-sftp-storage.service netmarga-sftp-quota.service [Service] RuntimeDirectory=netmarga-sftp RuntimeDirectoryMode=0755 ExecStartPre=/usr/bin/install -d -m 0755 /run/sshd ExecStartPre=/usr/bin/python3 -S /opt/netmarga-sftp/worker.py --check-storage ExecStartPre=/usr/bin/python3 -S /opt/netmarga-sftp/quota.py ExecStartPre=/usr/sbin/sshd -t -f /etc/netmarga-sftp/sshd_config ExecStart=/usr/sbin/sshd -D -e -f /etc/netmarga-sftp/sshd_config ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 KillMode=control-group [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/netmarga-sftp-panel.service <<'UNIT' [Unit] Description=Netmarga SFTP administrator HTTPS panel Requires=netmarga-sftp-storage.service After=network.target netmarga-sftp-storage.service [Service] User=netmarga-sftp-web Group=netmarga-sftp-web ExecStart=/usr/bin/python3 -S /opt/netmarga-sftp/panel.py Restart=on-failure RestartSec=5 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/netmarga-sftp-web UMask=0077 [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/netmarga-sftp-worker.service <<'UNIT' [Unit] Description=Apply approved Netmarga SFTP account changes Requires=netmarga-sftp-storage.service After=netmarga-sftp-storage.service netmarga-sftp.service netmarga-sftp-quota.service Requires=netmarga-sftp-quota.service [Service] Type=oneshot ExecStart=/usr/bin/python3 -S /opt/netmarga-sftp/worker.py TimeoutStartSec=300 UMask=0077 PrivateTmp=true ProtectHome=true UNIT cat > /etc/systemd/system/netmarga-sftp-worker.timer <<'UNIT' [Unit] Description=Check approved SFTP account requests [Timer] OnBootSec=10 OnUnitInactiveSec=2 AccuracySec=1 [Install] WantedBy=timers.target UNIT # Prevent SFTP accounts bypassing their dedicated restricted listener via port 22. install -d -m 0755 /etc/ssh/sshd_config.d printf 'DenyGroups netmarga-sftp-users\n' > /etc/ssh/sshd_config.d/00-netmarga-sftp-deny.conf /usr/sbin/sshd -t /usr/sbin/sshd -t -f "$CONFIG/sshd_config" systemctl stop netmarga-sftp-worker.timer netmarga-sftp.service 2>/dev/null || true systemctl daemon-reload systemctl enable --now netmarga-sftp-storage.service PYTHONPATH="$APP" python3 -S "$APP/worker.py" --check-storage systemctl enable --now netmarga-sftp-quota.service systemctl enable netmarga-sftp.service netmarga-sftp-panel.service systemctl restart netmarga-sftp.service netmarga-sftp-panel.service systemctl enable --now netmarga-sftp-worker.timer systemctl start netmarga-sftp-worker.service if systemctl is-active --quiet ssh.service; then systemctl reload ssh.service; fi if command -v ufw >/dev/null && ufw status | grep -q '^Status: active'; then ufw allow 2222/tcp;ufw allow 9443/tcp;fi for attempt in 1 2 3 4 5; do if curl -k --connect-timeout 2 --max-time 5 -fsS https://127.0.0.1:9443/ >/dev/null; then break; fi [[ $attempt -lt 5 ]] || exit 1 sleep 1 done systemctl is-active --quiet netmarga-sftp.service netmarga-sftp-panel.service printf '%s\n' "$RELEASE" > "$APP/INSTALLER-MANAGED" rm -f "$APP/INSTALL-IN-PROGRESS" ip=$(hostname -I | awk '{print $1}') printf '\nInstallation completed.\nSFTP port: 2222\nAdministrator username: admin\n' >&3 if [[ -f $WEB/setup-token ]]; then printf 'Set your administrator password using this one-time link:\nhttps://%s:9443/setup#token=%s\n' "$ip" "$(cat "$WEB/setup-token")" >&3 else printf 'Admin panel: https://%s:9443/\nExisting administrator password and SFTP accounts were preserved.\n' "$ip" >&3 fi printf 'The HTTPS panel starts with a self-signed certificate. See https://hub.netmarga.com/install/sftp/ for connection and certificate guidance.\n' >&3