Skip to content

Commit 0609159

Browse files
authored
Merge pull request caktus#87 from caktus/sftp
Add AWS SFTP service
2 parents d4ceb26 + 6d6f2f4 commit 0609159

File tree

4 files changed

+288
-55
lines changed

4 files changed

+288
-55
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ What's new in 2.0.0:
3535
* Add CustomAppCertificateArn parameter to allow association with an existing ACM certificate.
3636
* Add VPC Endpoint for S3.
3737
* Add DatabaseReplication parameter to add a database replica (** this will fail if DatabaseBackupRetentionDays is 0.**).
38+
* Add optional SFTP server, including S3 bucket, transfer server, and user role and scopedown policy to use when creating
39+
users in the transfer server.
3840

3941

4042
`1.4.0`_ (2019-08-05)

stack/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
from . import sftp # noqa: F401
34
from . import assets # noqa: F401
45
from . import cache # noqa: F401
56
from . import database # noqa: F401

stack/assets.py

Lines changed: 226 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
If,
99
Join,
1010
Not,
11+
NoValue,
1112
Output,
1213
Ref,
1314
Split,
@@ -42,6 +43,7 @@
4243
use_cmk_arn
4344
)
4445
from .domain import domain_name, domain_name_alternates, no_alt_domains
46+
from .sftp import use_sftp_condition, use_sftp_with_kms_condition
4547
from .template import template
4648
from .utils import ParameterWithDefaults as Parameter
4749

@@ -102,32 +104,32 @@
102104
Bucket(
103105
"AssetsBucket",
104106
AccessControl=Ref(assets_bucket_access_control),
105-
BucketEncryption=BucketEncryption(
106-
ServerSideEncryptionConfiguration=If(
107-
use_aes256_encryption_cond,
108-
[
107+
BucketEncryption=If(
108+
use_aes256_encryption_cond,
109+
BucketEncryption(
110+
ServerSideEncryptionConfiguration=[
109111
ServerSideEncryptionRule(
110112
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
111113
SSEAlgorithm='AES256'
112114
)
113115
)
114-
],
115-
[
116-
ServerSideEncryptionRule()
117116
]
118-
)
117+
),
118+
NoValue
119119
),
120120
**common_bucket_conf,
121121
)
122122
)
123123

124124

125125
# Output S3 asset bucket name
126-
template.add_output(Output(
127-
"AssetsBucketDomainName",
128-
Description="Assets bucket domain name",
129-
Value=GetAtt(assets_bucket, "DomainName")
130-
))
126+
template.add_output(
127+
Output(
128+
"AssetsBucketDomainName",
129+
Description="Assets bucket domain name",
130+
Value=GetAtt(assets_bucket, "DomainName"),
131+
)
132+
)
131133

132134

133135
# Create an S3 bucket that holds user uploads or other non-public files
@@ -141,61 +143,122 @@
141143
IgnorePublicAcls=True,
142144
RestrictPublicBuckets=True,
143145
),
144-
BucketEncryption=BucketEncryption(
145-
ServerSideEncryptionConfiguration=If(
146-
use_aes256_encryption_cond,
147-
[
146+
BucketEncryption=If(
147+
use_aes256_encryption_cond,
148+
BucketEncryption(
149+
ServerSideEncryptionConfiguration=[
148150
ServerSideEncryptionRule(
149151
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
150152
SSEAlgorithm=If(use_cmk_arn, 'aws:kms', 'AES256'),
151153
KMSMasterKeyID=If(use_cmk_arn, Ref(cmk_arn), Ref("AWS::NoValue")),
152154
)
153155
)
154-
],
155-
[
156-
ServerSideEncryptionRule()
157156
]
158-
)
157+
),
158+
NoValue
159159
),
160160
**common_bucket_conf,
161161
)
162162
)
163163

164+
# Output S3 private assets bucket name
165+
template.add_output(
166+
Output(
167+
"PrivateAssetsBucketDomainName",
168+
Description="Private assets bucket domain name",
169+
Value=GetAtt(private_assets_bucket, "DomainName"),
170+
)
171+
)
164172

