From b4264bccfac1bddf182b5f2b2af92b3a62c51c6a Mon Sep 17 00:00:00 2001 From: meliurwen Date: Sat, 22 Aug 2020 17:33:09 +0200 Subject: [PATCH] Should work now --- Dockerfile | 15 ++++ docker-compose.yml | 3 +- root/config/sshd_config | 22 +++++ root/entrypoint.sh | 97 +++++++++++++++++++++ root/usr/local/bin/create-sftp-user.sh | 113 +++++++++++++++++++++++++ 5 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 root/config/sshd_config create mode 100644 root/entrypoint.sh create mode 100644 root/usr/local/bin/create-sftp-user.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7f05c94 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM alpine:latest + +# Steps done in one RUN layer: +# - Install packages +# - Fix default group (1000 does not exist) +# - OpenSSH needs /var/run/sshd to run +# - Remove generic host keys, entrypoint generates unique keys +RUN apk add --no-cache openssh openssh-sftp-server && \ + sed -i 's/GROUP=1000/GROUP=100/' /etc/default/useradd && \ + mkdir -p /var/run/sshd && \ + rm -f /etc/ssh/ssh_host_*key* + +COPY root/ / + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 42c3dd2..def4287 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,8 @@ version: "3" services: sftp: - image: ${SFTP_IMG:-atmoz/sftp}:${SFTP_TAG:-latest} + #image: ${SFTP_IMG:-atmoz/sftp}:${SFTP_TAG:-latest} + build: . container_name: ${SFTP_CONTAINER_NAME:-sftp} restart: ${SFTP_RESTART:-unless-stopped} volumes: diff --git a/root/config/sshd_config b/root/config/sshd_config new file mode 100644 index 0000000..1308c8b --- /dev/null +++ b/root/config/sshd_config @@ -0,0 +1,22 @@ +# Secure defaults +# See: https://stribika.github.io/2015/01/04/secure-secure-shell.html +Protocol 2 +HostKey /etc/ssh/ssh_host_ed25519_key +HostKey /etc/ssh/ssh_host_rsa_key + +# Faster connection +# See: https://github.com/atmoz/sftp/issues/11 +UseDNS no + +# Limited access +PermitRootLogin no +X11Forwarding no +AllowTcpForwarding no + +# Force sftp and chroot jail +Subsystem sftp internal-sftp +ForceCommand internal-sftp +ChrootDirectory %h + +# Enable this for more logs +#LogLevel VERBOSE diff --git a/root/entrypoint.sh b/root/entrypoint.sh new file mode 100644 index 0000000..a59fd27 --- /dev/null +++ b/root/entrypoint.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -Eeo pipefail + +if [ ! -e /etc/openvpn/sshd_config ]; then + log "Configuration file sshd_config not found. Initializing..." + cp /config/sshd_config /etc/ssh/sshd_config +fi + +# shellcheck disable=2154 +trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR + +reArgsMaybe="^[^:[:space:]]+:.*$" # Smallest indication of attempt to use argument +reArgSkip='^([[:blank:]]*#.*|[[:blank:]]*)$' # comment or empty line + +# Paths +userConfPath="/etc/sftp/users.conf" +userConfFinalPath="/var/run/sftp/users.conf" + +function log() { + echo "[$0] $*" >&2 +} + +# Allow running other programs, e.g. bash +if [[ -z "$1" || "$1" =~ $reArgsMaybe ]]; then + startSshd=true +else + startSshd=false +fi + +# Create users only on first run +if [ ! -f "$userConfFinalPath" ]; then + mkdir -p "$(dirname $userConfFinalPath)" + + if [ -f "$userConfPath" ]; then + # Append mounted config to final config + grep -v -E "$reArgSkip" < "$userConfPath" > "$userConfFinalPath" + fi + + if $startSshd; then + # Append users from arguments to final config + for user in "$@"; do + echo "$user" >> "$userConfFinalPath" + done + fi + + if [ -n "$SFTP_USERS" ]; then + # Append users from environment variable to final config + IFS=" " read -r -a usersFromEnv <<< "$SFTP_USERS" + for user in "${usersFromEnv[@]}"; do + echo "$user" >> "$userConfFinalPath" + done + fi + + # Check that we have users in config + if [ -f "$userConfFinalPath" ] && [ "$(wc -l < "$userConfFinalPath")" -gt 0 ]; then + # Import users from final conf file + while IFS= read -r user || [[ -n "$user" ]]; do + create-sftp-user.sh "$user" + done < "$userConfFinalPath" + elif $startSshd; then + log "FATAL: No users provided!" + exit 3 + fi + + # Generate unique ssh keys for this container, if needed + if [ ! -f /etc/ssh/ssh_host_ed25519_key ]; then + ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N '' + fi + if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then + ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N '' + fi + + # Restrict access from other users + chmod 600 /etc/ssh/ssh_host_ed25519_key || true + chmod 600 /etc/ssh/ssh_host_rsa_key || true +fi + +# Source custom scripts, if any +if [ -d /etc/sftp.d ]; then + for f in /etc/sftp.d/*; do + if [ -x "$f" ]; then + log "Running $f ..." + $f + else + log "Could not run $f, because it's missing execute permission (+x)." + fi + done + unset f +fi + +if $startSshd; then + log "Executing sshd" + exec /usr/sbin/sshd -D -e +else + log "Executing $*" + exec "$@" +fi diff --git a/root/usr/local/bin/create-sftp-user.sh b/root/usr/local/bin/create-sftp-user.sh new file mode 100644 index 0000000..874264c --- /dev/null +++ b/root/usr/local/bin/create-sftp-user.sh @@ -0,0 +1,113 @@ +#!/bin/bash +set -Eeo pipefail + +# shellcheck disable=2154 +trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR + +# Extended regular expression (ERE) for arguments +reUser='[A-Za-z0-9._][A-Za-z0-9._-]{0,31}' # POSIX.1-2008 +rePass='[^:]{0,255}' +reUid='[[:digit:]]*' +reGid='[[:digit:]]*' +reDir='[^:]*' +#reArgs="^($reUser)(:$rePass)(:e)?(:$reUid)?(:$reGid)?(:$reDir)?$" + +function log() { + echo "[$0] $*" +} + +function validateArg() { + name="$1" + val="$2" + re="$3" + + if [[ "$val" =~ ^$re$ ]]; then + return 0 + else + log "ERROR: Invalid $name \"$val\", do not match required regex pattern: $re" + return 1 + fi +} + +log "Parsing user data: \"$1\"" +IFS=':' read -ra args <<< "$1" + +skipIndex=0 +chpasswdOptions="" +useraddOptions=(--no-user-group) + +user="${args[0]}"; validateArg "username" "$user" "$reUser" || exit 1 +pass="${args[1]}"; validateArg "password" "$pass" "$rePass" || exit 1 + +if [ "${args[2]}" == "e" ]; then + chpasswdOptions="-e" + skipIndex=1 +fi + +uid="${args[$((skipIndex+2))]}"; validateArg "UID" "$uid" "$reUid" || exit 1 +gid="${args[$((skipIndex+3))]}"; validateArg "GID" "$gid" "$reGid" || exit 1 +dir="${args[$((skipIndex+4))]}"; validateArg "dirs" "$dir" "$reDir" || exit 1 + +if getent passwd "$user" > /dev/null; then + log "WARNING: User \"$user\" already exists. Skipping." + exit 0 +fi + +if [ -n "$uid" ]; then + useraddOptions+=(--non-unique --uid "$uid") +fi + +if [ -n "$gid" ]; then + if ! getent group "$gid" > /dev/null; then + groupadd --gid "$gid" "group_$gid" + fi + + useraddOptions+=(--gid "$gid") +fi + +useradd "${useraddOptions[@]}" "$user" +mkdir -p "/home/$user" +chown root:root "/home/$user" +chmod 755 "/home/$user" + +# Retrieving user id to use it in chown commands instead of the user name +# to avoid problems on alpine when the user name contains a '.' +uid="$(id -u "$user")" + +if [ -n "$pass" ]; then + echo "$user:$pass" | chpasswd $chpasswdOptions +else + usermod -p "*" "$user" # disabled password +fi + +# Add SSH keys to authorized_keys with valid permissions +userKeysQueuedDir="/home/$user/.ssh/keys" +if [ -d "$userKeysQueuedDir" ]; then + userKeysAllowedFileTmp="$(mktemp)" + userKeysAllowedFile="/home/$user/.ssh/authorized_keys" + + for publickey in "$userKeysQueuedDir"/*; do + cat "$publickey" >> "$userKeysAllowedFileTmp" + done + + # Remove duplicate keys + sort < "$userKeysAllowedFileTmp" | uniq > "$userKeysAllowedFile" + + chown "$uid" "$userKeysAllowedFile" + chmod 600 "$userKeysAllowedFile" +fi + +# Make sure dirs exists +if [ -n "$dir" ]; then + IFS=',' read -ra dirArgs <<< "$dir" + for dirPath in "${dirArgs[@]}"; do + dirPath="/home/$user/$dirPath" + if [ ! -d "$dirPath" ]; then + log "Creating directory: $dirPath" + mkdir -p "$dirPath" + chown -R "$uid:users" "$dirPath" + else + log "Directory already exists: $dirPath" + fi + done +fi