├── .npmignore ├── imgs ├── architecture.png └── stack-output.png ├── .gitignore ├── .prettierrc ├── jest.config.js ├── lib ├── ftp │ ├── custom-authorizer │ │ ├── conftest.py │ │ ├── test_index.py │ │ ├── .gitignore │ │ └── index.py │ ├── ftp-user.ts │ └── password-authenticated-ftp.ts └── password-authenticated-ftp-stack.ts ├── bin └── password-authenticated-ftp.ts ├── CODE_OF_CONDUCT.md ├── test ├── password-authenticated-ftp.test.ts └── __snapshots__ │ └── password-authenticated-ftp.test.ts.snap ├── .github └── workflows │ ├── update_snapshot.yml │ └── build.yml ├── package.json ├── tsconfig.json ├── cdk.json ├── LICENSE ├── CONTRIBUTING.md └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /imgs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/ftp-with-password-authentication-cdk-sample/HEAD/imgs/architecture.png -------------------------------------------------------------------------------- /imgs/stack-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/ftp-with-password-authentication-cdk-sample/HEAD/imgs/stack-output.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 150 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/ftp/custom-authorizer/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ['AWS_REGION'] = 'us-west-2' 4 | os.environ['AWS_ACCESS_KEY_ID'] = 'testing' 5 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing' 6 | os.environ['AWS_SECURITY_TOKEN'] = 'testing' 7 | os.environ['AWS_SESSION_TOKEN'] = 'testing' 8 | -------------------------------------------------------------------------------- /bin/password-authenticated-ftp.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { PasswordAuthenticatedFtpStack } from '../lib/password-authenticated-ftp-stack'; 5 | 6 | const app = new cdk.App(); 7 | new PasswordAuthenticatedFtpStack(app, "FtpSampleStack"); 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /test/password-authenticated-ftp.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Template } from "aws-cdk-lib/assertions"; 3 | import { PasswordAuthenticatedFtpStack } from "../lib/password-authenticated-ftp-stack"; 4 | 5 | test("Snapshot test", () => { 6 | const app = new cdk.App(); 7 | const stack = new PasswordAuthenticatedFtpStack(app, "MyTestStack"); 8 | const template = Template.fromStack(stack); 9 | expect(template).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /.github/workflows/update_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Update snapshot 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | update: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "20.x" 18 | - run: npm ci 19 | - run: npm run test -- -u 20 | - name: Add & Commit 21 | uses: EndBug/add-and-commit@v7.2.0 22 | with: 23 | add: "test/__snapshots__/." 24 | message: "update snapshot" 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/password-authenticated-ftp", 3 | "version": "0.1.0", 4 | "bin": { 5 | "password-authenticated-ftp": "bin/password-authenticated-ftp.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^26.0.10", 15 | "@types/node": "10.17.27", 16 | "aws-cdk": "^2.1007.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.4", 19 | "ts-node": "^9.0.0", 20 | "typescript": "^4" 21 | }, 22 | "dependencies": { 23 | "@aws-cdk/aws-lambda-python-alpha": "^2.145.0-alpha.0", 24 | "aws-cdk-lib": "^2.189.1", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.16" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/password-authenticated-ftp.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 25 | "@aws-cdk/cognito:logUserPoolClientSecretValue": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | pull_request: 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | Build-and-Test-CDK: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: "20.x" 20 | - run: npm ci 21 | - run: npm run build 22 | - run: npm run test 23 | Build-and-Test-Application: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Setup Python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: "3.12" 31 | - run: cd lib/ftp/custom-authorizer 32 | - run: pip install pytest boto3 moto[secretsmanager] 33 | - run: pytest 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /lib/ftp/custom-authorizer/test_index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | import os 4 | from moto import mock_aws 5 | 6 | region = os.environ["AWS_REGION"] 7 | 8 | 9 | def create_secret(client, server_id, username, password): 10 | role_arn = "roleArn" 11 | home_directory_details = json.dumps({ 12 | "Entry": "/", 13 | "Target": "/bucketName/homeDirectory" 14 | }) 15 | client.create_secret( 16 | Name=f"ftpSecret/{server_id}/{username}", 17 | SecretString=json.dumps({ 18 | "Role": role_arn, 19 | "HomeDirectoryDetails": home_directory_details, 20 | "Password": password 21 | }) 22 | ) 23 | return role_arn, home_directory_details 24 | 25 | 26 | @mock_aws 27 | def test_with_correct_password(): 28 | from index import handler 29 | client = boto3.session.Session().client( 30 | service_name="secretsmanager", region_name=region) 31 | server_id = "server" 32 | username = "user" 33 | password = "password" 34 | role_arn, home_directory_details = create_secret( 35 | client, server_id, username, password) 36 | 37 | event = { 38 | "pathParameters": { 39 | "serverId": server_id, 40 | "username": username, 41 | }, 42 | "queryStringParameters": { 43 | "protocol": "FTP", 44 | }, 45 | "requestContext": { 46 | "identity": { 47 | "sourceIp": "8.8.8.8", 48 | }, 49 | }, 50 | "headers": { 51 | "Password": password 52 | } 53 | } 54 | 55 | response = handler(event, {}) 56 | assert response["statusCode"] == 200 57 | 58 | policy = json.loads(response["body"]) 59 | assert policy["Role"] == role_arn 60 | assert policy["HomeDirectoryDetails"] == home_directory_details 61 | assert policy["HomeDirectoryType"] == "LOGICAL" 62 | -------------------------------------------------------------------------------- /lib/ftp/ftp-user.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as sm from "aws-cdk-lib/aws-secretsmanager"; 3 | import * as iam from "aws-cdk-lib/aws-iam"; 4 | import * as s3 from "aws-cdk-lib/aws-s3"; 5 | 6 | export interface FtpUserProps { 7 | /** 8 | * username to login 9 | * 10 | * @default - lowercase of its construct id 11 | */ 12 | readonly userName?: string; 13 | 14 | /** 15 | * password for this user 16 | * 17 | * @default - password is randomly generated 18 | */ 19 | readonly password?: string; 20 | 21 | /** 22 | * the id of transfer server for this user 23 | */ 24 | readonly transferServerId: string; 25 | 26 | /** 27 | * the S3 bucket that this user has access to 28 | */ 29 | readonly accessibleBucket: s3.IBucket; 30 | 31 | /** 32 | * the path of home directory for this user 33 | */ 34 | readonly homeDirectory: string; 35 | } 36 | 37 | export class FtpUser extends Construct { 38 | readonly secret: sm.Secret; 39 | 40 | constructor(scope: Construct, id: string, props: FtpUserProps) { 41 | super(scope, id); 42 | 43 | const userName = props.userName ?? id.toLowerCase(); 44 | 45 | // requirements for IAM role for ftp users: https://docs.aws.amazon.com/transfer/latest/userguide/requirements-roles.html 46 | const userRole = new iam.Role(this, `Role`, { 47 | assumedBy: new iam.ServicePrincipal("transfer.amazonaws.com"), 48 | }); 49 | 50 | this.secret = new sm.Secret(this, `User`, { 51 | secretName: `ftpSecret/${props.transferServerId}/${userName}`, 52 | generateSecretString: { 53 | secretStringTemplate: JSON.stringify({ 54 | Role: userRole.roleArn, 55 | HomeDirectoryDetails: JSON.stringify([ 56 | { 57 | Entry: "/", 58 | Target: `/${props.accessibleBucket.bucketName}/${props.homeDirectory}`, 59 | }, 60 | ]), 61 | Password: props.password, 62 | }), 63 | generateStringKey: props.password == null ? "Password" : "Dummy", 64 | excludePunctuation: true, 65 | }, 66 | }); 67 | 68 | props.accessibleBucket.grantReadWrite(userRole, `${props.homeDirectory}`); 69 | props.accessibleBucket.grantReadWrite(userRole, `${props.homeDirectory}/*`); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/password-authenticated-ftp-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib/core"; 2 | import * as iam from "aws-cdk-lib/aws-iam"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as s3 from "aws-cdk-lib/aws-s3"; 5 | import { Construct } from "constructs"; 6 | import { PasswordAuthenticatedFtp } from "./ftp/password-authenticated-ftp"; 7 | import { FtpUser } from "./ftp/ftp-user"; 8 | 9 | export class PasswordAuthenticatedFtpStack extends cdk.Stack { 10 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | const vpc = new ec2.Vpc(this, "Vpc", { 14 | natGateways: 1, 15 | }); 16 | 17 | // an AWS Transfer server 18 | const ftp = new PasswordAuthenticatedFtp(this, `Ftp`, { 19 | vpc, 20 | protocol: "SFTP", 21 | }); 22 | 23 | // an S3 bucket which is a destination for FTP transfers 24 | const bucket = new s3.Bucket(this, `Bucket`, { 25 | encryption: s3.BucketEncryption.S3_MANAGED, 26 | autoDeleteObjects: true, 27 | removalPolicy: cdk.RemovalPolicy.DESTROY, 28 | }); 29 | 30 | // Create an FTP user with randomly generated password 31 | new FtpUser(this, `User1`, { 32 | transferServerId: ftp.server.attrServerId, 33 | accessibleBucket: bucket, 34 | homeDirectory: "home", 35 | }); 36 | 37 | // You can specify password explicitly 38 | new FtpUser(this, `User2`, { 39 | transferServerId: ftp.server.attrServerId, 40 | accessibleBucket: bucket, 41 | homeDirectory: "home", 42 | password: "password", 43 | }); 44 | 45 | // an EC2 instance to test FTP connection 46 | const client = new ec2.Instance(this, `Client`, { 47 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM), 48 | machineImage: new ec2.AmazonLinuxImage({ 49 | generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 50 | }), 51 | vpc, 52 | blockDevices: [ 53 | { 54 | deviceName: "/dev/xvda", 55 | volume: ec2.BlockDeviceVolume.ebs(30, { encrypted: true }), 56 | }, 57 | ], 58 | }); 59 | 60 | // Policy for SSM access to the EC2 instance 61 | client.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")); 62 | 63 | new cdk.CfnOutput(this, "FtpClientInstanceName", { value: client.instanceId }); 64 | new cdk.CfnOutput(this, "DestinationS3BucketName", { value: bucket.bucketName }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/ftp/custom-authorizer/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transfer Family FTP server with password authentication CDK sample 2 | [![Build](https://github.com/aws-samples/ftp-with-password-authentication-cdk-sample/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/ftp-with-password-authentication-cdk-sample/actions/workflows/build.yml) 3 | 4 | This sample shows how to define Transfer Family FTP/SFTP server with password authentication by AWS CDK. 5 | 6 | It includes CDK constructs that follows the below architecture, and a CDK stack to show how to use the constructs. 7 | 8 | ![architecture](imgs/architecture.png) 9 | 10 | You can read this article for futher detail of the architecture: [Enable password authentication for AWS Transfer Family using AWS Secrets Manager (updated)](https://aws.amazon.com/blogs/storage/enable-password-authentication-for-aws-transfer-family-using-aws-secrets-manager-updated/) 11 | 12 | ## Code sample 13 | In this sample, you can define FTP server with password authentication by the following code: 14 | 15 | ```ts 16 | 17 | // an AWS Transfer SFTP server 18 | const ftp = new PasswordAuthenticatedFtp(this, `Ftp`, { 19 | vpc, 20 | protocol: "SFTP", 21 | }); 22 | 23 | // Create an FTP user with randomly generated password 24 | new FtpUser(this, `User1`, { 25 | transferServerId: ftp.server.attrServerId, 26 | accessibleBucket: bucket, 27 | homeDirectory: "home", 28 | }); 29 | 30 | // You can also specify password explicitly 31 | new FtpUser(this, `User2`, { 32 | transferServerId: ftp.server.attrServerId, 33 | accessibleBucket: bucket, 34 | homeDirectory: "home", 35 | password: "passw0rd", 36 | }); 37 | ``` 38 | 39 | The actual constructs are located in [`lib/ftp` directory](./lib/ftp). 40 | You can copy these files into your project and freely modify them as your own requirements. 41 | 42 | ## Deploy 43 | Before deploying this sample, you must install AWS Cloud Development Kit prerequisites. [Please refer to this document](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) for the detailed instruction. Please make sure you've successfully completed `cdk bootstrap` step. 44 | 45 | After that, clone this repository and go to the root directory. 46 | 47 | You must first install Node.js dependencies for CDK code by the following commands: 48 | 49 | ```sh 50 | npm ci 51 | ``` 52 | 53 | Now you can deploy this sample stack (`FtpSampleStack`) by the following command: 54 | 55 | ```sh 56 | npx cdk deploy --require-approval never 57 | ``` 58 | 59 | Initial deployment usually takes about 10 minutes. 60 | 61 | After a successful deployment, you can check the name of the S3 bucket and EC2 instance id for the testing purpose in the stack output. 62 | 63 | ![stack output](imgs/stack-output.png) 64 | 65 | ## Usage 66 | After the deployment, let's check if the sample is successfuly deployed by actually connecting to your SFTP server. 67 | 68 | First check the IP address of your SFTP server. You can find it in [AWS Transfer Family Management console](https://console.aws.amazon.com/transfer/home). 69 | 70 | Then connect to the EC2 instance via SSM Session Manager. It will work as a SFTP client for testing. To connect your EC2 instancne, please check the following document: [Connect to your Linux instance using Session Manager](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/session-manager.html). 71 | 72 | In the EC2 ssh session, to connect your SFTP server, run the command below and enter the password. 73 | 74 | ```sh 75 | sftp [username]@[IP address of your SFTP server] 76 | ``` 77 | 78 | For this sample, you can use the below login credentials. 79 | 80 | |username|password| 81 | |--|--| 82 | |user1|Randomly generated; see [Secrets Manager console](https://console.aws.amazon.com/secretsmanager/home#!/listSecrets) for its value| 83 | |user2|password| 84 | 85 | Now you should be able to successfully login to the SFTP server. Let's test some SFTP commands to transfer files. 86 | 87 | You can find the actual transferred files in the S3 bucket. Check it in [S3 management console](https://s3.console.aws.amazon.com/s3/home). 88 | 89 | ## Clean up 90 | To avoid incurring future charges, clean up the resources you created. 91 | 92 | You can remove all the AWS resources deployed by this sample running the following command: 93 | 94 | ```sh 95 | npx cdk destroy --force 96 | ``` 97 | 98 | ## Security 99 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 100 | 101 | ## License 102 | This library is licensed under the MIT-0 License. See the LICENSE file. 103 | -------------------------------------------------------------------------------- /lib/ftp/password-authenticated-ftp.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib/core"; 2 | import * as transfer from "aws-cdk-lib/aws-transfer"; 3 | import * as agw from "aws-cdk-lib/aws-apigateway"; 4 | import * as lambda from "@aws-cdk/aws-lambda-python-alpha"; 5 | import * as logs from "aws-cdk-lib/aws-logs"; 6 | import { Runtime } from "aws-cdk-lib/aws-lambda"; 7 | import * as iam from "aws-cdk-lib/aws-iam"; 8 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 9 | import { Construct } from "constructs"; 10 | 11 | export interface PasswordAuthenticatedFtpProps { 12 | readonly vpc: ec2.IVpc; 13 | 14 | /** 15 | * Cidrs allowed to connect this Transfer server. 16 | * 17 | * @default - [props.vpc.vpcCidrBlock] 18 | */ 19 | readonly allowedCidrs?: string[]; 20 | 21 | /** 22 | * Protocol for Transfer server 23 | * 24 | * @default - "SFTP" 25 | */ 26 | readonly protocol?: "SFTP" | "FTP"; 27 | } 28 | 29 | export class PasswordAuthenticatedFtp extends Construct { 30 | readonly server: transfer.CfnServer; 31 | 32 | constructor(scope: Construct, id: string, props: PasswordAuthenticatedFtpProps) { 33 | super(scope, id); 34 | 35 | const vpc = props.vpc; 36 | const { allowedCidrs = [vpc.vpcCidrBlock], protocol = "SFTP" } = props; 37 | 38 | const apiLog = new logs.LogGroup(this, "TransferServiceAuthApiLogs"); 39 | const api = new agw.RestApi(this, `TransferServiceAuthApi`, { 40 | deployOptions: { 41 | accessLogDestination: new agw.LogGroupLogDestination(apiLog), 42 | accessLogFormat: agw.AccessLogFormat.jsonWithStandardFields(), 43 | }, 44 | }); 45 | 46 | const authHandler = new lambda.PythonFunction(this, `AuthHandler`, { 47 | entry: "lib/ftp/custom-authorizer", 48 | runtime: Runtime.PYTHON_3_12, 49 | }); 50 | 51 | const route = api.root.addResource("servers").addResource("{serverId}").addResource("users").addResource("{username}").addResource("config"); 52 | route.addMethod("GET", new agw.LambdaIntegration(authHandler), { 53 | authorizationType: agw.AuthorizationType.IAM, 54 | }); 55 | 56 | const transferServerSg = new ec2.SecurityGroup(this, "SecurityGroup", { 57 | vpc, 58 | }); 59 | 60 | // FTP server uses Port 21 (Control Channel) and Port Range 8192-8200 (Data Channel). 61 | // https://docs.aws.amazon.com/transfer/latest/userguide/create-server-ftp.html 62 | const ports = protocol == "SFTP" ? [ec2.Port.tcp(22)] : [ec2.Port.tcp(21), ec2.Port.tcpRange(8192, 8200)]; 63 | for (const cidr of allowedCidrs) { 64 | for (const port of ports) { 65 | transferServerSg.addIngressRule(ec2.Peer.ipv4(cidr), port); 66 | } 67 | } 68 | 69 | const loggingRole = new iam.Role(this, `LoggingRole`, { 70 | assumedBy: new iam.ServicePrincipal("transfer.amazonaws.com"), 71 | }); 72 | loggingRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSTransferLoggingAccess")); 73 | 74 | const authenticationRole = new iam.Role(this, `AuthenticationRole`, { 75 | assumedBy: new iam.ServicePrincipal("transfer.amazonaws.com"), 76 | }); 77 | authenticationRole.addToPolicy( 78 | new iam.PolicyStatement({ 79 | effect: iam.Effect.ALLOW, 80 | resources: [api.arnForExecuteApi()], 81 | actions: ["execute-api:Invoke"], 82 | }), 83 | ); 84 | 85 | const server = new transfer.CfnServer(this, "Server", { 86 | endpointDetails: { 87 | subnetIds: vpc.privateSubnets.map((s) => s.subnetId), 88 | vpcId: vpc.vpcId, 89 | securityGroupIds: [transferServerSg.securityGroupId], 90 | }, 91 | endpointType: "VPC", 92 | protocols: [protocol], 93 | loggingRole: loggingRole.roleArn, 94 | identityProviderType: "API_GATEWAY", 95 | identityProviderDetails: { 96 | url: api.url, 97 | invocationRole: authenticationRole.roleArn, 98 | }, 99 | domain: "S3", 100 | }); 101 | 102 | // cannot use inline policy due to circular dependency 103 | const authHandlerPolicy = new iam.Policy(this, "AuthHandlerPolicy", { 104 | statements: [ 105 | new iam.PolicyStatement({ 106 | effect: iam.Effect.ALLOW, 107 | resources: [ 108 | cdk.Stack.of(this).formatArn({ 109 | service: "secretsmanager", 110 | resource: `secret:ftpSecret/${server.attrServerId}/*`, 111 | }), 112 | ], 113 | actions: ["secretsmanager:GetSecretValue"], 114 | }), 115 | ], 116 | }); 117 | 118 | authHandlerPolicy.attachToRole(authHandler.role!); 119 | 120 | this.server = server; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/ftp/custom-authorizer/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import boto3 4 | import base64 5 | from ipaddress import ip_network, ip_address 6 | from botocore.exceptions import ClientError 7 | 8 | # modified the code from this blog: https://aws.amazon.com/blogs/storage/enable-password-authentication-for-aws-transfer-family-using-aws-secrets-manager-updated/ 9 | 10 | region = os.environ["AWS_REGION"] 11 | # Create a Secrets Manager client 12 | sm_client = boto3.session.Session().client( 13 | service_name="secretsmanager", region_name=region) 14 | 15 | 16 | def handler(event, context): 17 | response = handler_helper(event) 18 | return { 19 | 'statusCode': 200, 20 | 'body': json.dumps(response) 21 | } 22 | 23 | 24 | def handler_helper(event): 25 | print(event) 26 | 27 | input_serverId = event["pathParameters"]["serverId"] 28 | input_username = event["pathParameters"]["username"] 29 | input_protocol = event["queryStringParameters"]["protocol"] 30 | input_sourceIp = event["requestContext"]["identity"]["sourceIp"] 31 | input_password = event["headers"].get("Password", "") 32 | 33 | print("ServerId: {}, Username: {}, Protocol: {}, SourceIp: {}" 34 | .format(input_serverId, input_username, input_protocol, input_sourceIp)) 35 | 36 | # Check for password and set authentication type appropriately. No password means SSH auth 37 | print("Start User Authentication Flow") 38 | if input_password != "": 39 | print("Using PASSWORD authentication") 40 | authentication_type = "PASSWORD" 41 | else: 42 | if input_protocol == 'FTP' or input_protocol == 'FTPS': 43 | print("Empty password not allowed for FTP/S") 44 | return {} 45 | print("Using SSH authentication") 46 | authentication_type = "SSH" 47 | 48 | # Retrieve our user details from the secret. For all key-value pairs stored in SecretManager, 49 | # checking the protocol-specified secret first, then use generic ones. 50 | # e.g. If SFTPPassword and Password both exists, will be using SFTPPassword for authentication 51 | secret = get_secret("ftpSecret/" + input_serverId + "/" + input_username) 52 | 53 | if secret is not None: 54 | secret_dict = json.loads(secret) 55 | # Run our password checks 56 | user_authenticated = authenticate_user( 57 | authentication_type, secret_dict, input_password, input_protocol) 58 | # Run sourceIp checks 59 | ip_match = check_ipaddress(secret_dict, input_sourceIp, input_protocol) 60 | 61 | if user_authenticated and ip_match: 62 | print("User authenticated, calling build_response with: " + 63 | authentication_type) 64 | return build_response(secret_dict, authentication_type, input_protocol) 65 | else: 66 | print("User failed authentication return empty response") 67 | return {} 68 | else: 69 | # Otherwise something went wrong. Most likely the object name is not there 70 | print("Secrets Manager exception thrown - Returning empty response") 71 | # Return an empty data response meaning the user was not authenticated 72 | return {} 73 | 74 | 75 | def lookup(secret_dict, key, input_protocol): 76 | if input_protocol + key in secret_dict: 77 | print("Found protocol-specified {}".format(key)) 78 | return secret_dict[input_protocol + key] 79 | else: 80 | return secret_dict.get(key, None) 81 | 82 | 83 | def check_ipaddress(secret_dict, input_sourceIp, input_protocol): 84 | accepted_ip_network = lookup( 85 | secret_dict, "AcceptedIpNetwork", input_protocol) 86 | if not accepted_ip_network: 87 | # No IP provided so skip checks 88 | print("No IP range provided - Skip IP check") 89 | return True 90 | 91 | net = ip_network(accepted_ip_network) 92 | if ip_address(input_sourceIp) in net: 93 | print("Source IP address match") 94 | return True 95 | else: 96 | print("Source IP address not in range") 97 | return False 98 | 99 | 100 | def authenticate_user(auth_type, secret_dict, input_password, input_protocol): 101 | # Function returns True if: auth_type is password and passwords match or auth_type is SSH. Otherwise returns False 102 | if auth_type == "SSH": 103 | # Place for additional checks in future 104 | print("Skip password check as SSH login request") 105 | return True 106 | # auth_type could only be SSH or PASSWORD 107 | else: 108 | # Retrieve the password from the secret if exists 109 | password = lookup(secret_dict, "Password", input_protocol) 110 | if not password: 111 | print("Unable to authenticate user - No field match in Secret for password") 112 | return False 113 | 114 | if input_password == password: 115 | return True 116 | else: 117 | print("Unable to authenticate user - Incoming password does not match stored") 118 | return False 119 | 120 | 121 | # Build out our response data for an authenticated response 122 | def build_response(secret_dict, auth_type, input_protocol): 123 | response_data = {} 124 | # Check for each key value pair. These are required so set to empty string if missing 125 | role = lookup(secret_dict, "Role", input_protocol) 126 | if role: 127 | response_data["Role"] = role 128 | else: 129 | print("No field match for role - Set empty string in response") 130 | response_data["Role"] = "" 131 | 132 | # These are optional so ignore if not present 133 | policy = lookup(secret_dict, "Policy", input_protocol) 134 | if policy: 135 | response_data["Policy"] = policy 136 | 137 | # External Auth providers support chroot and virtual folder assignments so we'll check for that 138 | home_directory_details = lookup( 139 | secret_dict, "HomeDirectoryDetails", input_protocol) 140 | if home_directory_details: 141 | print("HomeDirectoryDetails found - Applying setting for virtual folders - " 142 | "Note: Cannot be used in conjunction with key: HomeDirectory") 143 | response_data["HomeDirectoryDetails"] = home_directory_details 144 | # If we have a virtual folder setup then we also need to set HomeDirectoryType to "Logical" 145 | print("Setting HomeDirectoryType to LOGICAL") 146 | response_data["HomeDirectoryType"] = "LOGICAL" 147 | 148 | # Note that HomeDirectory and HomeDirectoryDetails / Logical mode 149 | # can't be used together but we're not checking for this 150 | home_directory = lookup(secret_dict, "HomeDirectory", input_protocol) 151 | if home_directory: 152 | print("HomeDirectory found - Note: Cannot be used in conjunction with key: HomeDirectoryDetails") 153 | response_data["HomeDirectory"] = home_directory 154 | 155 | if auth_type == "SSH": 156 | public_key = lookup(secret_dict, "PublicKey", input_protocol) 157 | if public_key: 158 | response_data["PublicKeys"] = [public_key] 159 | else: 160 | # SSH Auth Flow - We don't have keys so we can't help 161 | print("Unable to authenticate user - No public keys found") 162 | return {} 163 | 164 | return response_data 165 | 166 | 167 | def get_secret(id): 168 | print("Secrets Manager Region: " + region) 169 | print("Secret Name: " + id) 170 | 171 | try: 172 | resp = sm_client.get_secret_value(SecretId=id) 173 | # Decrypts secret using the associated KMS CMK. 174 | # Depending on whether the secret is a string or binary, one of these fields will be populated. 175 | if "SecretString" in resp: 176 | print("Found Secret String") 177 | return resp["SecretString"] 178 | else: 179 | print("Found Binary Secret") 180 | return base64.b64decode(resp["SecretBinary"]) 181 | except ClientError as err: 182 | print("Error Talking to SecretsManager: " + err.response["Error"]["Code"] + ", Message: " + 183 | err.response["Error"]["Message"]) 184 | return None 185 | -------------------------------------------------------------------------------- /test/__snapshots__/password-authenticated-ftp.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot test 1`] = ` 4 | { 5 | "Mappings": { 6 | "LatestNodeRuntimeMap": { 7 | "af-south-1": { 8 | "value": "nodejs20.x", 9 | }, 10 | "ap-east-1": { 11 | "value": "nodejs20.x", 12 | }, 13 | "ap-northeast-1": { 14 | "value": "nodejs20.x", 15 | }, 16 | "ap-northeast-2": { 17 | "value": "nodejs20.x", 18 | }, 19 | "ap-northeast-3": { 20 | "value": "nodejs20.x", 21 | }, 22 | "ap-south-1": { 23 | "value": "nodejs20.x", 24 | }, 25 | "ap-south-2": { 26 | "value": "nodejs20.x", 27 | }, 28 | "ap-southeast-1": { 29 | "value": "nodejs20.x", 30 | }, 31 | "ap-southeast-2": { 32 | "value": "nodejs20.x", 33 | }, 34 | "ap-southeast-3": { 35 | "value": "nodejs20.x", 36 | }, 37 | "ap-southeast-4": { 38 | "value": "nodejs20.x", 39 | }, 40 | "ap-southeast-5": { 41 | "value": "nodejs20.x", 42 | }, 43 | "ap-southeast-7": { 44 | "value": "nodejs20.x", 45 | }, 46 | "ca-central-1": { 47 | "value": "nodejs20.x", 48 | }, 49 | "ca-west-1": { 50 | "value": "nodejs20.x", 51 | }, 52 | "cn-north-1": { 53 | "value": "nodejs20.x", 54 | }, 55 | "cn-northwest-1": { 56 | "value": "nodejs20.x", 57 | }, 58 | "eu-central-1": { 59 | "value": "nodejs20.x", 60 | }, 61 | "eu-central-2": { 62 | "value": "nodejs20.x", 63 | }, 64 | "eu-isoe-west-1": { 65 | "value": "nodejs18.x", 66 | }, 67 | "eu-north-1": { 68 | "value": "nodejs20.x", 69 | }, 70 | "eu-south-1": { 71 | "value": "nodejs20.x", 72 | }, 73 | "eu-south-2": { 74 | "value": "nodejs20.x", 75 | }, 76 | "eu-west-1": { 77 | "value": "nodejs20.x", 78 | }, 79 | "eu-west-2": { 80 | "value": "nodejs20.x", 81 | }, 82 | "eu-west-3": { 83 | "value": "nodejs20.x", 84 | }, 85 | "il-central-1": { 86 | "value": "nodejs20.x", 87 | }, 88 | "me-central-1": { 89 | "value": "nodejs20.x", 90 | }, 91 | "me-south-1": { 92 | "value": "nodejs20.x", 93 | }, 94 | "mx-central-1": { 95 | "value": "nodejs20.x", 96 | }, 97 | "sa-east-1": { 98 | "value": "nodejs20.x", 99 | }, 100 | "us-east-1": { 101 | "value": "nodejs20.x", 102 | }, 103 | "us-east-2": { 104 | "value": "nodejs20.x", 105 | }, 106 | "us-gov-east-1": { 107 | "value": "nodejs20.x", 108 | }, 109 | "us-gov-west-1": { 110 | "value": "nodejs20.x", 111 | }, 112 | "us-iso-east-1": { 113 | "value": "nodejs18.x", 114 | }, 115 | "us-iso-west-1": { 116 | "value": "nodejs18.x", 117 | }, 118 | "us-isob-east-1": { 119 | "value": "nodejs18.x", 120 | }, 121 | "us-west-1": { 122 | "value": "nodejs20.x", 123 | }, 124 | "us-west-2": { 125 | "value": "nodejs20.x", 126 | }, 127 | }, 128 | }, 129 | "Outputs": { 130 | "DestinationS3BucketName": { 131 | "Value": { 132 | "Ref": "Bucket83908E77", 133 | }, 134 | }, 135 | "FtpClientInstanceName": { 136 | "Value": { 137 | "Ref": "Client4A7F64DF", 138 | }, 139 | }, 140 | "FtpTransferServiceAuthApiEndpoint7F8DF193": { 141 | "Value": { 142 | "Fn::Join": [ 143 | "", 144 | [ 145 | "https://", 146 | { 147 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 148 | }, 149 | ".execute-api.", 150 | { 151 | "Ref": "AWS::Region", 152 | }, 153 | ".", 154 | { 155 | "Ref": "AWS::URLSuffix", 156 | }, 157 | "/", 158 | { 159 | "Ref": "FtpTransferServiceAuthApiDeploymentStageprodD1F90C1F", 160 | }, 161 | "/", 162 | ], 163 | ], 164 | }, 165 | }, 166 | }, 167 | "Parameters": { 168 | "BootstrapVersion": { 169 | "Default": "/cdk-bootstrap/hnb659fds/version", 170 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 171 | "Type": "AWS::SSM::Parameter::Value", 172 | }, 173 | "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { 174 | "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2", 175 | "Type": "AWS::SSM::Parameter::Value", 176 | }, 177 | }, 178 | "Resources": { 179 | "Bucket83908E77": { 180 | "DeletionPolicy": "Delete", 181 | "Properties": { 182 | "BucketEncryption": { 183 | "ServerSideEncryptionConfiguration": [ 184 | { 185 | "ServerSideEncryptionByDefault": { 186 | "SSEAlgorithm": "AES256", 187 | }, 188 | }, 189 | ], 190 | }, 191 | "Tags": [ 192 | { 193 | "Key": "aws-cdk:auto-delete-objects", 194 | "Value": "true", 195 | }, 196 | ], 197 | }, 198 | "Type": "AWS::S3::Bucket", 199 | "UpdateReplacePolicy": "Delete", 200 | }, 201 | "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { 202 | "DeletionPolicy": "Delete", 203 | "DependsOn": [ 204 | "BucketPolicyE9A3008A", 205 | ], 206 | "Properties": { 207 | "BucketName": { 208 | "Ref": "Bucket83908E77", 209 | }, 210 | "ServiceToken": { 211 | "Fn::GetAtt": [ 212 | "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", 213 | "Arn", 214 | ], 215 | }, 216 | }, 217 | "Type": "Custom::S3AutoDeleteObjects", 218 | "UpdateReplacePolicy": "Delete", 219 | }, 220 | "BucketPolicyE9A3008A": { 221 | "Properties": { 222 | "Bucket": { 223 | "Ref": "Bucket83908E77", 224 | }, 225 | "PolicyDocument": { 226 | "Statement": [ 227 | { 228 | "Action": [ 229 | "s3:PutBucketPolicy", 230 | "s3:GetBucket*", 231 | "s3:List*", 232 | "s3:DeleteObject*", 233 | ], 234 | "Effect": "Allow", 235 | "Principal": { 236 | "AWS": { 237 | "Fn::GetAtt": [ 238 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 239 | "Arn", 240 | ], 241 | }, 242 | }, 243 | "Resource": [ 244 | { 245 | "Fn::GetAtt": [ 246 | "Bucket83908E77", 247 | "Arn", 248 | ], 249 | }, 250 | { 251 | "Fn::Join": [ 252 | "", 253 | [ 254 | { 255 | "Fn::GetAtt": [ 256 | "Bucket83908E77", 257 | "Arn", 258 | ], 259 | }, 260 | "/*", 261 | ], 262 | ], 263 | }, 264 | ], 265 | }, 266 | ], 267 | "Version": "2012-10-17", 268 | }, 269 | }, 270 | "Type": "AWS::S3::BucketPolicy", 271 | }, 272 | "Client4A7F64DF": { 273 | "DependsOn": [ 274 | "ClientInstanceRole5C37E2F7", 275 | ], 276 | "Properties": { 277 | "AvailabilityZone": { 278 | "Fn::Select": [ 279 | 0, 280 | { 281 | "Fn::GetAZs": "", 282 | }, 283 | ], 284 | }, 285 | "BlockDeviceMappings": [ 286 | { 287 | "DeviceName": "/dev/xvda", 288 | "Ebs": { 289 | "Encrypted": true, 290 | "VolumeSize": 30, 291 | }, 292 | }, 293 | ], 294 | "IamInstanceProfile": { 295 | "Ref": "ClientInstanceProfile89AE75A3", 296 | }, 297 | "ImageId": { 298 | "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter", 299 | }, 300 | "InstanceType": "t3.medium", 301 | "SecurityGroupIds": [ 302 | { 303 | "Fn::GetAtt": [ 304 | "ClientInstanceSecurityGroupDD1AF628", 305 | "GroupId", 306 | ], 307 | }, 308 | ], 309 | "SubnetId": { 310 | "Ref": "VpcPrivateSubnet1Subnet536B997A", 311 | }, 312 | "Tags": [ 313 | { 314 | "Key": "Name", 315 | "Value": "MyTestStack/Client", 316 | }, 317 | ], 318 | "UserData": { 319 | "Fn::Base64": "#!/bin/bash", 320 | }, 321 | }, 322 | "Type": "AWS::EC2::Instance", 323 | }, 324 | "ClientInstanceProfile89AE75A3": { 325 | "Properties": { 326 | "Roles": [ 327 | { 328 | "Ref": "ClientInstanceRole5C37E2F7", 329 | }, 330 | ], 331 | }, 332 | "Type": "AWS::IAM::InstanceProfile", 333 | }, 334 | "ClientInstanceRole5C37E2F7": { 335 | "Properties": { 336 | "AssumeRolePolicyDocument": { 337 | "Statement": [ 338 | { 339 | "Action": "sts:AssumeRole", 340 | "Effect": "Allow", 341 | "Principal": { 342 | "Service": "ec2.amazonaws.com", 343 | }, 344 | }, 345 | ], 346 | "Version": "2012-10-17", 347 | }, 348 | "ManagedPolicyArns": [ 349 | { 350 | "Fn::Join": [ 351 | "", 352 | [ 353 | "arn:", 354 | { 355 | "Ref": "AWS::Partition", 356 | }, 357 | ":iam::aws:policy/AmazonSSMManagedInstanceCore", 358 | ], 359 | ], 360 | }, 361 | ], 362 | "Tags": [ 363 | { 364 | "Key": "Name", 365 | "Value": "MyTestStack/Client", 366 | }, 367 | ], 368 | }, 369 | "Type": "AWS::IAM::Role", 370 | }, 371 | "ClientInstanceSecurityGroupDD1AF628": { 372 | "Properties": { 373 | "GroupDescription": "MyTestStack/Client/InstanceSecurityGroup", 374 | "SecurityGroupEgress": [ 375 | { 376 | "CidrIp": "0.0.0.0/0", 377 | "Description": "Allow all outbound traffic by default", 378 | "IpProtocol": "-1", 379 | }, 380 | ], 381 | "Tags": [ 382 | { 383 | "Key": "Name", 384 | "Value": "MyTestStack/Client", 385 | }, 386 | ], 387 | "VpcId": { 388 | "Ref": "Vpc8378EB38", 389 | }, 390 | }, 391 | "Type": "AWS::EC2::SecurityGroup", 392 | }, 393 | "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { 394 | "DependsOn": [ 395 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 396 | ], 397 | "Properties": { 398 | "Code": { 399 | "S3Bucket": { 400 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 401 | }, 402 | "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip", 403 | }, 404 | "Description": { 405 | "Fn::Join": [ 406 | "", 407 | [ 408 | "Lambda function for auto-deleting objects in ", 409 | { 410 | "Ref": "Bucket83908E77", 411 | }, 412 | " S3 bucket.", 413 | ], 414 | ], 415 | }, 416 | "Handler": "index.handler", 417 | "MemorySize": 128, 418 | "Role": { 419 | "Fn::GetAtt": [ 420 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 421 | "Arn", 422 | ], 423 | }, 424 | "Runtime": { 425 | "Fn::FindInMap": [ 426 | "LatestNodeRuntimeMap", 427 | { 428 | "Ref": "AWS::Region", 429 | }, 430 | "value", 431 | ], 432 | }, 433 | "Timeout": 900, 434 | }, 435 | "Type": "AWS::Lambda::Function", 436 | }, 437 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { 438 | "Properties": { 439 | "AssumeRolePolicyDocument": { 440 | "Statement": [ 441 | { 442 | "Action": "sts:AssumeRole", 443 | "Effect": "Allow", 444 | "Principal": { 445 | "Service": "lambda.amazonaws.com", 446 | }, 447 | }, 448 | ], 449 | "Version": "2012-10-17", 450 | }, 451 | "ManagedPolicyArns": [ 452 | { 453 | "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 454 | }, 455 | ], 456 | }, 457 | "Type": "AWS::IAM::Role", 458 | }, 459 | "FtpAuthHandler13566C97": { 460 | "DependsOn": [ 461 | "FtpAuthHandlerServiceRole936451CF", 462 | ], 463 | "Properties": { 464 | "Code": { 465 | "S3Bucket": { 466 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 467 | }, 468 | "S3Key": "510b9a7ca59d6bbb4b07f222180c1bdc26e0fe2415a52adefb7cf5673cba91ce.zip", 469 | }, 470 | "Handler": "index.handler", 471 | "Role": { 472 | "Fn::GetAtt": [ 473 | "FtpAuthHandlerServiceRole936451CF", 474 | "Arn", 475 | ], 476 | }, 477 | "Runtime": "python3.12", 478 | }, 479 | "Type": "AWS::Lambda::Function", 480 | }, 481 | "FtpAuthHandlerPolicy472ABAEA": { 482 | "Properties": { 483 | "PolicyDocument": { 484 | "Statement": [ 485 | { 486 | "Action": "secretsmanager:GetSecretValue", 487 | "Effect": "Allow", 488 | "Resource": { 489 | "Fn::Join": [ 490 | "", 491 | [ 492 | "arn:", 493 | { 494 | "Ref": "AWS::Partition", 495 | }, 496 | ":secretsmanager:", 497 | { 498 | "Ref": "AWS::Region", 499 | }, 500 | ":", 501 | { 502 | "Ref": "AWS::AccountId", 503 | }, 504 | ":secret:ftpSecret/", 505 | { 506 | "Fn::GetAtt": [ 507 | "FtpServerCFDFB6F8", 508 | "ServerId", 509 | ], 510 | }, 511 | "/*", 512 | ], 513 | ], 514 | }, 515 | }, 516 | ], 517 | "Version": "2012-10-17", 518 | }, 519 | "PolicyName": "FtpAuthHandlerPolicy472ABAEA", 520 | "Roles": [ 521 | { 522 | "Ref": "FtpAuthHandlerServiceRole936451CF", 523 | }, 524 | ], 525 | }, 526 | "Type": "AWS::IAM::Policy", 527 | }, 528 | "FtpAuthHandlerServiceRole936451CF": { 529 | "Properties": { 530 | "AssumeRolePolicyDocument": { 531 | "Statement": [ 532 | { 533 | "Action": "sts:AssumeRole", 534 | "Effect": "Allow", 535 | "Principal": { 536 | "Service": "lambda.amazonaws.com", 537 | }, 538 | }, 539 | ], 540 | "Version": "2012-10-17", 541 | }, 542 | "ManagedPolicyArns": [ 543 | { 544 | "Fn::Join": [ 545 | "", 546 | [ 547 | "arn:", 548 | { 549 | "Ref": "AWS::Partition", 550 | }, 551 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 552 | ], 553 | ], 554 | }, 555 | ], 556 | }, 557 | "Type": "AWS::IAM::Role", 558 | }, 559 | "FtpAuthenticationRole97A193FA": { 560 | "Properties": { 561 | "AssumeRolePolicyDocument": { 562 | "Statement": [ 563 | { 564 | "Action": "sts:AssumeRole", 565 | "Effect": "Allow", 566 | "Principal": { 567 | "Service": "transfer.amazonaws.com", 568 | }, 569 | }, 570 | ], 571 | "Version": "2012-10-17", 572 | }, 573 | }, 574 | "Type": "AWS::IAM::Role", 575 | }, 576 | "FtpAuthenticationRoleDefaultPolicyAE5A3DE9": { 577 | "Properties": { 578 | "PolicyDocument": { 579 | "Statement": [ 580 | { 581 | "Action": "execute-api:Invoke", 582 | "Effect": "Allow", 583 | "Resource": { 584 | "Fn::Join": [ 585 | "", 586 | [ 587 | "arn:", 588 | { 589 | "Ref": "AWS::Partition", 590 | }, 591 | ":execute-api:", 592 | { 593 | "Ref": "AWS::Region", 594 | }, 595 | ":", 596 | { 597 | "Ref": "AWS::AccountId", 598 | }, 599 | ":", 600 | { 601 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 602 | }, 603 | "/*/*/*", 604 | ], 605 | ], 606 | }, 607 | }, 608 | ], 609 | "Version": "2012-10-17", 610 | }, 611 | "PolicyName": "FtpAuthenticationRoleDefaultPolicyAE5A3DE9", 612 | "Roles": [ 613 | { 614 | "Ref": "FtpAuthenticationRole97A193FA", 615 | }, 616 | ], 617 | }, 618 | "Type": "AWS::IAM::Policy", 619 | }, 620 | "FtpLoggingRole98926542": { 621 | "Properties": { 622 | "AssumeRolePolicyDocument": { 623 | "Statement": [ 624 | { 625 | "Action": "sts:AssumeRole", 626 | "Effect": "Allow", 627 | "Principal": { 628 | "Service": "transfer.amazonaws.com", 629 | }, 630 | }, 631 | ], 632 | "Version": "2012-10-17", 633 | }, 634 | "ManagedPolicyArns": [ 635 | { 636 | "Fn::Join": [ 637 | "", 638 | [ 639 | "arn:", 640 | { 641 | "Ref": "AWS::Partition", 642 | }, 643 | ":iam::aws:policy/service-role/AWSTransferLoggingAccess", 644 | ], 645 | ], 646 | }, 647 | ], 648 | }, 649 | "Type": "AWS::IAM::Role", 650 | }, 651 | "FtpSecurityGroupB05910F8": { 652 | "Properties": { 653 | "GroupDescription": "MyTestStack/Ftp/SecurityGroup", 654 | "SecurityGroupEgress": [ 655 | { 656 | "CidrIp": "0.0.0.0/0", 657 | "Description": "Allow all outbound traffic by default", 658 | "IpProtocol": "-1", 659 | }, 660 | ], 661 | "SecurityGroupIngress": [ 662 | { 663 | "CidrIp": { 664 | "Fn::GetAtt": [ 665 | "Vpc8378EB38", 666 | "CidrBlock", 667 | ], 668 | }, 669 | "Description": { 670 | "Fn::Join": [ 671 | "", 672 | [ 673 | "from ", 674 | { 675 | "Fn::GetAtt": [ 676 | "Vpc8378EB38", 677 | "CidrBlock", 678 | ], 679 | }, 680 | ":22", 681 | ], 682 | ], 683 | }, 684 | "FromPort": 22, 685 | "IpProtocol": "tcp", 686 | "ToPort": 22, 687 | }, 688 | ], 689 | "VpcId": { 690 | "Ref": "Vpc8378EB38", 691 | }, 692 | }, 693 | "Type": "AWS::EC2::SecurityGroup", 694 | }, 695 | "FtpServerCFDFB6F8": { 696 | "Properties": { 697 | "Domain": "S3", 698 | "EndpointDetails": { 699 | "SecurityGroupIds": [ 700 | { 701 | "Fn::GetAtt": [ 702 | "FtpSecurityGroupB05910F8", 703 | "GroupId", 704 | ], 705 | }, 706 | ], 707 | "SubnetIds": [ 708 | { 709 | "Ref": "VpcPrivateSubnet1Subnet536B997A", 710 | }, 711 | { 712 | "Ref": "VpcPrivateSubnet2Subnet3788AAA1", 713 | }, 714 | ], 715 | "VpcId": { 716 | "Ref": "Vpc8378EB38", 717 | }, 718 | }, 719 | "EndpointType": "VPC", 720 | "IdentityProviderDetails": { 721 | "InvocationRole": { 722 | "Fn::GetAtt": [ 723 | "FtpAuthenticationRole97A193FA", 724 | "Arn", 725 | ], 726 | }, 727 | "Url": { 728 | "Fn::Join": [ 729 | "", 730 | [ 731 | "https://", 732 | { 733 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 734 | }, 735 | ".execute-api.", 736 | { 737 | "Ref": "AWS::Region", 738 | }, 739 | ".", 740 | { 741 | "Ref": "AWS::URLSuffix", 742 | }, 743 | "/", 744 | { 745 | "Ref": "FtpTransferServiceAuthApiDeploymentStageprodD1F90C1F", 746 | }, 747 | "/", 748 | ], 749 | ], 750 | }, 751 | }, 752 | "IdentityProviderType": "API_GATEWAY", 753 | "LoggingRole": { 754 | "Fn::GetAtt": [ 755 | "FtpLoggingRole98926542", 756 | "Arn", 757 | ], 758 | }, 759 | "Protocols": [ 760 | "SFTP", 761 | ], 762 | }, 763 | "Type": "AWS::Transfer::Server", 764 | }, 765 | "FtpTransferServiceAuthApi4837DBB2": { 766 | "Properties": { 767 | "Name": "TransferServiceAuthApi", 768 | }, 769 | "Type": "AWS::ApiGateway::RestApi", 770 | }, 771 | "FtpTransferServiceAuthApiAccount0D5D1357": { 772 | "DeletionPolicy": "Retain", 773 | "DependsOn": [ 774 | "FtpTransferServiceAuthApi4837DBB2", 775 | ], 776 | "Properties": { 777 | "CloudWatchRoleArn": { 778 | "Fn::GetAtt": [ 779 | "FtpTransferServiceAuthApiCloudWatchRoleC5A655F1", 780 | "Arn", 781 | ], 782 | }, 783 | }, 784 | "Type": "AWS::ApiGateway::Account", 785 | "UpdateReplacePolicy": "Retain", 786 | }, 787 | "FtpTransferServiceAuthApiCloudWatchRoleC5A655F1": { 788 | "DeletionPolicy": "Retain", 789 | "Properties": { 790 | "AssumeRolePolicyDocument": { 791 | "Statement": [ 792 | { 793 | "Action": "sts:AssumeRole", 794 | "Effect": "Allow", 795 | "Principal": { 796 | "Service": "apigateway.amazonaws.com", 797 | }, 798 | }, 799 | ], 800 | "Version": "2012-10-17", 801 | }, 802 | "ManagedPolicyArns": [ 803 | { 804 | "Fn::Join": [ 805 | "", 806 | [ 807 | "arn:", 808 | { 809 | "Ref": "AWS::Partition", 810 | }, 811 | ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", 812 | ], 813 | ], 814 | }, 815 | ], 816 | }, 817 | "Type": "AWS::IAM::Role", 818 | "UpdateReplacePolicy": "Retain", 819 | }, 820 | "FtpTransferServiceAuthApiDeployment080F98CD4add796e6e918f16607ae1e96ca530a7": { 821 | "DependsOn": [ 822 | "FtpTransferServiceAuthApiserversserverId81EB42BA", 823 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfigGET3F86580F", 824 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfig25060533", 825 | "FtpTransferServiceAuthApiserversserverIdusersusername3D9E66E3", 826 | "FtpTransferServiceAuthApiserversserverIdusers3F549AC8", 827 | "FtpTransferServiceAuthApiserversC237EBAF", 828 | ], 829 | "Properties": { 830 | "Description": "Automatically created by the RestApi construct", 831 | "RestApiId": { 832 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 833 | }, 834 | }, 835 | "Type": "AWS::ApiGateway::Deployment", 836 | }, 837 | "FtpTransferServiceAuthApiDeploymentStageprodD1F90C1F": { 838 | "DependsOn": [ 839 | "FtpTransferServiceAuthApiAccount0D5D1357", 840 | ], 841 | "Properties": { 842 | "AccessLogSetting": { 843 | "DestinationArn": { 844 | "Fn::GetAtt": [ 845 | "FtpTransferServiceAuthApiLogsCEBBCB61", 846 | "Arn", 847 | ], 848 | }, 849 | "Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}", 850 | }, 851 | "DeploymentId": { 852 | "Ref": "FtpTransferServiceAuthApiDeployment080F98CD4add796e6e918f16607ae1e96ca530a7", 853 | }, 854 | "RestApiId": { 855 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 856 | }, 857 | "StageName": "prod", 858 | }, 859 | "Type": "AWS::ApiGateway::Stage", 860 | }, 861 | "FtpTransferServiceAuthApiLogsCEBBCB61": { 862 | "DeletionPolicy": "Retain", 863 | "Properties": { 864 | "RetentionInDays": 731, 865 | }, 866 | "Type": "AWS::Logs::LogGroup", 867 | "UpdateReplacePolicy": "Retain", 868 | }, 869 | "FtpTransferServiceAuthApiserversC237EBAF": { 870 | "Properties": { 871 | "ParentId": { 872 | "Fn::GetAtt": [ 873 | "FtpTransferServiceAuthApi4837DBB2", 874 | "RootResourceId", 875 | ], 876 | }, 877 | "PathPart": "servers", 878 | "RestApiId": { 879 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 880 | }, 881 | }, 882 | "Type": "AWS::ApiGateway::Resource", 883 | }, 884 | "FtpTransferServiceAuthApiserversserverId81EB42BA": { 885 | "Properties": { 886 | "ParentId": { 887 | "Ref": "FtpTransferServiceAuthApiserversC237EBAF", 888 | }, 889 | "PathPart": "{serverId}", 890 | "RestApiId": { 891 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 892 | }, 893 | }, 894 | "Type": "AWS::ApiGateway::Resource", 895 | }, 896 | "FtpTransferServiceAuthApiserversserverIdusers3F549AC8": { 897 | "Properties": { 898 | "ParentId": { 899 | "Ref": "FtpTransferServiceAuthApiserversserverId81EB42BA", 900 | }, 901 | "PathPart": "users", 902 | "RestApiId": { 903 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 904 | }, 905 | }, 906 | "Type": "AWS::ApiGateway::Resource", 907 | }, 908 | "FtpTransferServiceAuthApiserversserverIdusersusername3D9E66E3": { 909 | "Properties": { 910 | "ParentId": { 911 | "Ref": "FtpTransferServiceAuthApiserversserverIdusers3F549AC8", 912 | }, 913 | "PathPart": "{username}", 914 | "RestApiId": { 915 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 916 | }, 917 | }, 918 | "Type": "AWS::ApiGateway::Resource", 919 | }, 920 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfig25060533": { 921 | "Properties": { 922 | "ParentId": { 923 | "Ref": "FtpTransferServiceAuthApiserversserverIdusersusername3D9E66E3", 924 | }, 925 | "PathPart": "config", 926 | "RestApiId": { 927 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 928 | }, 929 | }, 930 | "Type": "AWS::ApiGateway::Resource", 931 | }, 932 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfigGET3F86580F": { 933 | "Properties": { 934 | "AuthorizationType": "AWS_IAM", 935 | "HttpMethod": "GET", 936 | "Integration": { 937 | "IntegrationHttpMethod": "POST", 938 | "Type": "AWS_PROXY", 939 | "Uri": { 940 | "Fn::Join": [ 941 | "", 942 | [ 943 | "arn:", 944 | { 945 | "Ref": "AWS::Partition", 946 | }, 947 | ":apigateway:", 948 | { 949 | "Ref": "AWS::Region", 950 | }, 951 | ":lambda:path/2015-03-31/functions/", 952 | { 953 | "Fn::GetAtt": [ 954 | "FtpAuthHandler13566C97", 955 | "Arn", 956 | ], 957 | }, 958 | "/invocations", 959 | ], 960 | ], 961 | }, 962 | }, 963 | "ResourceId": { 964 | "Ref": "FtpTransferServiceAuthApiserversserverIdusersusernameconfig25060533", 965 | }, 966 | "RestApiId": { 967 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 968 | }, 969 | }, 970 | "Type": "AWS::ApiGateway::Method", 971 | }, 972 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfigGETApiPermissionMyTestStackFtpTransferServiceAuthApiAF442CCAGETserversserverIdusersusernameconfigDE647C50": { 973 | "Properties": { 974 | "Action": "lambda:InvokeFunction", 975 | "FunctionName": { 976 | "Fn::GetAtt": [ 977 | "FtpAuthHandler13566C97", 978 | "Arn", 979 | ], 980 | }, 981 | "Principal": "apigateway.amazonaws.com", 982 | "SourceArn": { 983 | "Fn::Join": [ 984 | "", 985 | [ 986 | "arn:", 987 | { 988 | "Ref": "AWS::Partition", 989 | }, 990 | ":execute-api:", 991 | { 992 | "Ref": "AWS::Region", 993 | }, 994 | ":", 995 | { 996 | "Ref": "AWS::AccountId", 997 | }, 998 | ":", 999 | { 1000 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 1001 | }, 1002 | "/", 1003 | { 1004 | "Ref": "FtpTransferServiceAuthApiDeploymentStageprodD1F90C1F", 1005 | }, 1006 | "/GET/servers/*/users/*/config", 1007 | ], 1008 | ], 1009 | }, 1010 | }, 1011 | "Type": "AWS::Lambda::Permission", 1012 | }, 1013 | "FtpTransferServiceAuthApiserversserverIdusersusernameconfigGETApiPermissionTestMyTestStackFtpTransferServiceAuthApiAF442CCAGETserversserverIdusersusernameconfig87BE1F93": { 1014 | "Properties": { 1015 | "Action": "lambda:InvokeFunction", 1016 | "FunctionName": { 1017 | "Fn::GetAtt": [ 1018 | "FtpAuthHandler13566C97", 1019 | "Arn", 1020 | ], 1021 | }, 1022 | "Principal": "apigateway.amazonaws.com", 1023 | "SourceArn": { 1024 | "Fn::Join": [ 1025 | "", 1026 | [ 1027 | "arn:", 1028 | { 1029 | "Ref": "AWS::Partition", 1030 | }, 1031 | ":execute-api:", 1032 | { 1033 | "Ref": "AWS::Region", 1034 | }, 1035 | ":", 1036 | { 1037 | "Ref": "AWS::AccountId", 1038 | }, 1039 | ":", 1040 | { 1041 | "Ref": "FtpTransferServiceAuthApi4837DBB2", 1042 | }, 1043 | "/test-invoke-stage/GET/servers/*/users/*/config", 1044 | ], 1045 | ], 1046 | }, 1047 | }, 1048 | "Type": "AWS::Lambda::Permission", 1049 | }, 1050 | "User1Role60FF65D3": { 1051 | "Properties": { 1052 | "AssumeRolePolicyDocument": { 1053 | "Statement": [ 1054 | { 1055 | "Action": "sts:AssumeRole", 1056 | "Effect": "Allow", 1057 | "Principal": { 1058 | "Service": "transfer.amazonaws.com", 1059 | }, 1060 | }, 1061 | ], 1062 | "Version": "2012-10-17", 1063 | }, 1064 | }, 1065 | "Type": "AWS::IAM::Role", 1066 | }, 1067 | "User1RoleDefaultPolicy2B8CC8B6": { 1068 | "Properties": { 1069 | "PolicyDocument": { 1070 | "Statement": [ 1071 | { 1072 | "Action": [ 1073 | "s3:GetObject*", 1074 | "s3:GetBucket*", 1075 | "s3:List*", 1076 | "s3:DeleteObject*", 1077 | "s3:PutObject", 1078 | "s3:PutObjectLegalHold", 1079 | "s3:PutObjectRetention", 1080 | "s3:PutObjectTagging", 1081 | "s3:PutObjectVersionTagging", 1082 | "s3:Abort*", 1083 | ], 1084 | "Effect": "Allow", 1085 | "Resource": [ 1086 | { 1087 | "Fn::GetAtt": [ 1088 | "Bucket83908E77", 1089 | "Arn", 1090 | ], 1091 | }, 1092 | { 1093 | "Fn::Join": [ 1094 | "", 1095 | [ 1096 | { 1097 | "Fn::GetAtt": [ 1098 | "Bucket83908E77", 1099 | "Arn", 1100 | ], 1101 | }, 1102 | "/home", 1103 | ], 1104 | ], 1105 | }, 1106 | ], 1107 | }, 1108 | { 1109 | "Action": [ 1110 | "s3:GetObject*", 1111 | "s3:GetBucket*", 1112 | "s3:List*", 1113 | "s3:DeleteObject*", 1114 | "s3:PutObject", 1115 | "s3:PutObjectLegalHold", 1116 | "s3:PutObjectRetention", 1117 | "s3:PutObjectTagging", 1118 | "s3:PutObjectVersionTagging", 1119 | "s3:Abort*", 1120 | ], 1121 | "Effect": "Allow", 1122 | "Resource": [ 1123 | { 1124 | "Fn::GetAtt": [ 1125 | "Bucket83908E77", 1126 | "Arn", 1127 | ], 1128 | }, 1129 | { 1130 | "Fn::Join": [ 1131 | "", 1132 | [ 1133 | { 1134 | "Fn::GetAtt": [ 1135 | "Bucket83908E77", 1136 | "Arn", 1137 | ], 1138 | }, 1139 | "/home/*", 1140 | ], 1141 | ], 1142 | }, 1143 | ], 1144 | }, 1145 | ], 1146 | "Version": "2012-10-17", 1147 | }, 1148 | "PolicyName": "User1RoleDefaultPolicy2B8CC8B6", 1149 | "Roles": [ 1150 | { 1151 | "Ref": "User1Role60FF65D3", 1152 | }, 1153 | ], 1154 | }, 1155 | "Type": "AWS::IAM::Policy", 1156 | }, 1157 | "User1UserFF437088": { 1158 | "DeletionPolicy": "Delete", 1159 | "Properties": { 1160 | "GenerateSecretString": { 1161 | "ExcludePunctuation": true, 1162 | "GenerateStringKey": "Password", 1163 | "SecretStringTemplate": { 1164 | "Fn::Join": [ 1165 | "", 1166 | [ 1167 | "{"Role":"", 1168 | { 1169 | "Fn::GetAtt": [ 1170 | "User1Role60FF65D3", 1171 | "Arn", 1172 | ], 1173 | }, 1174 | "","HomeDirectoryDetails":"[{\\"Entry\\":\\"/\\",\\"Target\\":\\"/", 1175 | { 1176 | "Ref": "Bucket83908E77", 1177 | }, 1178 | "/home\\"}]"}", 1179 | ], 1180 | ], 1181 | }, 1182 | }, 1183 | "Name": { 1184 | "Fn::Join": [ 1185 | "", 1186 | [ 1187 | "ftpSecret/", 1188 | { 1189 | "Fn::GetAtt": [ 1190 | "FtpServerCFDFB6F8", 1191 | "ServerId", 1192 | ], 1193 | }, 1194 | "/user1", 1195 | ], 1196 | ], 1197 | }, 1198 | }, 1199 | "Type": "AWS::SecretsManager::Secret", 1200 | "UpdateReplacePolicy": "Delete", 1201 | }, 1202 | "User2RoleDefaultPolicy59F43806": { 1203 | "Properties": { 1204 | "PolicyDocument": { 1205 | "Statement": [ 1206 | { 1207 | "Action": [ 1208 | "s3:GetObject*", 1209 | "s3:GetBucket*", 1210 | "s3:List*", 1211 | "s3:DeleteObject*", 1212 | "s3:PutObject", 1213 | "s3:PutObjectLegalHold", 1214 | "s3:PutObjectRetention", 1215 | "s3:PutObjectTagging", 1216 | "s3:PutObjectVersionTagging", 1217 | "s3:Abort*", 1218 | ], 1219 | "Effect": "Allow", 1220 | "Resource": [ 1221 | { 1222 | "Fn::GetAtt": [ 1223 | "Bucket83908E77", 1224 | "Arn", 1225 | ], 1226 | }, 1227 | { 1228 | "Fn::Join": [ 1229 | "", 1230 | [ 1231 | { 1232 | "Fn::GetAtt": [ 1233 | "Bucket83908E77", 1234 | "Arn", 1235 | ], 1236 | }, 1237 | "/home", 1238 | ], 1239 | ], 1240 | }, 1241 | ], 1242 | }, 1243 | { 1244 | "Action": [ 1245 | "s3:GetObject*", 1246 | "s3:GetBucket*", 1247 | "s3:List*", 1248 | "s3:DeleteObject*", 1249 | "s3:PutObject", 1250 | "s3:PutObjectLegalHold", 1251 | "s3:PutObjectRetention", 1252 | "s3:PutObjectTagging", 1253 | "s3:PutObjectVersionTagging", 1254 | "s3:Abort*", 1255 | ], 1256 | "Effect": "Allow", 1257 | "Resource": [ 1258 | { 1259 | "Fn::GetAtt": [ 1260 | "Bucket83908E77", 1261 | "Arn", 1262 | ], 1263 | }, 1264 | { 1265 | "Fn::Join": [ 1266 | "", 1267 | [ 1268 | { 1269 | "Fn::GetAtt": [ 1270 | "Bucket83908E77", 1271 | "Arn", 1272 | ], 1273 | }, 1274 | "/home/*", 1275 | ], 1276 | ], 1277 | }, 1278 | ], 1279 | }, 1280 | ], 1281 | "Version": "2012-10-17", 1282 | }, 1283 | "PolicyName": "User2RoleDefaultPolicy59F43806", 1284 | "Roles": [ 1285 | { 1286 | "Ref": "User2RoleF7116B36", 1287 | }, 1288 | ], 1289 | }, 1290 | "Type": "AWS::IAM::Policy", 1291 | }, 1292 | "User2RoleF7116B36": { 1293 | "Properties": { 1294 | "AssumeRolePolicyDocument": { 1295 | "Statement": [ 1296 | { 1297 | "Action": "sts:AssumeRole", 1298 | "Effect": "Allow", 1299 | "Principal": { 1300 | "Service": "transfer.amazonaws.com", 1301 | }, 1302 | }, 1303 | ], 1304 | "Version": "2012-10-17", 1305 | }, 1306 | }, 1307 | "Type": "AWS::IAM::Role", 1308 | }, 1309 | "User2User0C6B37B0": { 1310 | "DeletionPolicy": "Delete", 1311 | "Properties": { 1312 | "GenerateSecretString": { 1313 | "ExcludePunctuation": true, 1314 | "GenerateStringKey": "Dummy", 1315 | "SecretStringTemplate": { 1316 | "Fn::Join": [ 1317 | "", 1318 | [ 1319 | "{"Role":"", 1320 | { 1321 | "Fn::GetAtt": [ 1322 | "User2RoleF7116B36", 1323 | "Arn", 1324 | ], 1325 | }, 1326 | "","HomeDirectoryDetails":"[{\\"Entry\\":\\"/\\",\\"Target\\":\\"/", 1327 | { 1328 | "Ref": "Bucket83908E77", 1329 | }, 1330 | "/home\\"}]","Password":"password"}", 1331 | ], 1332 | ], 1333 | }, 1334 | }, 1335 | "Name": { 1336 | "Fn::Join": [ 1337 | "", 1338 | [ 1339 | "ftpSecret/", 1340 | { 1341 | "Fn::GetAtt": [ 1342 | "FtpServerCFDFB6F8", 1343 | "ServerId", 1344 | ], 1345 | }, 1346 | "/user2", 1347 | ], 1348 | ], 1349 | }, 1350 | }, 1351 | "Type": "AWS::SecretsManager::Secret", 1352 | "UpdateReplacePolicy": "Delete", 1353 | }, 1354 | "Vpc8378EB38": { 1355 | "Properties": { 1356 | "CidrBlock": "10.0.0.0/16", 1357 | "EnableDnsHostnames": true, 1358 | "EnableDnsSupport": true, 1359 | "InstanceTenancy": "default", 1360 | "Tags": [ 1361 | { 1362 | "Key": "Name", 1363 | "Value": "MyTestStack/Vpc", 1364 | }, 1365 | ], 1366 | }, 1367 | "Type": "AWS::EC2::VPC", 1368 | }, 1369 | "VpcIGWD7BA715C": { 1370 | "Properties": { 1371 | "Tags": [ 1372 | { 1373 | "Key": "Name", 1374 | "Value": "MyTestStack/Vpc", 1375 | }, 1376 | ], 1377 | }, 1378 | "Type": "AWS::EC2::InternetGateway", 1379 | }, 1380 | "VpcPrivateSubnet1DefaultRouteBE02A9ED": { 1381 | "Properties": { 1382 | "DestinationCidrBlock": "0.0.0.0/0", 1383 | "NatGatewayId": { 1384 | "Ref": "VpcPublicSubnet1NATGateway4D7517AA", 1385 | }, 1386 | "RouteTableId": { 1387 | "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", 1388 | }, 1389 | }, 1390 | "Type": "AWS::EC2::Route", 1391 | }, 1392 | "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { 1393 | "Properties": { 1394 | "RouteTableId": { 1395 | "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", 1396 | }, 1397 | "SubnetId": { 1398 | "Ref": "VpcPrivateSubnet1Subnet536B997A", 1399 | }, 1400 | }, 1401 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 1402 | }, 1403 | "VpcPrivateSubnet1RouteTableB2C5B500": { 1404 | "Properties": { 1405 | "Tags": [ 1406 | { 1407 | "Key": "Name", 1408 | "Value": "MyTestStack/Vpc/PrivateSubnet1", 1409 | }, 1410 | ], 1411 | "VpcId": { 1412 | "Ref": "Vpc8378EB38", 1413 | }, 1414 | }, 1415 | "Type": "AWS::EC2::RouteTable", 1416 | }, 1417 | "VpcPrivateSubnet1Subnet536B997A": { 1418 | "Properties": { 1419 | "AvailabilityZone": { 1420 | "Fn::Select": [ 1421 | 0, 1422 | { 1423 | "Fn::GetAZs": "", 1424 | }, 1425 | ], 1426 | }, 1427 | "CidrBlock": "10.0.128.0/18", 1428 | "MapPublicIpOnLaunch": false, 1429 | "Tags": [ 1430 | { 1431 | "Key": "aws-cdk:subnet-name", 1432 | "Value": "Private", 1433 | }, 1434 | { 1435 | "Key": "aws-cdk:subnet-type", 1436 | "Value": "Private", 1437 | }, 1438 | { 1439 | "Key": "Name", 1440 | "Value": "MyTestStack/Vpc/PrivateSubnet1", 1441 | }, 1442 | ], 1443 | "VpcId": { 1444 | "Ref": "Vpc8378EB38", 1445 | }, 1446 | }, 1447 | "Type": "AWS::EC2::Subnet", 1448 | }, 1449 | "VpcPrivateSubnet2DefaultRoute060D2087": { 1450 | "Properties": { 1451 | "DestinationCidrBlock": "0.0.0.0/0", 1452 | "NatGatewayId": { 1453 | "Ref": "VpcPublicSubnet1NATGateway4D7517AA", 1454 | }, 1455 | "RouteTableId": { 1456 | "Ref": "VpcPrivateSubnet2RouteTableA678073B", 1457 | }, 1458 | }, 1459 | "Type": "AWS::EC2::Route", 1460 | }, 1461 | "VpcPrivateSubnet2RouteTableA678073B": { 1462 | "Properties": { 1463 | "Tags": [ 1464 | { 1465 | "Key": "Name", 1466 | "Value": "MyTestStack/Vpc/PrivateSubnet2", 1467 | }, 1468 | ], 1469 | "VpcId": { 1470 | "Ref": "Vpc8378EB38", 1471 | }, 1472 | }, 1473 | "Type": "AWS::EC2::RouteTable", 1474 | }, 1475 | "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { 1476 | "Properties": { 1477 | "RouteTableId": { 1478 | "Ref": "VpcPrivateSubnet2RouteTableA678073B", 1479 | }, 1480 | "SubnetId": { 1481 | "Ref": "VpcPrivateSubnet2Subnet3788AAA1", 1482 | }, 1483 | }, 1484 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 1485 | }, 1486 | "VpcPrivateSubnet2Subnet3788AAA1": { 1487 | "Properties": { 1488 | "AvailabilityZone": { 1489 | "Fn::Select": [ 1490 | 1, 1491 | { 1492 | "Fn::GetAZs": "", 1493 | }, 1494 | ], 1495 | }, 1496 | "CidrBlock": "10.0.192.0/18", 1497 | "MapPublicIpOnLaunch": false, 1498 | "Tags": [ 1499 | { 1500 | "Key": "aws-cdk:subnet-name", 1501 | "Value": "Private", 1502 | }, 1503 | { 1504 | "Key": "aws-cdk:subnet-type", 1505 | "Value": "Private", 1506 | }, 1507 | { 1508 | "Key": "Name", 1509 | "Value": "MyTestStack/Vpc/PrivateSubnet2", 1510 | }, 1511 | ], 1512 | "VpcId": { 1513 | "Ref": "Vpc8378EB38", 1514 | }, 1515 | }, 1516 | "Type": "AWS::EC2::Subnet", 1517 | }, 1518 | "VpcPublicSubnet1DefaultRoute3DA9E72A": { 1519 | "DependsOn": [ 1520 | "VpcVPCGWBF912B6E", 1521 | ], 1522 | "Properties": { 1523 | "DestinationCidrBlock": "0.0.0.0/0", 1524 | "GatewayId": { 1525 | "Ref": "VpcIGWD7BA715C", 1526 | }, 1527 | "RouteTableId": { 1528 | "Ref": "VpcPublicSubnet1RouteTable6C95E38E", 1529 | }, 1530 | }, 1531 | "Type": "AWS::EC2::Route", 1532 | }, 1533 | "VpcPublicSubnet1EIPD7E02669": { 1534 | "Properties": { 1535 | "Domain": "vpc", 1536 | "Tags": [ 1537 | { 1538 | "Key": "Name", 1539 | "Value": "MyTestStack/Vpc/PublicSubnet1", 1540 | }, 1541 | ], 1542 | }, 1543 | "Type": "AWS::EC2::EIP", 1544 | }, 1545 | "VpcPublicSubnet1NATGateway4D7517AA": { 1546 | "DependsOn": [ 1547 | "VpcPublicSubnet1DefaultRoute3DA9E72A", 1548 | "VpcPublicSubnet1RouteTableAssociation97140677", 1549 | ], 1550 | "Properties": { 1551 | "AllocationId": { 1552 | "Fn::GetAtt": [ 1553 | "VpcPublicSubnet1EIPD7E02669", 1554 | "AllocationId", 1555 | ], 1556 | }, 1557 | "SubnetId": { 1558 | "Ref": "VpcPublicSubnet1Subnet5C2D37C4", 1559 | }, 1560 | "Tags": [ 1561 | { 1562 | "Key": "Name", 1563 | "Value": "MyTestStack/Vpc/PublicSubnet1", 1564 | }, 1565 | ], 1566 | }, 1567 | "Type": "AWS::EC2::NatGateway", 1568 | }, 1569 | "VpcPublicSubnet1RouteTable6C95E38E": { 1570 | "Properties": { 1571 | "Tags": [ 1572 | { 1573 | "Key": "Name", 1574 | "Value": "MyTestStack/Vpc/PublicSubnet1", 1575 | }, 1576 | ], 1577 | "VpcId": { 1578 | "Ref": "Vpc8378EB38", 1579 | }, 1580 | }, 1581 | "Type": "AWS::EC2::RouteTable", 1582 | }, 1583 | "VpcPublicSubnet1RouteTableAssociation97140677": { 1584 | "Properties": { 1585 | "RouteTableId": { 1586 | "Ref": "VpcPublicSubnet1RouteTable6C95E38E", 1587 | }, 1588 | "SubnetId": { 1589 | "Ref": "VpcPublicSubnet1Subnet5C2D37C4", 1590 | }, 1591 | }, 1592 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 1593 | }, 1594 | "VpcPublicSubnet1Subnet5C2D37C4": { 1595 | "Properties": { 1596 | "AvailabilityZone": { 1597 | "Fn::Select": [ 1598 | 0, 1599 | { 1600 | "Fn::GetAZs": "", 1601 | }, 1602 | ], 1603 | }, 1604 | "CidrBlock": "10.0.0.0/18", 1605 | "MapPublicIpOnLaunch": true, 1606 | "Tags": [ 1607 | { 1608 | "Key": "aws-cdk:subnet-name", 1609 | "Value": "Public", 1610 | }, 1611 | { 1612 | "Key": "aws-cdk:subnet-type", 1613 | "Value": "Public", 1614 | }, 1615 | { 1616 | "Key": "Name", 1617 | "Value": "MyTestStack/Vpc/PublicSubnet1", 1618 | }, 1619 | ], 1620 | "VpcId": { 1621 | "Ref": "Vpc8378EB38", 1622 | }, 1623 | }, 1624 | "Type": "AWS::EC2::Subnet", 1625 | }, 1626 | "VpcPublicSubnet2DefaultRoute97F91067": { 1627 | "DependsOn": [ 1628 | "VpcVPCGWBF912B6E", 1629 | ], 1630 | "Properties": { 1631 | "DestinationCidrBlock": "0.0.0.0/0", 1632 | "GatewayId": { 1633 | "Ref": "VpcIGWD7BA715C", 1634 | }, 1635 | "RouteTableId": { 1636 | "Ref": "VpcPublicSubnet2RouteTable94F7E489", 1637 | }, 1638 | }, 1639 | "Type": "AWS::EC2::Route", 1640 | }, 1641 | "VpcPublicSubnet2RouteTable94F7E489": { 1642 | "Properties": { 1643 | "Tags": [ 1644 | { 1645 | "Key": "Name", 1646 | "Value": "MyTestStack/Vpc/PublicSubnet2", 1647 | }, 1648 | ], 1649 | "VpcId": { 1650 | "Ref": "Vpc8378EB38", 1651 | }, 1652 | }, 1653 | "Type": "AWS::EC2::RouteTable", 1654 | }, 1655 | "VpcPublicSubnet2RouteTableAssociationDD5762D8": { 1656 | "Properties": { 1657 | "RouteTableId": { 1658 | "Ref": "VpcPublicSubnet2RouteTable94F7E489", 1659 | }, 1660 | "SubnetId": { 1661 | "Ref": "VpcPublicSubnet2Subnet691E08A3", 1662 | }, 1663 | }, 1664 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 1665 | }, 1666 | "VpcPublicSubnet2Subnet691E08A3": { 1667 | "Properties": { 1668 | "AvailabilityZone": { 1669 | "Fn::Select": [ 1670 | 1, 1671 | { 1672 | "Fn::GetAZs": "", 1673 | }, 1674 | ], 1675 | }, 1676 | "CidrBlock": "10.0.64.0/18", 1677 | "MapPublicIpOnLaunch": true, 1678 | "Tags": [ 1679 | { 1680 | "Key": "aws-cdk:subnet-name", 1681 | "Value": "Public", 1682 | }, 1683 | { 1684 | "Key": "aws-cdk:subnet-type", 1685 | "Value": "Public", 1686 | }, 1687 | { 1688 | "Key": "Name", 1689 | "Value": "MyTestStack/Vpc/PublicSubnet2", 1690 | }, 1691 | ], 1692 | "VpcId": { 1693 | "Ref": "Vpc8378EB38", 1694 | }, 1695 | }, 1696 | "Type": "AWS::EC2::Subnet", 1697 | }, 1698 | "VpcVPCGWBF912B6E": { 1699 | "Properties": { 1700 | "InternetGatewayId": { 1701 | "Ref": "VpcIGWD7BA715C", 1702 | }, 1703 | "VpcId": { 1704 | "Ref": "Vpc8378EB38", 1705 | }, 1706 | }, 1707 | "Type": "AWS::EC2::VPCGatewayAttachment", 1708 | }, 1709 | }, 1710 | "Rules": { 1711 | "CheckBootstrapVersion": { 1712 | "Assertions": [ 1713 | { 1714 | "Assert": { 1715 | "Fn::Not": [ 1716 | { 1717 | "Fn::Contains": [ 1718 | [ 1719 | "1", 1720 | "2", 1721 | "3", 1722 | "4", 1723 | "5", 1724 | ], 1725 | { 1726 | "Ref": "BootstrapVersion", 1727 | }, 1728 | ], 1729 | }, 1730 | ], 1731 | }, 1732 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 1733 | }, 1734 | ], 1735 | }, 1736 | }, 1737 | } 1738 | `; 1739 | --------------------------------------------------------------------------------