Applicable when
- Transfer Family is used to provide SFTP access to the customers
- Custom identity management should be used
Implementation
First, we need to create identity provider lambda
import { APIGatewayProxyEvent, APIGatewayProxyResult, PolicyDocument } from 'aws-lambda';
import { IdentityProviderResponse } from './identity-provider-response.model';
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
const username = event.pathParameters?.username as string;
const password = event.headers?.Password;
// If credentials are invalid, status code is still 200 but role should be blank
const responseBody: IdentityProviderResponse = {};
if (await isValidCredentials(username, password)) {
const homeFolder = username; // Set whatever home folder you want
responseBody.Role = process.env.UPLOAD_ROLE_ARN as string;
responseBody.Policy = JSON.stringify(getFolderRestrictedPolicy(homeFolder));
responseBody.HomeDirectory = `/${process.env.UPLOAD_BUCKET_NAME}/${homeFolder}`;
}
return {
statusCode: 200,
body: JSON.stringify(responseBody),
};
}
async function isValidCredentials(username: string, password: string): Promise<boolean> {
// Your authentication code here
return true;
}
// Only allow upload operation inside of the home folder
function getFolderRestrictedPolicy(homeFolder: string): PolicyDocument {
return {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['s3:ListBucket'],
Resource: `arn:aws:s3:::${process.env.UPLOAD_BUCKET_NAME}`,
Condition: {
StringLike: {
's3:prefix': [`${homeFolder}/*`],
},
},
},
{
Effect: 'Allow',
Action: ['s3:PutObject*'],
Resource: [
`arn:aws:s3:::${process.env.UPLOAD_BUCKET_NAME}/${homeFolder}/`,
],
},
],
};
}
Now create CDK code with Transfer Family configuration
import { AuthorizationType, LambdaIntegration } from '@aws-cdk/aws-apigateway';
import { PolicyDocument, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
import { Bucket } from '@aws-cdk/aws-s3';
import { CfnServer } from '@aws-cdk/aws-transfer';
import { App, Stack, StackProps } from '@aws-cdk/core';
import { UPLOAD_BUCKET_TAG, withEnv } from '../util/consts';
import { LambdasStack } from './lambdas-stack';
import { RestApiStack } from './rest-api-stack';
export class TransferFamilyStack extends Stack {
public lambdas: LambdasStack;
public restAPI: RestApiStack;
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
// S3 bucket for customer uploads
const uploadBucketName = withEnv(UPLOAD_BUCKET_TAG);
const uploadBucket = new Bucket(this, uploadBucketName, {
bucketName: uploadBucketName
});
// Custom identity provider lambda
const identityProviderLambda = this.lambdas.createLambda('<your path>', 'IdentityProvider');
// Role for the uploading user (basic access restriction)
const uploadRole = new Role(this, withEnv('upload-transfer-family-role'), {
roleName: withEnv('upload-transfer-family-role'),
assumedBy: new ServicePrincipal('transfer.amazonaws.com'),
inlinePolicies: {
allowS3FileUpload: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['s3:ListBucket'],
Resource: uploadBucket.bucketArn
}, {
Effect: 'Allow',
Action: ['s3:PutObject*'],
Resource: `${ uploadBucket.bucketArn }/*`
}, {
Action: ['s3:PutObject'],
Effect: 'Deny',
Resource: `${ uploadBucket.bucketArn }/*/`
}]
})
}
});
identityProviderLambda.addEnvironment('UPLOAD_ROLE_ARN', uploadRole.roleArn);
identityProviderLambda.addEnvironment('UPLOAD_BUCKET_NAME', uploadBucket.bucketName);
// Create custom identity provider endpoint - URL should be exactly this one
this.restAPI
.getResource('servers/{serverId}/users/{username}/config')
.addMethod('GET', new LambdaIntegration(identityProviderLambda, { proxy: true }), {
authorizationType: AuthorizationType.IAM
});
// ApiGateway role to access custom identity provider endpoint
const apiGatewayRoleName = withEnv('api-gateway-invocation-role');
const apiGatewayAccessRole = new Role(this, apiGatewayRoleName, {
roleName: apiGatewayRoleName,
assumedBy: new ServicePrincipal('transfer.amazonaws.com'),
inlinePolicies: {
allowIdentityProviderInvocation: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['execute-api:Invoke'],
Resource: this.restAPI.api.arnForExecuteApi('GET', '/servers/*')
}]
})
}
});
// Transfer Family logging role
const cloudwatchRoleName = withEnv('write-to-cloudwatch');
const cloudwatchWriterRole = new Role(this, cloudwatchRoleName, {
roleName: cloudwatchRoleName,
assumedBy: new ServicePrincipal('transfer.amazonaws.com'),
inlinePolicies: {
allowLogging: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents'],
Resource: `arn:aws:logs:${ this.region }:${ this.account }:log-group:/aws/transfer/*`
}]
})
}
});
// Ftp server
const ftpServer = new CfnServer(this, withEnv('sftp-transfer-server'), {
identityProviderType: 'API_GATEWAY',
protocols: ['SFTP'],
loggingRole: cloudwatchWriterRole.roleArn,
identityProviderDetails: {
invocationRole: apiGatewayAccessRole.roleArn,
url: this.restAPI.api.url
}
});
}
}
Comments
0 comments
Please sign in to leave a comment.