Ssh – Configure SFTP with OpenSSH and an AWS S3 Bucket mounted via S3FS on Amazon EC2

amazon ec2amazon-web-servicess3fssftpssh

How do I allow multiple SFTP Users with S3FS and OpenSSH?

Everything works, except SFTP Users don't have permission to write to their Chrooted Home Directory: remote open("/some_file"): Permission denied

Setup

I've got an Amazon EC2 instance running Amazon Linux. I've installed S3FS and mounted an S3 bucket. I've also configured OpenSSH to allow SFTP Users to access a Chrooted Home directory inside the mounted S3 Bucket /s3_mounted_folder/user_folder/. I've successfully used the SFTP connection on a non S3 mounted directory. I've successfully used the S3 bucket to create and download files from S3 as root on the EC2 instance via SSH. My SFTP users can successfully download files from their /s3_mounted_folder/user_folder/ directory. The problem is that the SFTP users cannot put files into the S3 mounted folder.

The Problem … I think

I am only able to configure all folders (/s3_mounted_folder/ and /s3_mounted_folder/user_folder/)with the same user:group and same permissions, thus, I can't give the user access to write to his/her home directory (/s3_mounted_folder/user_folder/). If I mount the bucket with the user or group and give either write permissions, then OpenSSH SFTP won't let users connect because it believes the user permissions are misconfigured (example: drwxr-xr-x 1 root root vs. drwxrwxr-x 1 root usergroup).

S3FS Commands

Here are the two different commands to launch S3FS in these two modes (where user 501 and group 501 are the SFTP user and group):

root user permissions (drwxr-xr-x 1 root root): sudo s3fs nwd-sftp /sftp/ -o iam_role=sftp-server -o allow_other -o umask=022

sftp user permissions (drwxrwxr-x 1 root usergroup): sudo s3fs nwd-sftp /sftp/ -o iam_role=sftp-server -o allow_other -o umask=002 -o gid=501

In that second scenario, the user would theoretically be able to put files into their home directory via SFTP, but SFTP won't let them connect because their Chrooted home directory has write permissions for a group that isn't root.

Best Answer

I've had a very similar setup, but with NFS instead of S3. My solution was to mount NFS home directories to a mount point outside of the home directory path, and then use autofs to automatically mount the users home directory on demand inside a top-level directory, /jail, that the user has no write access to.

Relevant configs:

sshd

Subsystem sftp internal-sftp -l DEBUG -u 002 -d %u

UsePAM yes
Match group usergroup
    # This configuration section is part of the sftp-chroot project – https://github.com/mle86/sftp-chroot/
    X11Forwarding no
    AllowTcpForwarding no
    ChrootDirectory /jail/%u  # see /etc/auto.master.d/jails.autofs
    ForceCommand internal-sftp -l DEBUG -u 002  # see sftp-server(8) for options

auto.master

/jail  program:/etc/autofs-sftp-jails.sh  --timeout=20

auto.autohome

*   -fstype=nfs4,rw,hard,intr,rsize=2048,wsize=2048,nosuid,nfsvers3 10.0.0.1:/nfsmount/&

autofs-sftp-jails.sh

#!/bin/sh
# 
# This file is part of the sftp-chroot project – https://github.com/mle86/sftp-chroot/
# 
# This autofs script will allow any local user's homedir to be mounted
# under /jail with a mountpoint named like the user, e.g.
# /jail/xyz      → fake empty directory (root:root 0755)
# /jail/xyz/~xyz → /~xyz
# 
# The base directory /jail will only be accessible for root.
# All mountpoints under /jail are therefore only usable as chroot base directories.
# 
# Exit codes:
#  - 0  Success. Has printed one autofs(5) map entry.
#  - 1  Argument was not a valid username.
#  - 2  User exists, but has no homedir entry.
#  - 3  User's homedir does not exist (or is not a directory).
#  - 4  User's homedir contains a symlink component.
#  - 5  User's homedir contains a non-directory component (?!).
#  - 6  User's homedir has too many components.
#  - 7  User homedir component could not be resolved.
#  - 8  User's homedir contains forbidden characters.


## Initialization:  ############################################################

set -e  # die on errors

username="$1"

PROGNAME="$0($username)"

MOUNT_TO=/jail  # duplicated in auto.master.d/jails.autofs


## Helper functions:  ##########################################################

# fail [exitStatus=1] errorMessage
#  Exits the script with an error message and an optional exit status.
fail () {
    local status=1
    if [ -n "$2" ]; then
        status="$1"
        shift
    fi

    printf '%s: %s\n' "${PROGNAME:-$0}" "$*"  >&2
    exit $status
}

