Skip to main content

Transfer Family (SFTP / FTPS)

SFTP Port - 22 FTP(S) Port - 21

Create a VPC

  • Create a VPC
  • Create Subnets (Public); need at least one
  • Create internet gateway
  • Security Group must allow the following rules
security groups inbound

Policy to access S3 Bucket

There are two steps here to create a "Scope-down" policy to only allow users access to their home directory

1. Create an IAM Role

This step will create a role that allows allows read and write access to a specific Amazon S3 bucket. This IAM role CANNOT containe the AWS Transfer variables (i.e. ${transfer:XXXXXX}), those can only be used in the next step.

"Version": "2012-10-17",
"Statement": [
"Sid": "ReadWriteS3",
"Action": ["s3:ListBucket"],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::bucketname"]
"Effect": "Allow",
"Action": [
"Resource": ["arn:aws:s3:::bucketname/*"]

2. Further scope-down Policy

The previous step allows users to acces the bucket, and do various actions. Only that must exisit with the IAM Role policy.

For a custom authorizer, we can now additionally pass down a scope-down policy to limit the users access to specific directories.

Only the below extra attached policy is able to resolve the transfer service variables

import {PolicyDocument} from 'aws-lambda'

const bucketArn = process.env.BUCKET_ARN || ''
const roleArn = process.env.TRADING_PARTNER_ROLE_ARN || ''
const bucketName = process.env.BUCKET_NAME || ''

type AuthResponse = {
Role: string
HomeDirectory?: string
Policy: string
HomeDirectoryType?: string
HomeDirectoryDetails?: string
PublicKeys?: string[]

type PolicyParameters = {
username: string
effect?: string
resource?: string
context?: any

function directoryMap(username) {
return [
Entry: '/',
Target: '/' + bucketName + '/' + username

// // IMPORTANT NOTE: Policy parameter must be a string, it doesn’t work with an object type.
export const generatePolicy = ({username, effect, resource, context}: PolicyParameters) => {
const authResponse: AuthResponse = {
Role: roleArn, // ARN of the IAM Role that contains the policy used to provide the user access to your S3 bucket.
Policy: JSON.stringify(userPolicy),
HomeDirectoryType: 'LOGICAL',
HomeDirectoryDetails: JSON.stringify(directoryMap(username))
return authResponse

const userPolicy: PolicyDocument = {
Version: '2012-10-17',
Statement: [
Sid: 'AllowListingOfUserFolder',
Action: ['s3:ListBucket', 's3:GetBucketLocation'],
Effect: 'Allow',
Resource: [bucketArn],
Condition: {
StringLike: {
's3:prefix': ['${transfer:UserName}/*', '${transfer:UserName}']
Sid: 'HomeDirObjectAccess',
Effect: 'Allow',
Action: [
Resource: 'arn:aws:s3:::' + bucketName + '/${transfer:UserName}/*'
Sid: 'DenyMkdir',
Action: ['s3:PutObject'],
Effect: 'Deny',
Resource: `${bucketArn}/*/`

So when our authorizer returns a response to AWS Transfer, it will supply the role for the user which gives the user permission to access the S3 bucket, and then the additional policy we provide in the response limits the user's access to only their home folder.

Copied from AWS Docs:

  • The transfer:HomeBucket parameter is replaced with the first component of HomeDirectory.
  • The transfer:HomeFolder parameter is replaced with the remaining portions of the HomeDirectory parameter.
  • The transfer:HomeDirectory parameter has the leading forward slash (/) removed so that it can be used as part of an S3 Amazon Resource Name (ARN) in a Resource statement.

For example, assume that the HomeDirectory parameter that is configured for the transfer user is /home/bob/amazon/stuff/.

  • transfer:HomeBucket is set to /home.
  • transfer:HomeFolder is set to /bob/amazon/stuff/.
  • transfer:HomeDirectory becomes home/bob/amazon/stuff/.

See here for details on user policies:

Logical directories

Building the folder structure that you want for your users is easily done using logical directories. Each logical directory is an object with two fields: Entry and Target. The Entry is the name of the folder that the user will see and the Target is the S3 folder path.

Another way: Key: HomeDirectoryDetails Value: [{"Entry": "/pics", "Target": "/bucket1/pics"}, {"Entry": "/reporting", "Target": "/bucket2/path/reporting"}] Remember when I mentioned the HomeDirectoryType=LOGICAL setting? This is automatically included in the response header by the AWS Lambda integration when it finds the HomeDirectoryDetails key in the user config.

You can't use Transfer variable except UserName with Logical

From this Forum question: "As of now, we only support ${transfer:Username} variable when using a scopedown policy in combination with Logical Directories. The reason being that multiple mappings(Entry, Target values) can be provided when using Logical Directories and we would not be able to pass all the values to a single ${transfer:Home*} variable. Also, when using Logical Directories, the User will be mapped to the specified directory and therefore you could skip the option of using a scopedown policy to restrict access to the HomeDirectory. "

"TLS resumption"

Error from FileZilla:

"This server does not support TLS session resumption on the data connection. TLS session resumption on the data connection is an important security feature to protect against data connection stealing attacks. If you continue, transferred files may be intercepted or their contents replaced by an attacker."

Detailed AWS docs

Architecture diagram:

Final Working Setup

AWS Transfer Dashboard

AWS Transfer Dashboard

AWS Transfer Server Details

AWS Transfer Server Details

VPC Dash

VPC Dashboard

VPC Details

AWS Transfer VPC Details

VPC Security Groupgs

AWS Transfer Security Groups

VPC Subnets

AWS Transfer VPC Subnets

Subnet Detail

AWS Transfer Subnet Details

Subnet Route Table

AWS Transfer Subnet Route Table

Subnet: Network ACL, Rules

AWS Transfer Subnet Network ACL Rules

Route Table

AWS Transfer VPC Route Table

SG Inbound Rules

AWS Transfer  Security Group Inbound Rules

SG Outbound Rules

AWS Transfer Security Group Outbound Rules

Network Interface

AWS Transfer Network Interface