#!/bin/bash # _____ _____ _____ _____ _____ # /\ \ /\ \ /\ \ /\ \ /\ \ # /::\ \ /::\ \ /::\ \ /::\ \ /::\ \ # /::::\ \ /::::\ \ /::::\ \ \:::\ \ /::::\ \ # /::::::\ \ /::::::\ \ /::::::\ \ \:::\ \ /::::::\ \ # /:::/\:::\ \ /:::/\:::\ \ /:::/\:::\ \ \:::\ \ /:::/\:::\ \ # /:::/__\:::\ \ /:::/__\:::\ \ /:::/__\:::\ \ \:::\ \ /:::/__\:::\ \ # /::::\ \:::\ \ \:::\ \:::\ \ /::::\ \:::\ \ /::::\ \ /::::\ \:::\ \ # /::::::\ \:::\ \ ___\:::\ \:::\ \ /::::::\ \:::\ \ /::::::\ \ /::::::\ \:::\ \ # /:::/\:::\ \:::\ \ /\ \:::\ \:::\ \ /:::/\:::\ \:::\ \ /:::/\:::\ \ /:::/\:::\ \:::\____\ # /:::/ \:::\ \:::\____\/::\ \:::\ \:::\____\/:::/ \:::\ \:::\____\ /:::/ \:::\____\/:::/ \:::\ \:::| | # \::/ \:::\ /:::/ /\:::\ \:::\ \::/ /\::/ \:::\ \::/ / /:::/ \::/ /\::/ \:::\ /:::|____| # \/____/ \:::\/:::/ / \:::\ \:::\ \/____/ \/____/ \:::\ \/____/ /:::/ / \/____/ \/_____/\:::\/:::/ / # \::::::/ / \:::\ \:::\ \ \:::\ \ /:::/ / \::::::/ / # \::::/ / \:::\ \:::\____\ \:::\____\ /:::/ / \::::/ / # /:::/ / \:::\ /:::/ / \::/ / \::/ / \::/____/ # /:::/ / \:::\/:::/ / \/____/ \/____/ ~~ # /:::/ / \::::::/ / # /:::/ / \::::/ / # \::/ / \::/ / # \/____/ \/____/ # # --- Anonymous SFTP --- # ASFTP is a tool for configuring SFTP. It allows for setting authenticated sftp and anonymous sftp routes. # It also includes some tools for restricting access to the sftp directories. # # Features: # - sftp with password # - sftp without password # - auto chroot configuration # - defaults to read only # - create append only directories # - create upload only directories # - create both append and upload only directories (users can upload and edit files but not delete anything, including file contents) # - installs nothing! # # For more information on these features, reference the --help flag. # # --- Install --- # Local: # curl -L asftp.sh -o asftp.sh && chmod +x asftp.sh # # Path: # curl -L asftp.sh -o /usr/local/bin/asftp && chmod +x /usr/local/bin/asftp # # --- Examples --- # - asftp.sh/examples/grim.sh # - asftp --help # --- Global Configuration --- SFTP_GROUP="ASFTP" CHROOT_ROOT_DIR="/var/asftp" SSHD_CONFIG_FILE="/etc/ssh/sshd_config" PAM_SSHD_FILE="/etc/pam.d/sshd" PAM_COMMON_AUTH_FILE="/etc/pam.d/common-auth" # A lock file to prevent the script from reacting to its own changes (e.g., during file restoration). LOCK_FILE="/tmp/append-only-sync.lock" # Define the options SHORTOPTS="i:a:p:l:d:r:m:c:h" LONGOPTS="init,add,password:,lock:,delete,route:,mirror:,comment:,help,append" # Use getopt to re-order the arguments PARSED_ARGS=$(getopt -o "$SHORTOPTS" --long "$LONGOPTS" -n "$0" -- "$@") # Check for parsing errors if [ $? -ne 0 ]; then exit 1 fi # Eval the arguments into positional parameters eval set -- "$PARSED_ARGS" MODE="" APPEND=false UPLOAD=false PASSWORD="" KEY="" SOURCE="" ROUTE="" MIRROR="" COMMENT="" # Process the arguments while true; do case "$1" in -i | --init) MODE="init" shift ;; -a | --add) MODE="add" shift ;; -d | --delete) MODE="delete" shift ;; -h | --help) MODE="help" shift ;; -l | --lock) MODE="lock" SOURCE="$2" shift 2 ;; --append) APPEND=true shift ;; --upload) UPLOAD=true shift ;; -p | --password) PASSWORD="$2" shift 2 ;; -k | --key) KEY="$2" shift 2 ;; -r | --route) ROUTE="$2" shift 2 ;; -m | --mirror) MIRROR="$2" shift 2 ;; -c | --comment) COMMENT="$2 " shift 2 ;; --) # End of options shift break ;; *) # This should not be reached with a well-formed getopt call echo "Error parsing arguments!" exit 1 ;; esac done if [[ $MODE == "init" ]]; then # Init if grep -q "^${SFTP_GROUP}:" /etc/group > /dev/null 2>&1; then else echo -e "${CYAN}Creating group '${SFTP_GROUP}'...${NC}" sudo groupadd "${SFTP_GROUP}" fi SSHD_CONFIG_BAK="${SSHD_CONFIG_FILE}.bak" if [ ! -f "${SSHD_CONFIG_BAK}" ]; then sudo cp "${SSHD_CONFIG_FILE}" "${SSHD_CONFIG_BAK}" else echo -e "Warning: Backup of ${SSHD_CONFIG_FILE} already exists at ${SSHD_CONFIG_BAK}. Skipping." fi if grep -q "^PermitEmptyPasswords yes" "${SSHD_CONFIG_FILE}" > /dev/null 2>&1; then echo -e "PermitEmptyPasswords is already 'yes'." else if grep -q "^#PermitEmptyPasswords" "${SSHD_CONFIG_FILE}" > /dev/null 2>&1; then sudo sed -i.bak 's/^#PermitEmptyPasswords.*/PermitEmptyPasswords yes/' "${SSHD_CONFIG_FILE}" elif grep -q "^PermitEmptyPasswords" "${SSHD_CONFIG_FILE}" > /dev/null 2>&1; then sudo sed -i.bak 's/^PermitEmptyPasswords.*/PermitEmptyPasswords yes/' "${SSHD_CONFIG_FILE}" else echo -e "PermitEmptyPasswords yes" | sudo tee -a "${SSHD_CONFIG_FILE}" > /dev/null fi fi if grep -q "^Subsystem sftp internal-sftp" "${SSHD_CONFIG_FILE}" > /dev/null 2>&1; then echo -e "Subsystem sftp internal-sftp is already configured." else if grep -q "^Subsystem sftp" "${SSHD_CONFIG_FILE}" > /dev/null 2>&1; then sudo sed -i.bak 's/^Subsystem sftp.*/Subsystem sftp internal-sftp/' "${SSHD_CONFIG_FILE}" else echo -e "Subsystem sftp internal-sftp" | sudo tee -a "${SSHD_CONFIG_FILE}" > /dev/null fi fi MATCH_BLOCK=" Match Group ${SFTP_GROUP} ChrootDirectory ${CHROOT_ROOT_DIR}/%u ForceCommand internal-sftp AllowTcpForwarding no X11Forwarding no PermitTunnel no PasswordAuthentication yes " sudo sed -i.bak "/^Match Group ${SFTP_GROUP}/,/^[[:space:]]*PasswordAuthentication yes/d" "${SSHD_CONFIG_FILE}" 2>/dev/null || true echo "${MATCH_BLOCK}" | sudo tee -a "${SSHD_CONFIG_FILE}" > /dev/null PAM_SSHD_BAK="${PAM_SSHD_FILE}.bak" if [ ! -f "${PAM_SSHD_BAK}" ]; then sudo cp "${PAM_SSHD_FILE}" "${PAM_SSHD_BAK}" else echo -e "Warning: Backup of ${PAM_SSHD_FILE} already exists at ${PAM_SSHD_BAK}. Skipping." fi if ! grep -q "^@include common-auth" "${PAM_SSHD_FILE}" > /dev/null 2>&1; then echo -e "${CYAN}Adding '@include common-auth' to ${PAM_SSHD_FILE}.${NC}" if grep -q "^#%PAM-1.0" "${PAM_SSHD_FILE}" > /dev/null 2>&1; then sudo sed -i.bak '/^#%PAM-1.0/a @include common-auth' "${PAM_SSHD_FILE}" else echo "@include common-auth" | sudo tee "${PAM_SSHD_FILE}.tmp" > /dev/null cat "${PAM_SSHD_FILE}" | sudo tee -a "${PAM_SSHD_FILE}.tmp" > /dev/null sudo mv "${PAM_SSHD_FILE}.tmp" "${PAM_SSHD_FILE}" fi sudo sed -i.bak '/^auth \[success=1 default=ignore\] pam_unix.so nullok/d' "${PAM_SSHD_FILE}" sudo sed -i.bak '/^auth requisite pam_deny.so/d' "${PAM_SSHD_FILE}" sudo sed -i.bak '/^auth required pam_permit.so/d' "${PAM_SSHD_FILE}" else echo -e "'@include common-auth' is already present in ${PAM_SSHD_FILE}." fi PAM_COMMON_AUTH_BAK="${PAM_COMMON_AUTH_FILE}.bak" if [ ! -f "${PAM_COMMON_AUTH_BAK}" ]; then sudo cp "${PAM_COMMON_AUTH_FILE}" "${PAM_COMMON_AUTH_BAK}" else echo -e "Warning: Backup of ${PAM_COMMON_AUTH_FILE} already exists at ${PAM_COMMON_AUTH_BAK}. Skipping." fi if grep -q "pam_unix.so nullok" "${PAM_COMMON_AUTH_FILE}" > /dev/null 2>&1; then echo -e "'pam_unix.so nullok' already configured in ${PAM_COMMON_AUTH_FILE}." elif grep -q "pam_unix.so nullok_secure" "${PAM_COMMON_AUTH_FILE}" > /dev/null 2>&1; then sudo sed -i.bak 's/pam_unix.so nullok_secure/pam_unix.so nullok/' "${PAM_COMMON_AUTH_FILE}" elif grep -q "pam_unix.so" "${PAM_COMMON_AUTH_FILE}" > /dev/null 2>&1; then sudo sed -i.bak 's/\(auth.*pam_unix.so\)/\1 nullok/' "${PAM_COMMON_AUTH_FILE}" else echo -e "Warning: 'pam_unix.so' line not found in ${PAM_COMMON_AUTH_FILE}. Manual check might be required." fi sudo systemctl restart ssh elif [[ $MODE == "add" ]]; then if id "$ROUTE" > /dev/null 2>&1; then echo -e "Warning: Route '$ROUTE' already exists. Skipping user creation." else sudo useradd -r -s /bin/false -M "$ROUTE" echo "Route '$ROUTE' created successfully." if [[ $PASSWORD != "" ]]; then echo "$ROUTE:$PASSWORD" | sudo chpasswd else sudo passwd -d "$ROUTE" fi echo -e "Adding route '$ROUTE' to group '${SFTP_GROUP}'" if groups "$ROUTE" | grep -q "\b${SFTP_GROUP}\b" > /dev/null 2>&1; then echo -e "Warning: Route '$ROUTE' is already a member of group '${SFTP_GROUP}'. Skipping group assignment." else sudo usermod -aG "${SFTP_GROUP}" "$ROUTE" fi USER_CHROOT_DIR="${CHROOT_ROOT_DIR}/$ROUTE" echo -e "Creating chroot directory for '$ROUTE' at '${USER_CHROOT_DIR}'" sudo mkdir -p "${USER_CHROOT_DIR}" sudo chown root:root "${USER_CHROOT_DIR}" sudo chmod 755 "${USER_CHROOT_DIR}" echo -e "SFTP route '$ROUTE' setup complete." fi elif [[ $MODE == "delete" ]]; then sudo userdel "$ROUTE" check_status "User '$ROUTE' removed from system." "Failed to remove user '$ROUTE'. Manual intervention may be required." USER_CHROOT_DIR="${CHROOT_ROOT_DIR}/$ROUTE" if [ -d "${USER_CHROOT_DIR}" ]; then echo -e "Removing chroot directory '${USER_CHROOT_DIR}'." sudo rm -rf "${USER_CHROOT_DIR}" else echo -e "Warning: Chroot directory '${USER_CHROOT_DIR}' not found. Skipping directory removal." fi echo -e "SFTP route '$ROUTE' removed successfully." elif [[ $MODE == "lock" ]]; then # --- Function Definitions --- log_event() { # Print to standard output for real-time monitoring via `journalctl -f`. echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" } # --- Script Logic for 'lock' mode --- # Ensure the source and mirror directories exist mkdir -p "$SOURCE" mkdir -p "$MIRROR" # Initialize the timestamp file TIMESTAMP_FILE="${MIRROR}/.sync" # Find all files that have been modified more recently than the timestamp file. # The find command here will list files that are newer than the timestamp file. # We then loop through these changed files to apply the custom append-only logic. find "$SOURCE" -type f -newer "$TIMESTAMP_FILE" -print0 | while IFS= read -r -d '' file; do log_event "Found modified file: '$file'" relative_path="${file#$SOURCE/}" mirror_file="$MIRROR/$relative_path" # We need to handle two cases: new files and modified files. if [[ ! -f "$mirror_file" ]]; then # This file is new. Copy it to the mirror. log_event "File is new. Copying '$file' to mirror." cp "$file" "$MIRROR" else if $APPEND ; then # This file already exists in the mirror. Apply append-only logic. log_event "Applying custom merge logic for '$file'." echo "" >> "$mirror_file" diff "$mirror_file" "$file" | grep "^>" | sed -e "s/^> /$COMMENT/" >> "$mirror_file" fi # If append resync else over write cp "$mirror_file" "$SOURCE" log_event "Merged new content into '$mirror_file' and synced back to '$file'." fi done if $UPLOAD ; then find "$SOURCE" -type f -print0 | xargs -0 -I {} bash -c 'dest="$MIRROR{}"; [ ! -f "$dest" ] && mkdir -p "$(dirname "$dest")" && cp -v "{}" "$dest"' fi # After processing all changed files, update the timestamp for the next run. touch "$TIMESTAMP_FILE" log_event "Timestamp file updated. Next run will check for changes after this time." elif [[ $MODE == "help" ]]; then echo -e "Usage: $0 [OPTIONS]" echo -e "Manages SFTP users and enforces append-only file synchronization." echo "" echo -e "Modes:" echo -e " -i, --init : Initializes the SFTP server configuration." echo -e " This backs up sshd_config and pam.d files, and configures" echo -e " sshd for internal-sftp and chrooting, and pam.d for authentication." echo -e " Requires 'sudo'." echo "" echo -e " -a, --add : Adds a new SFTP user (route)." echo -e " Requires --route and optionally --password ." echo -e " The user will be added to the '${SFTP_GROUP}' group and a chroot directory created." echo "" echo -e " -d, --delete : Deletes an existing SFTP user (route)." echo -e " Requires --route ." echo -e " Removes the user and their associated chroot directory." echo "" echo -e " -l, --lock : Starts append-only file synchronization for a specified directory." echo -e " This mode requires --mirror and optionally --comment ." echo -e " This mode should be ran inside a cron job to contiuously monitor file changes" echo -e " - New files/modifications in SOURCE_DIR are appended to MIRROR_DIR." echo -e " - Deletions in SOURCE_DIR are restored from MIRROR_DIR." echo "" echo -e " --append : Enable's file appending in lock mode. Without this flag all file changes will" echo -e " be overwriten. New uploads with still be saved." echo "" echo -e " --upload : Enbale's file uploads (the parent directory must be chmod 755 and chown route:ASFTP)." echo -e " If the append flag is set and --upload is not then users will be able to append to existing" echo -e " files but not upload new files" echo "" echo -e "Options (used with modes):" echo -e " -p, --password : Specifies the password for a new user (used with --add)." echo -e " If not provided, the user will have an empty password." echo -e " -r, --route : Specifies the username for add or delete operations (required with --add, --delete)." echo -e " -m, --mirror : Specifies the mirror directory for the --lock mode (required with --lock)." echo -e " -c, --comment : Specifies a comment prefix for appended content in the --lock mode." echo -e " E.g., '-c \"# APPENDED:\"' will add '# APPENDED:' before new lines." echo "" echo -e " -h, --help : Displays this help message." echo "" echo "" echo -e "Append Only Usage:" echo -e " $0 mkdir -p /var/asftp/myuser/upload" echo -e " $0 chown myuser:ASFTP /var/asftp/myuser/upload" echo -e " $0 chmod 755 /var/asftp/myuser/upload" echo -e " $0 asftp --lock /var/sftp/myuser/upload --mirror /var/sftp/myuser/mirror/upload --comment \"#\"" echo "" echo -e "Note: This script requires 'sudo' for most operations" echo "" echo -e "Example Usage" echo -e "" echo -e "asftp --init # Configure sftp in the ssh damon" echo -e "asftp --add --route git # Create a route named git. Accessed via sftp://git@ip-address" echo -e "mkdir /var/asftp/git/patches # Create a directory to upload patches to" echo -e "chmod 755 /var/asftp/git/patches # Enable writing to the folder" echo -e "chown git:ASFTP /var/asftp/git/patches # Assign owner ship to the directory" echo -e "" echo -e "crontab -e" echo -e "Sync the files every 1 second" echo -e "* * * * * for i in \$(seq 1 60); do asftp --lock /var/asftp/git/patches --mirror /var/asftp/mirror/git/patches --comment \"#\" --append --upload; sleep 1; done" fi