# user_homedir username
#  Retrieves and prints a user's homedir.
#  Exits with an error message and a non-zero status if
#   "getent passwd $username" did not succeed, or
#   the resulting passwd line did not contain a homedir part, or
#   the homedir does not exist or is not actually a directory.
#  The result is therefore guaranteed to be an existing directory.
user_homedir () {
    local username="$1"

    local uent=
    uent="$(getent passwd -- "$username")"  || fail 1 "user not found"

    local homedir="$(printf '%s' "$uent" | cut -d':' -f6)"
    [ -n "$homedir" ]  || fail 2 "no homedir entry"
    [ -d "$homedir" ]  || fail 3 "homedir not found: $homedir"

    printf '%s\n' "$homedir"
}

# remove_trailing_slashes string
#  Removes any trailing slashes in the string, if there are any,
#  and prints the result.
remove_trailing_slashes () {
    printf '%s' "$1" | sed 's:/*$::'
}

# homedir_name_check homedir
#  Checks its argument for dangerous characters.
homedir_name_check () {
    local homedir="$1"
    case "$homedir" in
        *':'*)      fail 8 "homedir with colon rejected: $homedir" ;;
        *'*'*)      fail 8 "homedir with asterisk rejected: $homedir" ;;
        *'`'*)      fail 8 "homedir with backtick rejected: $homedir" ;;
        *'"'*|*"'"*)    fail 8 "homedir with quote rejected: $homedir" ;;
        *"\\")      fail 8 "homedir with backslash rejected: $homedir" ;;
    esac
    true
}

# homedir_symlink_check homedir
#  Checks that its argument's path components do not contain any symlinks
#  and are all existing, real directories.
homedir_symlink_check () {
    local homedir="$(remove_trailing_slashes "$1")"
        # trailing slash has to go, or the -L test will RESOLVE the symlink instead of recognizing it!
    local n_max=50
    local stopdir='/'

    local testdir="$homedir"
    while [ -n "$testdir" ] && [ "$testdir" != "$stopdir" ]; do
        [ ! -L "$testdir" ]  || fail 4 "homedir component is a symlink: $testdir"
        [   -d "$testdir" ]  || fail 5 "homedir component is not a directory: $testdir"

        [ $n_max -gt 0 ]  || fail 6 "too many homedir components: $homedir"
        n_max=$((n_max - 1))

        # go to next-higher path component:
        testdir="$(dirname -- "$testdir")"  || fail 7 "could not resolve component: $homedir"
    done
    true
}


## Integrity checks:  ##########################################################

homedir="$(user_homedir "$username")"
# Now we know that the user exists and has an existing homedir.

# Make sure that the homedir does not contain any dangerous characters.
# In theory, they should not be a real problem,
# but we'd have to escape them in our final output.
# Since special characters in homedir names are really uncommon,
# we'll just reject them altogether:
homedir_name_check "$homedir"

# None of the homedir components can be a symlink!
# Otherwise the main account could remove the sub account's homedir
# and replace it with a symlink to, say, /root/secrets/.
# No matter what modes /root/ has -- as long as the sub account
# could enter secrets/ itself, they can read all files there.
# To prevent this, we check all path components:
homedir_symlink_check "$homedir"


## Prepare the environment:  ###################################################

# The /jail directory has to belong to root (or internal-sftp won't chroot).
# Restrictive modes are essential, or local users might access the jail mountpoints,
# circumventing the /home/$BASE_USER modes.
chmod -- 0700      "$MOUNT_TO"
chown -- root:root "$MOUNT_TO"


## Perform the mount:  #########################################################

# Finally tell autofs to bind-mount /jail/$username/$homedir to the real $homedir.
# 
# Because /jail/$username does not actually exist (yet),
# autofs will temporarily create it (root:root 0755)
# and all the other path components of $homedir, including home/.
# 
# But if we were to emit a "/" mount too (expanding to /jail/$username),
# it would have to contain an empty $homedir mount point!

# "key" is the relative mountpoint directory. Our base directory is /jail,
#  so the key is the requested directory name therein -- the username.
# "location" is the device/directory/network resource to mount.
#  The ":" prefix indicates a local device/directory.
#          key  [options]     location
printf -- '"/%s" -fstype=bind ":%s"\n'  "$homedir" "$homedir"

That's the basics of it. On another note, I've always heard that S3FS was not reliable. I'm not sure how true that is anymore, but if you do wind up having issues with it, I'd front S3 with AWS File Gateway.