165-
# Output S3 asset bucket name
166-
template.add_output(Output(
167-
"PrivateAssetsBucketDomainName",
168-
Description="Private assets bucket domain name",
169-
Value=GetAtt(private_assets_bucket, "DomainName")
170-
))
173+
# Bucket for SFTP service
174+
sftp_assets_bucket = Bucket(
175+
"SFTPAssetsBucket",
176+
Condition=use_sftp_condition,
177+
AccessControl=Private,
178+
PublicAccessBlockConfiguration=PublicAccessBlockConfiguration(
179+
BlockPublicAcls=True,
180+
BlockPublicPolicy=True,
181+
IgnorePublicAcls=True,
182+
RestrictPublicBuckets=True,
183+
),
184+
BucketEncryption=If(
185+
use_aes256_encryption_cond,
186+
BucketEncryption(
187+
ServerSideEncryptionConfiguration=[
188+
ServerSideEncryptionRule(
189+
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
190+
SSEAlgorithm=If(use_cmk_arn, "aws:kms", "AES256"),
191+
KMSMasterKeyID=If(
192+
use_cmk_arn, Ref(cmk_arn), Ref("AWS::NoValue")
193+
),
194+
)
195+
)
196+
]
197+
),
198+
NoValue,
199+
),
200+
**common_bucket_conf,
201+
)
202+
template.add_resource(sftp_assets_bucket)
203+
204+
# Output SFTP asset bucket name
205+
template.add_output(
206+
Output(
207+
"SFTPBucketDomainName",
208+
Condition=use_sftp_condition,
209+
Description="SFTP bucket domain name",
210+
Value=GetAtt(sftp_assets_bucket, "DomainName"),
211+
)
212+
)
171213

214+
assets_management_policy_statements = [
215+
dict(
216+
Effect="Allow",
217+
Action=["s3:ListBucket"],
218+
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket)]),
219+
),
220+
dict(
221+
Effect="Allow",
222+
Action=["s3:*"],
223+
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket), "/*"]),
224+
),
225+
dict(
226+
Effect="Allow",
227+
Action=["s3:ListBucket"],
228+
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket)]),
229+
),
230+
dict(
231+
Effect="Allow",
232+
Action=["s3:*"],
233+
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket), "/*"]),
234+
),
235+
]
236+
237+
assets_management_policy_statements_including_sftp_bucket = (
238+
assets_management_policy_statements
239+
+ [
240+
dict(
241+
Effect="Allow",
242+
Action=["s3:ListBucket"],
243+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket)]),
244+
),
245+
dict(
246+
Effect="Allow",
247+
Action=["s3:*"],
248+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket), "/*"]),
249+
),
250+
]
251+
)
172252

173253
# central asset management policy for use in instance roles
174254
assets_management_policy = iam.Policy(
175255
PolicyName="AssetsManagementPolicy",
176256
PolicyDocument=dict(
177-
Statement=[
178-
dict(
179-
Effect="Allow",
180-
Action=["s3:ListBucket"],
181-
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket)]),
182-
),
183-
dict(
184-
Effect="Allow",
185-
Action=["s3:*"],
186-
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket), "/*"]),
187-
),
188-
dict(
189-
Effect="Allow",
190-
Action=["s3:ListBucket"],
191-
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket)]),
192-
),
193-
dict(
194-
Effect="Allow",
195-
Action=["s3:*"],
196-
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket), "/*"]),
197-
),
198-
],
257+
Statement=If(
258+
use_sftp_condition,
259+
assets_management_policy_statements_including_sftp_bucket,
260+
assets_management_policy_statements,
261+
)
199262
),
200263
)
201264

@@ -321,11 +384,119 @@
321384
)
322385

