Skip to content

Commit 4e92b64

Browse files
committed
Add optional SFTP service
1 parent 9f0cd65 commit 4e92b64

File tree

5 files changed

+289
-93
lines changed

5 files changed

+289
-93
lines changed

CHANGELOG.rst

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

3840

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

stack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from . import database # noqa: F401
66
from . import logs # noqa: F401
77
from . import vpc # noqa: F401
8-
from . import transfer # noqa: F401
8+
from . import sftp # noqa: F401
99
from . import template
1010

1111
if os.environ.get('USE_GOVCLOUD') != 'on':

stack/assets.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
And,
66
Equals,
77
GetAtt,
8+
iam,
89
If,
910
Join,
1011
Not,
12+
NoValue,
1113
Output,
1214
Ref,
1315
Split,
14-
iam
1516
)
1617
from troposphere.certificatemanager import Certificate, DomainValidationOption
1718
from troposphere.cloudfront import (
@@ -102,20 +103,18 @@
102103
Bucket(
103104
"AssetsBucket",
104105
AccessControl=Ref(assets_bucket_access_control),
105-
BucketEncryption=BucketEncryption(
106-
ServerSideEncryptionConfiguration=If(
107-
use_aes256_encryption_cond,
108-
[
106+
BucketEncryption=If(
107+
use_aes256_encryption_cond,
108+
BucketEncryption(
109+
ServerSideEncryptionConfiguration=[
109110
ServerSideEncryptionRule(
110111
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
111112
SSEAlgorithm='AES256'
112113
)
113114
)
114-
],
115-
[
116-
ServerSideEncryptionRule()
117115
]
118-
)
116+
),
117+
NoValue
119118
),
120119
**common_bucket_conf,
121120
)
@@ -141,21 +140,19 @@
141140
IgnorePublicAcls=True,
142141
RestrictPublicBuckets=True,
143142
),
144-
BucketEncryption=BucketEncryption(
145-
ServerSideEncryptionConfiguration=If(
146-
use_aes256_encryption_cond,
147-
[
143+
BucketEncryption=If(
144+
use_aes256_encryption_cond,
145+
BucketEncryption(
146+
ServerSideEncryptionConfiguration=[
148147
ServerSideEncryptionRule(
149148
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
150149
SSEAlgorithm=If(use_cmk_arn, 'aws:kms', 'AES256'),
151150
KMSMasterKeyID=If(use_cmk_arn, Ref(cmk_arn), Ref("AWS::NoValue")),
152151
)
153152
)
154-
],
155-
[
156-
ServerSideEncryptionRule()
157153
]
158-
)
154+
),
155+
NoValue
159156
),
160157
**common_bucket_conf,
161158
)

stack/sftp.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
from troposphere import (
2+
And,
3+
Condition,
4+
Equals,
5+
GetAtt,
6+
If,
7+
Join,
8+
Not,
9+
NoValue,
10+
Output,
11+
Parameter,
12+
Ref,
13+
Tags,
14+
iam,
15+
transfer,
16+
)
17+
from troposphere.s3 import (
18+
Bucket,
19+
BucketEncryption,
20+
Private,
21+
PublicAccessBlockConfiguration,
22+
ServerSideEncryptionByDefault,
23+
ServerSideEncryptionRule,
24+
)
25+
26+
from .assets import common_bucket_conf
27+
from .common import arn_prefix, cmk_arn, use_aes256_encryption_cond, use_cmk_arn
28+
from .template import template
29+
30+
use_sftp_server = template.add_parameter(
31+
Parameter(
32+
"UseSFTPServer",
33+
Description="Whether or not to set up an SFTP service. If 'true', this will set up a transfer server and "
34+
"add an S3 bucket for its use, along with a role and policies for use when adding users.",
35+
Type="String",
36+
AllowedValues=["true", "false"],
37+
Default="false",
38+
),
39+
group="SFTP",
40+
label="Enable SFTP Server",
41+
)
42+
43+
use_sftp_condition = "UseSFTPServerCondition"
44+
use_sftp_with_kms_condition = "UseSFTPWithKMSCondition"
45+
use_sftp_without_kms_condition = "UseSFTPWithoutKMSCondition"
46+
47+
template.add_condition(use_sftp_condition, Equals(Ref(use_sftp_server), "true"))
48+
template.add_condition(
49+
# If this condition is true, we need to create policies and roles that give
50+
# access to the customer KMS.
51+
use_sftp_with_kms_condition,
52+
And(
53+
Equals(Ref(use_sftp_server), "true"),
54+
Condition(use_aes256_encryption_cond),
55+
Condition(use_cmk_arn),
56+
),
57+
)
58+
template.add_condition(
59+
# If this condition is true, we need to create policies and roles,
60+
# but they should not give access to customer KMS.
61+
use_sftp_without_kms_condition,
62+
And(Equals(Ref(use_sftp_server), "true"), Not(Condition(use_cmk_arn))),
63+
)
64+
65+
sftp_assets_bucket = Bucket(
66+
"SFTPAssetsBucket",
67+
Condition=use_sftp_condition,
68+
AccessControl=Private,
69+
PublicAccessBlockConfiguration=PublicAccessBlockConfiguration(
70+
BlockPublicAcls=True,
71+
BlockPublicPolicy=True,
72+
IgnorePublicAcls=True,
73+
RestrictPublicBuckets=True,
74+
),
75+
BucketEncryption=If(
76+
use_aes256_encryption_cond,
77+
BucketEncryption(
78+
ServerSideEncryptionConfiguration=[
79+
ServerSideEncryptionRule(
80+
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
81+
SSEAlgorithm=If(use_cmk_arn, "aws:kms", "AES256"),
82+
KMSMasterKeyID=If(
83+
use_cmk_arn, Ref(cmk_arn), Ref("AWS::NoValue")
84+
),
85+
)
86+
)
87+
]
88+
),
89+
NoValue,
90+
),
91+
**common_bucket_conf,
92+
)
93+
template.add_resource(sftp_assets_bucket)
94+
95+
# Output SFTP asset bucket name
96+
template.add_output(
97+
Output(
98+
"SFTPBucketDomainName",
99+
Condition=use_sftp_condition,
100+
Description="SFTP bucket domain name",
101+
Value=GetAtt(sftp_assets_bucket, "DomainName"),
102+
)
103+
)
104+
105+
106+
transfer_server = transfer.Server(
107+
"TransferServer",
108+
template=template,
109+
Condition=use_sftp_condition,
110+
IdentityProviderType="SERVICE_MANAGED",
111+
EndpointType="PUBLIC",
112+
Tags=Tags(Name=Join("-", [Ref("AWS::StackName"), "sftp"])),
113+
)
114+
115+
"""
116+
# WORKING SCOPEDOWN POLICY:
117+
{
118+
"Version": "2012-10-17",
119+
"Statement": [
120+
{
121+
"Effect": "Allow",
122+
"Action": [
123+
"kms:DescribeKey",
124+
"kms:GenerateDataKey",
125+
"kms:Encrypt",
126+
"kms:Decrypt"
127+
],
128+
"Resource": "arn:aws:kms:us-east-1:XXXXXX"
129+
},
130+
{
131+
"Sid": "AllowListingOfUserFolder",
132+
"Action": [
133+
"s3:ListBucket"
134+
],
135+
"Effect": "Allow",
136+
"Resource": [
137+
"arn:aws:s3:::${transfer:HomeBucket}"
138+
],
139+
"Condition": {
140+
"StringLike": {
141+
"s3:prefix": [
142+
"${transfer:UserName}/*",
143+
"${transfer:UserName}"
144+
]
145+
}
146+
}
147+
},
148+
{
149+
"Sid": "HomeDirObjectAccess",
150+
"Effect": "Allow",
151+
"Action": [
152+
"s3:PutObject",
153+
"s3:GetObject",
154+
"s3:DeleteObjectVersion",
155+
"s3:DeleteObject",
156+
"s3:GetObjectVersion"
157+
],
158+
"Resource": [
159+
"arn:aws:s3:::sftp-staging.forcetherapeutics.com/${transfer:UserName}",
160+
"arn:aws:s3:::sftp-staging.forcetherapeutics.com/${transfer:UserName}/*"
161+
]
162+
}
163+
]
164+
}
165+
"""
166+
167+
168+
# The scopedown policy is used to restrict a user's access to the parts of the bucket
169+
# we don't want them to access.
170+
common_scopedown_policy_statements = [
171+
{
172+
"Sid": "AllowListingOfSFTPUserFolder",
173+
"Action": ["s3:ListBucket"],
174+
"Effect": "Allow",
175+
"Resource": ["arn:aws:s3:::${transfer:HomeBucket}"],
176+
"Condition": {
177+
"StringLike": {
178+
"s3:prefix": ["${transfer:UserName}/*", "${transfer:UserName}"]
179+
}
180+
},
181+
},
182+
{
183+
"Sid": "HomeDirObjectAccess",
184+
"Effect": "Allow",
185+
"Action": [
186+
"s3:PutObject",
187+
"s3:GetObject",
188+
"s3:DeleteObjectVersion",
189+
"s3:DeleteObject",
190+
"s3:GetObjectVersion",
191+
],
192+
"Resource": [
193+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}"]),
194+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}/*"]),
195+
],
196+
},
197+
]
198+
199+
kms_policy_statement = dict(
200+
Effect="Allow",
201+
Action=["kms:DescribeKey", "kms:GenerateDataKey", "kms:Encrypt", "kms:Decrypt"],
202+
Resource=Ref(cmk_arn),
203+
)
204+
205+
scopedown_policy = iam.ManagedPolicy(
206+
# This is for applying when adding users to the transfer server. It's not used directly in the stack creation,
207+
# other than adding it to IAM for later use.
208+
"SFTPUserScopeDownPolicy",
209+
PolicyDocument=dict(
210+
Version="2012-10-17",
211+
Statement=If(
212+
use_sftp_with_kms_condition,
213+
common_scopedown_policy_statements + [kms_policy_statement],
214+
common_scopedown_policy_statements,
215+
),
216+
),
217+
)
218+
template.add_resource(scopedown_policy)
219+
220+
# The ROLE is applied to users to let them access the bucket in general,
221+
# without regart to who they are.
222+
common_role_statements = [
223+
dict(
224+
Effect="Allow",
225+
Action=["s3:ListBucket", "s3:GetBucketLocation"],
226+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket)]),
227+
),
228+
dict(
229+
Effect="Allow",
230+
Action=[
231+
"s3:PutObject",
232+
"s3:GetObject",
233+
"s3:DeleteObject",
234+
"s3:DeleteObjectVersion",
235+
"s3:GetObjectVersion",
236+
"s3:GetObjectACL",
237+
"s3:PutObjectACL",
238+
],
239+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket), "/*"]),
240+
),
241+
]
242+
243+
sftp_user_role = iam.Role(
244+
# This also is not used directly during the stack setup, but is put into IAM
245+
# to be used later when adding users to the transfer server.
246+
"SFTPUserRole",
247+
template=template,
248+
AssumeRolePolicyDocument=dict(
249+
Statement=[
250+
dict(
251+
Effect="Allow",
252+
Principal=dict(Service=["transfer.amazonaws.com"]),
253+
Action=["sts:AssumeRole"],
254+
)
255+
]
256+
),
257+
Policies=[
258+
iam.Policy(
259+
"SFTPSUserRolePolicy",
260+
PolicyName="SFTPSUserRolePolicy",
261+
PolicyDocument=dict(
262+
Version="2012-10-17",
263+
Statement=If(
264+
use_sftp_with_kms_condition,
265+
common_role_statements + [kms_policy_statement],
266+
common_role_statements,
267+
),
268+
),
269+
)
270+
],
271+
RoleName=Join("-", [Ref("AWS::StackName"), "SFTPUserRole"]),
272+
)

0 commit comments

Comments
 (0)