323386
# Output CloudFront url
324-
template.add_output(Output(
325-
"AssetsDistributionDomainName",
326-
Description="The assets CDN domain name",
327-
Value=GetAtt(distribution, "DomainName"),
328-
Condition=assets_use_cloudfront_condition,
329-
))
387+
template.add_output(
388+
Output(
389+
"AssetsDistributionDomainName",
390+
Description="The assets CDN domain name",
391+
Value=GetAtt(distribution, "DomainName"),
392+
Condition=assets_use_cloudfront_condition,
393+
)
394+
)
330395
else:
331396
distribution = None
397+
398+
# The scopedown policy is used to restrict a user's access to the parts of the bucket
399+
# we don't want them to access.
400+
common_sftp_scopedown_policy_statements = [
401+
{
402+
"Sid": "AllowListingOfSFTPUserFolder",
403+
"Action": ["s3:ListBucket"],
404+
"Effect": "Allow",
405+
"Resource": ["arn:aws:s3:::${transfer:HomeBucket}"],
406+
"Condition": {
407+
"StringLike": {
408+
"s3:prefix": ["${transfer:UserName}/*", "${transfer:UserName}"]
409+
}
410+
},
411+
},
412+
{
413+
"Sid": "HomeDirObjectAccess",
414+
"Effect": "Allow",
415+
"Action": [
416+
"s3:PutObject",
417+
"s3:GetObject",
418+
"s3:DeleteObjectVersion",
419+
"s3:DeleteObject",
420+
"s3:GetObjectVersion",
421+
],
422+
"Resource": [
423+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}"]),
424+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}/*"]),
425+
],
426+
},
427+
]
428+
429+
sftp_kms_policy_statement = dict(
430+
Effect="Allow",
431+
Action=["kms:DescribeKey", "kms:GenerateDataKey", "kms:Encrypt", "kms:Decrypt"],
432+
Resource=Ref(cmk_arn),
433+
)
434+
435+
sftp_scopedown_policy = iam.ManagedPolicy(
436+
# This is for applying when adding users to the transfer server. It's not used directly in the stack creation,
437+
# other than adding it to IAM for later use.
438+
"SFTPUserScopeDownPolicy",
439+
PolicyDocument=dict(
440+
Version="2012-10-17",
441+
Statement=If(
442+
use_sftp_with_kms_condition,
443+
common_sftp_scopedown_policy_statements + [sftp_kms_policy_statement],
444+
common_sftp_scopedown_policy_statements,
445+
),
446+
),
447+
)
448+
template.add_resource(sftp_scopedown_policy)
449+
450+
# The ROLE is applied to users to let them access the bucket in general,
451+
# without regart to who they are.
452+
common_sftp_user_role_statements = [
453+
dict(
454+
Effect="Allow",
455+
Action=["s3:ListBucket", "s3:GetBucketLocation"],
456+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket)]),
457+
),
458+
dict(
459+
Effect="Allow",
460+
Action=[
461+
"s3:PutObject",
462+
"s3:GetObject",
463+
"s3:DeleteObject",
464+
"s3:DeleteObjectVersion",
465+
"s3:GetObjectVersion",
466+
"s3:GetObjectACL",
467+
"s3:PutObjectACL",
468+
],
469+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket), "/*"]),
470+
),
471+
]
472+
473+
sftp_user_role = iam.Role(
474+
# This also is not used directly during the stack setup, but is put into IAM
475+
# to be used later when adding users to the transfer server.
476+
"SFTPUserRole",
477+
template=template,
478+
AssumeRolePolicyDocument=dict(
479+
Statement=[
480+
dict(
481+
Effect="Allow",
482+
Principal=dict(Service=["transfer.amazonaws.com"]),
483+
Action=["sts:AssumeRole"],
484+
)
485+
]
486+
),
487+
Policies=[
488+
iam.Policy(
489+
"SFTPSUserRolePolicy",
490+
PolicyName="SFTPSUserRolePolicy",
491+
PolicyDocument=dict(
492+
Version="2012-10-17",
493+
Statement=If(
494+
use_sftp_with_kms_condition,
495+
common_sftp_user_role_statements + [sftp_kms_policy_statement],
496+
common_sftp_user_role_statements,
497+
),
498+
),
499+
)
500+
],
501+
RoleName=Join("-", [Ref("AWS::StackName"), "SFTPUserRole"]),
502+
)

0 commit comments

Comments
 (0)