mirror of
https://github.com/Sea-Haven-Industries/file-share.git
synced 2026-09-30 04:23:11 +00:00
Compare commits
2 commits
e4f156bb98
...
7c72159f31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c72159f31 | ||
|
|
ee3b28b5b5 |
17 changed files with 1232 additions and 9 deletions
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
|
|
@ -13,3 +13,29 @@ jobs:
|
|||
with:
|
||||
node-version: "24"
|
||||
run-tests: true
|
||||
|
||||
terraform:
|
||||
name: Terraform
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: terraform
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: "1.16.0"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform fmt
|
||||
run: terraform fmt -check -recursive
|
||||
|
||||
- name: Terraform init
|
||||
run: terraform init -backend=false
|
||||
|
||||
- name: Terraform validate
|
||||
run: terraform validate
|
||||
|
|
|
|||
6
.github/workflows/deploy.yaml
vendored
6
.github/workflows/deploy.yaml
vendored
|
|
@ -1,7 +1,9 @@
|
|||
name: Deploy
|
||||
# PLAT-77: push-to-main CDK deploy is frozen so HCP Terraform is the only
|
||||
# path that can change this stack. workflow_dispatch stays for an explicit
|
||||
# rollback of the management-account stack.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -4,3 +4,9 @@ cdk.out/
|
|||
*.d.ts
|
||||
*.js.map
|
||||
.env
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
crash.log
|
||||
override.tf
|
||||
override.tf.json
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -6,9 +6,14 @@
|
|||
|
||||
Personal file share server on AWS — Samba for macOS Finder integration and FileBrowser for web-based file management. Accessible exclusively over the site-to-site VPN.
|
||||
|
||||
## Infrastructure (CDK)
|
||||
## Infrastructure
|
||||
|
||||
All infrastructure is defined as code with the [AWS CDK](https://docs.aws.amazon.com/cdk/) (TypeScript). The app synthesizes a single CloudFormation stack — `file-share` — that provisions everything described under [Architecture](#architecture), deployed to account `328440206208` in `us-east-1`.
|
||||
The live share is still the management-account CDK stack until cutover proof. The prod replacement is HCP Terraform in `terraform/`, workspace `file-share-prod`, trigger `terraform/**`. CDK deploy on push to main is frozen.
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `terraform/` | Prod EC2, subnet in the syslog VPC, DLM, and HCP roles |
|
||||
| `lib/file-share-stack.ts` | Management-account CDK stack, still the live path until decommission |
|
||||
|
||||
```
|
||||
bin/app.ts # CDK app entry point — instantiates the stack
|
||||
|
|
@ -80,12 +85,13 @@ aws secretsmanager create-secret --name file-share/filebrowser-password --secret
|
|||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx cdk deploy
|
||||
```
|
||||
Prod changes go through HCP Terraform workspace `file-share-prod` (manual apply until the move is sealed). The bootstrap apply creates the HCP roles and the instance boundary. The following apply, using `hcptf-file-share`, creates the subnet, security group, and DLM policy. `data_volume_id` stays empty until the snapshot copy exists, so those applies do not boot an instance.
|
||||
|
||||
The stack outputs the instance's private IP for SMB and FileBrowser access.
|
||||
The management-account CDK workflow no longer runs on push. `workflow_dispatch` remains for an explicit rollback of that stack.
|
||||
|
||||
The instance has no public IP. Its route table sends `10.10.0.0/16` and `10.30.0.0/16` through the syslog VPN gateway and everything else through a NAT gateway in the syslog public subnet. `10.10.0.0/16` is the Ronkonkoma office LAN and `10.30.0.0/16` is the Locust office LAN, the same pair the syslog VPN already routes. `10.20.0.0/16` is the management VPC and is not routed here.
|
||||
|
||||
Clients use the private IP. Office routing must include `10.40.20.0/24` on the existing syslog IPsec before SMB from the office will work. A check from `10.10.70.0/24` on 2026-09-28 reached the gateway for `10.40.10.254` and got no hop-1 reply for `10.40.20.1`. The nightly DLM policy targets volumes tagged `file-share-backup=true`. Tag the copied volume with that key at cutover.
|
||||
|
||||
## Expanding Storage
|
||||
|
||||
|
|
|
|||
26
terraform/.terraform.lock.hcl
generated
Normal file
26
terraform/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.66.0"
|
||||
constraints = "~> 6.64"
|
||||
hashes = [
|
||||
"h1:OnLj4nhqJnEcUzyyRKUjp1FgWG00Y8maikJEYSf9Zjw=",
|
||||
"zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
|
||||
"zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
|
||||
"zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
|
||||
"zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
|
||||
"zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
|
||||
"zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
|
||||
"zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
|
||||
"zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
|
||||
"zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
|
||||
"zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
|
||||
"zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
|
||||
"zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
|
||||
"zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
|
||||
"zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
|
||||
]
|
||||
}
|
||||
29
terraform/dlm.tf
Normal file
29
terraform/dlm.tf
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
resource "aws_dlm_lifecycle_policy" "nightly" {
|
||||
description = "Nightly EBS snapshots for file share"
|
||||
execution_role_arn = local.dlm_service_role_arn
|
||||
state = "ENABLED"
|
||||
|
||||
policy_details {
|
||||
resource_types = ["VOLUME"]
|
||||
|
||||
target_tags = {
|
||||
"file-share-backup" = "true"
|
||||
}
|
||||
|
||||
schedule {
|
||||
name = "file-share-nightly"
|
||||
|
||||
create_rule {
|
||||
interval = 24
|
||||
interval_unit = "HOURS"
|
||||
times = ["06:00"]
|
||||
}
|
||||
|
||||
retain_rule {
|
||||
count = 30
|
||||
}
|
||||
|
||||
copy_tags = true
|
||||
}
|
||||
}
|
||||
}
|
||||
44
terraform/ec2.tf
Normal file
44
terraform/ec2.tf
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
resource "aws_instance" "this" {
|
||||
count = local.create_instance ? 1 : 0
|
||||
|
||||
ami = var.ami_id
|
||||
instance_type = "t4g.small"
|
||||
subnet_id = aws_subnet.file_share.id
|
||||
vpc_security_group_ids = [aws_security_group.file_share.id]
|
||||
iam_instance_profile = aws_iam_instance_profile.this.name
|
||||
associate_public_ip_address = false
|
||||
user_data = local.user_data
|
||||
user_data_replace_on_change = false
|
||||
|
||||
root_block_device {
|
||||
volume_size = 20
|
||||
volume_type = "gp3"
|
||||
encrypted = true
|
||||
}
|
||||
|
||||
metadata_options {
|
||||
http_endpoint = "enabled"
|
||||
http_tokens = "required"
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "file-share"
|
||||
"file-share-backup" = "true"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
postcondition {
|
||||
condition = self.public_ip == null || self.public_ip == ""
|
||||
error_message = "file-share must not have a public IP."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_volume_attachment" "data" {
|
||||
count = local.create_instance ? 1 : 0
|
||||
|
||||
device_name = "/dev/xvdf"
|
||||
volume_id = var.data_volume_id
|
||||
instance_id = aws_instance.this[0].id
|
||||
stop_instance_before_detaching = true
|
||||
}
|
||||
529
terraform/hcp_iam.tf
Normal file
529
terraform/hcp_iam.tf
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
# HCP plan/apply roles for file-share-prod (PLAT-77).
|
||||
# Copy of the syslog-server EC2 shape, narrowed to this stack.
|
||||
# Create, do not import.
|
||||
#
|
||||
# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy
|
||||
# and PutRolePolicy on hcptf-* (including this role). First-apply sequence:
|
||||
# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh
|
||||
# --account prod --allow-workspace file-share-prod
|
||||
# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap /
|
||||
# hcptf-bootstrap-plan (workspace vars, never a project set).
|
||||
# 3. One Manual apply. Bootstrap can create these roles and the boundary.
|
||||
# Subnet, DLM, and the instance are created on the following apply
|
||||
# after TFC_AWS_* points at hcptf-file-share. Tolerate that partial
|
||||
# state.
|
||||
# 4. Point TFC_AWS_* back at hcptf-file-share / hcptf-file-share-plan.
|
||||
# 5. Re-run the create script without --allow-workspace to pin trust
|
||||
# back to iam-bootstrap-prod only.
|
||||
# Later apply-role IAM edits use the same window. Do not add StringLike
|
||||
# on bootstrap trust. CreatePolicy stays on hcptf-bootstrap only.
|
||||
|
||||
data "aws_iam_policy_document" "hcptf_apply_trust" {
|
||||
statement {
|
||||
sid = "HcpApply"
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/app.terraform.io"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = [
|
||||
"organization:seahaven:project:${local.hcp_project}:workspace:${local.hcp_workspace}:run_phase:apply",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcptf_plan_trust" {
|
||||
statement {
|
||||
sid = "HcpPlan"
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/app.terraform.io"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = [
|
||||
"organization:seahaven:project:${local.hcp_project}:workspace:${local.hcp_workspace}:run_phase:plan",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcptf_scoped_iam" {
|
||||
statement {
|
||||
sid = "DenyCreatePolicy"
|
||||
effect = "Deny"
|
||||
actions = [
|
||||
"iam:CreatePolicy",
|
||||
"iam:CreatePolicyVersion",
|
||||
"iam:DeletePolicy",
|
||||
"iam:DeletePolicyVersion",
|
||||
"iam:SetDefaultPolicyVersion",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "CreateExecRoleWithBoundary"
|
||||
effect = "Allow"
|
||||
actions = ["iam:CreateRole"]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
|
||||
condition {
|
||||
test = "StringLike"
|
||||
variable = "iam:PermissionsBoundary"
|
||||
values = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "MutateExecRoleWithBoundary"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:PutRolePermissionsBoundary",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
|
||||
condition {
|
||||
test = "StringLike"
|
||||
variable = "iam:PermissionsBoundary"
|
||||
values = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "WriteExecRoles"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:DeleteRole",
|
||||
"iam:DeleteRolePolicy",
|
||||
"iam:DetachRolePolicy",
|
||||
"iam:TagRole",
|
||||
"iam:UntagRole",
|
||||
"iam:UpdateAssumeRolePolicy",
|
||||
"iam:UpdateRole",
|
||||
"iam:UpdateRoleDescription",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "PassExecRoleToEc2"
|
||||
effect = "Allow"
|
||||
actions = ["iam:PassRole"]
|
||||
resources = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/tf-managed/${local.instance_role_name}"]
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "iam:PassedToService"
|
||||
values = ["ec2.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "PassDlmServiceRole"
|
||||
effect = "Allow"
|
||||
actions = ["iam:PassRole"]
|
||||
resources = [local.dlm_service_role_arn]
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "iam:PassedToService"
|
||||
values = ["dlm.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "CreateDlmServiceLinkedRole"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:CreateServiceLinkedRole",
|
||||
]
|
||||
resources = [local.dlm_service_role_arn]
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "iam:AWSServiceName"
|
||||
values = ["dlm.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "InstanceProfiles"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:AddRoleToInstanceProfile",
|
||||
"iam:CreateInstanceProfile",
|
||||
"iam:DeleteInstanceProfile",
|
||||
"iam:GetInstanceProfile",
|
||||
"iam:ListInstanceProfileTags",
|
||||
"iam:RemoveRoleFromInstanceProfile",
|
||||
"iam:TagInstanceProfile",
|
||||
"iam:UntagInstanceProfile",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:instance-profile/tf-managed/${local.instance_profile_name}",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "IamReadOnly"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetPolicy",
|
||||
"iam:GetPolicyVersion",
|
||||
"iam:GetRole",
|
||||
"iam:GetRolePolicy",
|
||||
"iam:GetInstanceProfile",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListInstanceProfiles",
|
||||
"iam:ListInstanceProfilesForRole",
|
||||
"iam:ListPolicies",
|
||||
"iam:ListPolicyVersions",
|
||||
"iam:ListRolePolicies",
|
||||
"iam:ListRoleTags",
|
||||
"iam:ListRoles",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "DenySelfMutation"
|
||||
effect = "Deny"
|
||||
actions = [
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:DeleteRole",
|
||||
"iam:DeleteRolePolicy",
|
||||
"iam:DeleteRolePermissionsBoundary",
|
||||
"iam:DetachRolePolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:PutRolePermissionsBoundary",
|
||||
"iam:UpdateAssumeRolePolicy",
|
||||
"iam:UpdateRole",
|
||||
"iam:UpdateRoleDescription",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/hcptf-*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/github-cfn-execution-role",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/githubdeploy-*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/cdk-hnb659fds-*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/OrganizationAccountAccessRole",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/seahaven-*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "DenyBoundaryTampering"
|
||||
effect = "Deny"
|
||||
actions = [
|
||||
"iam:DeleteRolePermissionsBoundary",
|
||||
"iam:DeleteUserPermissionsBoundary",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "DenyBoundaryPolicyEdit"
|
||||
effect = "Deny"
|
||||
actions = [
|
||||
"iam:CreatePolicyVersion",
|
||||
"iam:DeletePolicy",
|
||||
"iam:DeletePolicyVersion",
|
||||
"iam:SetDefaultPolicyVersion",
|
||||
]
|
||||
resources = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/seahaven-*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadTfManagedBoundary"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetPolicy",
|
||||
"iam:GetPolicyVersion",
|
||||
"iam:ListPolicyVersions",
|
||||
"iam:ListPolicyTags",
|
||||
"iam:TagPolicy",
|
||||
"iam:UntagPolicy",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcptf_apply_services" {
|
||||
# checkov:skip=CKV_AWS_111: EC2 describe and RunInstances APIs require Resource=*. This role cannot CreateVolume or DeleteVolume.
|
||||
statement {
|
||||
sid = "Ec2Network"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2:AllocateAddress",
|
||||
"ec2:AssociateAddress",
|
||||
"ec2:AssociateRouteTable",
|
||||
"ec2:CreateNatGateway",
|
||||
"ec2:CreateRoute",
|
||||
"ec2:CreateRouteTable",
|
||||
"ec2:AuthorizeSecurityGroupEgress",
|
||||
"ec2:AuthorizeSecurityGroupIngress",
|
||||
"ec2:CreateSecurityGroup",
|
||||
"ec2:CreateSubnet",
|
||||
"ec2:CreateTags",
|
||||
"ec2:DeleteNatGateway",
|
||||
"ec2:DeleteRoute",
|
||||
"ec2:DeleteRouteTable",
|
||||
"ec2:DeleteSecurityGroup",
|
||||
"ec2:DeleteSubnet",
|
||||
"ec2:DeleteTags",
|
||||
"ec2:DescribeAccountAttributes",
|
||||
"ec2:DescribeAddresses",
|
||||
"ec2:DescribeAddressesAttribute",
|
||||
"ec2:DescribeNatGateways",
|
||||
"ec2:DescribeAvailabilityZones",
|
||||
"ec2:DescribeImages",
|
||||
"ec2:DescribeInstanceAttribute",
|
||||
"ec2:DescribeInstanceCreditSpecifications",
|
||||
"ec2:DescribeInstanceStatus",
|
||||
"ec2:DescribeInstanceTypes",
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeInternetGateways",
|
||||
"ec2:DescribeNetworkInterfaces",
|
||||
"ec2:DescribePrefixLists",
|
||||
"ec2:DescribeRouteTables",
|
||||
"ec2:DescribeSecurityGroupRules",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeVolumeAttribute",
|
||||
"ec2:DescribeVolumeStatus",
|
||||
"ec2:DescribeVolumes",
|
||||
"ec2:DescribeVpcAttribute",
|
||||
"ec2:DescribeVpcs",
|
||||
"ec2:DescribeVpnGateways",
|
||||
"ec2:DisassociateAddress",
|
||||
"ec2:DisassociateRouteTable",
|
||||
"ec2:ReleaseAddress",
|
||||
"ec2:ModifySecurityGroupRules",
|
||||
"ec2:ModifySubnetAttribute",
|
||||
"ec2:RevokeSecurityGroupEgress",
|
||||
"ec2:RevokeSecurityGroupIngress",
|
||||
"ec2:UpdateSecurityGroupRuleDescriptionsEgress",
|
||||
"ec2:UpdateSecurityGroupRuleDescriptionsIngress",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "Ec2Instance"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2:AssociateIamInstanceProfile",
|
||||
"ec2:AttachVolume",
|
||||
"ec2:DescribeIamInstanceProfileAssociations",
|
||||
"ec2:DetachVolume",
|
||||
"ec2:DisassociateIamInstanceProfile",
|
||||
"ec2:GetConsoleOutput",
|
||||
"ec2:ModifyInstanceAttribute",
|
||||
"ec2:MonitorInstances",
|
||||
"ec2:ReplaceIamInstanceProfileAssociation",
|
||||
"ec2:RunInstances",
|
||||
"ec2:StartInstances",
|
||||
"ec2:StopInstances",
|
||||
"ec2:TerminateInstances",
|
||||
"ec2:UnmonitorInstances",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "DlmPolicy"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"dlm:CreateLifecyclePolicy",
|
||||
"dlm:DeleteLifecyclePolicy",
|
||||
"dlm:GetLifecyclePolicy",
|
||||
"dlm:ListTagsForResource",
|
||||
"dlm:TagResource",
|
||||
"dlm:UntagResource",
|
||||
"dlm:UpdateLifecyclePolicy",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcptf_plan_refresh" {
|
||||
statement {
|
||||
sid = "RefreshIamRoles"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetRole",
|
||||
"iam:GetRolePolicy",
|
||||
"iam:GetInstanceProfile",
|
||||
"iam:ListRolePolicies",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListInstanceProfilesForRole",
|
||||
"iam:ListRoleTags",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/tf-managed/${local.stack_prefix}*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:instance-profile/tf-managed/${local.stack_prefix}*",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.apply_role}",
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.plan_role}",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshManagedPolicies"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetPolicy",
|
||||
"iam:GetPolicyVersion",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
|
||||
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshEc2"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2:DescribeAccountAttributes",
|
||||
"ec2:DescribeAddresses",
|
||||
"ec2:DescribeAddressesAttribute",
|
||||
"ec2:DescribeAvailabilityZones",
|
||||
"ec2:DescribeIamInstanceProfileAssociations",
|
||||
"ec2:DescribeImages",
|
||||
"ec2:DescribeInstanceAttribute",
|
||||
"ec2:DescribeInstanceCreditSpecifications",
|
||||
"ec2:DescribeInstanceStatus",
|
||||
"ec2:DescribeInstanceTypes",
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeInternetGateways",
|
||||
"ec2:DescribeNatGateways",
|
||||
"ec2:DescribeNetworkInterfaces",
|
||||
"ec2:DescribePrefixLists",
|
||||
"ec2:DescribeRouteTables",
|
||||
"ec2:DescribeSecurityGroupRules",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeVolumeAttribute",
|
||||
"ec2:DescribeVolumeStatus",
|
||||
"ec2:DescribeVolumes",
|
||||
"ec2:DescribeVpcAttribute",
|
||||
"ec2:DescribeVpcs",
|
||||
"ec2:DescribeVpnGateways",
|
||||
"ec2:GetConsoleOutput",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshDlm"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"dlm:GetLifecyclePolicy",
|
||||
"dlm:ListTagsForResource",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "hcptf_apply" {
|
||||
name = local.apply_role
|
||||
assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json
|
||||
max_session_duration = 3600
|
||||
|
||||
tags = {
|
||||
Owner = "adam@seahavenind.com"
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "hcptf_plan" {
|
||||
name = local.plan_role
|
||||
assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json
|
||||
max_session_duration = 3600
|
||||
|
||||
tags = {
|
||||
Owner = "adam@seahavenind.com"
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "hcptf_scoped_iam" {
|
||||
name = "scoped-iam-management"
|
||||
role = aws_iam_role.hcptf_apply.id
|
||||
policy = data.aws_iam_policy_document.hcptf_scoped_iam.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "hcptf_apply_services" {
|
||||
# checkov:skip=CKV_AWS_111: EC2 describe and RunInstances APIs require Resource=*. This role cannot CreateVolume or DeleteVolume.
|
||||
name = "file-share-services"
|
||||
role = aws_iam_role.hcptf_apply.id
|
||||
policy = data.aws_iam_policy_document.hcptf_apply_services.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "hcptf_plan_refresh" {
|
||||
# checkov:skip=CKV_AWS_107: ViewOnlyAccess plus this sidecar is the HCP plan-role pattern. Actions are named. It does not add iam:CreateAccessKey, secretsmanager:GetSecretValue, or ssm:GetParameter on *.
|
||||
name = "file-share-plan-refresh"
|
||||
role = aws_iam_role.hcptf_plan.id
|
||||
policy = data.aws_iam_policy_document.hcptf_plan_refresh.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "hcptf_plan_view_only" {
|
||||
role = aws_iam_role.hcptf_plan.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" {
|
||||
role_name = aws_iam_role.hcptf_apply.name
|
||||
policy_arns = []
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" {
|
||||
role_name = aws_iam_role.hcptf_plan.name
|
||||
policy_arns = [
|
||||
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
|
||||
]
|
||||
}
|
||||
167
terraform/iam.tf
Normal file
167
terraform/iam.tf
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# Instance permissions boundary.
|
||||
# The scoped apply role denies iam:CreatePolicy / CreatePolicyVersion,
|
||||
# so later edits to this document need the hcptf-bootstrap window.
|
||||
|
||||
data "aws_iam_policy_document" "instance_boundary" {
|
||||
# checkov:skip=CKV_AWS_111: SSM agent APIs require Resource=*. Secret reads are ARN-scoped.
|
||||
statement {
|
||||
sid = "SecretsRead"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"secretsmanager:GetSecretValue",
|
||||
]
|
||||
resources = [
|
||||
var.smb_password_secret_arn,
|
||||
var.filebrowser_password_secret_arn,
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "Ec2DescribeForAgent"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeVolumes",
|
||||
"ec2:DescribeInstances",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "SsmAgentBuckets"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"s3:GetObject",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:s3:::aws-ssm-*/*",
|
||||
"arn:aws:s3:::aws-windows-downloads-*/*",
|
||||
"arn:aws:s3:::amazon-ssm-*/*",
|
||||
"arn:aws:s3:::amazon-ssm-packages-*/*",
|
||||
"arn:aws:s3:::patch-baseline-snapshot-*/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "SsmManagedInstance"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ssm:DescribeAssociation",
|
||||
"ssm:GetDeployablePatchSnapshotForInstance",
|
||||
"ssm:GetDocument",
|
||||
"ssm:DescribeDocument",
|
||||
"ssm:GetManifest",
|
||||
"ssm:ListAssociations",
|
||||
"ssm:ListInstanceAssociations",
|
||||
"ssm:PutInventory",
|
||||
"ssm:PutComplianceItems",
|
||||
"ssm:PutConfigurePackageResult",
|
||||
"ssm:UpdateAssociationStatus",
|
||||
"ssm:UpdateInstanceAssociationStatus",
|
||||
"ssm:UpdateInstanceInformation",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "SsmAgentParameters"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ssm:GetParameter",
|
||||
"ssm:GetParameters",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:ssm:${var.aws_region}::parameter/aws/service/*",
|
||||
"arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/aws/service/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "SsmMessages"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ssmmessages:CreateControlChannel",
|
||||
"ssmmessages:CreateDataChannel",
|
||||
"ssmmessages:OpenControlChannel",
|
||||
"ssmmessages:OpenDataChannel",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "Ec2Messages"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2messages:AcknowledgeMessage",
|
||||
"ec2messages:DeleteMessage",
|
||||
"ec2messages:FailMessage",
|
||||
"ec2messages:GetEndpoint",
|
||||
"ec2messages:GetMessages",
|
||||
"ec2messages:SendReply",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "instance_boundary" {
|
||||
# checkov:skip=CKV_AWS_111: SSM agent APIs require Resource=*. Secret reads are ARN-scoped.
|
||||
name = local.boundary_name
|
||||
path = "/tf-managed/"
|
||||
description = "EC2 permissions boundary for file-share."
|
||||
policy = data.aws_iam_policy_document.instance_boundary.json
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "instance_assume" {
|
||||
statement {
|
||||
sid = "Ec2Assume"
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["ec2.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "instance" {
|
||||
name = local.instance_role_name
|
||||
path = "/tf-managed/"
|
||||
assume_role_policy = data.aws_iam_policy_document.instance_assume.json
|
||||
permissions_boundary = aws_iam_policy.instance_boundary.arn
|
||||
|
||||
tags = {
|
||||
Name = local.instance_role_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "ssm" {
|
||||
role = aws_iam_role.instance.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "instance_secrets" {
|
||||
statement {
|
||||
sid = "SecretsRead"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"secretsmanager:GetSecretValue",
|
||||
]
|
||||
resources = [
|
||||
var.smb_password_secret_arn,
|
||||
var.filebrowser_password_secret_arn,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "instance_secrets" {
|
||||
name = "file-share-secrets"
|
||||
role = aws_iam_role.instance.id
|
||||
policy = data.aws_iam_policy_document.instance_secrets.json
|
||||
}
|
||||
|
||||
resource "aws_iam_instance_profile" "this" {
|
||||
name = local.instance_profile_name
|
||||
path = "/tf-managed/"
|
||||
role = aws_iam_role.instance.name
|
||||
}
|
||||
28
terraform/locals.tf
Normal file
28
terraform/locals.tf
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
locals {
|
||||
project = "file-share"
|
||||
environment = "prod"
|
||||
|
||||
hcp_project = "seahaven-prod"
|
||||
hcp_workspace = "file-share-prod"
|
||||
apply_role = "hcptf-file-share"
|
||||
plan_role = "hcptf-file-share-plan"
|
||||
stack_name = local.project
|
||||
stack_prefix = "file-share-"
|
||||
|
||||
instance_role_name = "file-share-role"
|
||||
instance_profile_name = "file-share-profile"
|
||||
boundary_name = "file-share-instance-boundary"
|
||||
|
||||
office_lan_cidrs = ["10.10.0.0/16", "10.30.0.0/16"]
|
||||
subnet_cidr = "10.40.20.0/24"
|
||||
subnet_az = "us-east-1a"
|
||||
|
||||
create_instance = var.data_volume_id != ""
|
||||
|
||||
dlm_service_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/aws-service-role/dlm.amazonaws.com/AWSServiceRoleForDataLifecycleManager"
|
||||
|
||||
user_data = templatefile("${path.module}/user_data.sh.tftpl", {
|
||||
aws_region = var.aws_region
|
||||
filebrowser_version = var.filebrowser_version
|
||||
})
|
||||
}
|
||||
98
terraform/network.tf
Normal file
98
terraform/network.tf
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
data "aws_vpc" "syslog" {
|
||||
filter {
|
||||
name = "tag:Name"
|
||||
values = ["syslog-server-vpc"]
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_internet_gateway" "syslog" {
|
||||
filter {
|
||||
name = "tag:Name"
|
||||
values = ["syslog-server-igw"]
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_vpn_gateway" "syslog" {
|
||||
filter {
|
||||
name = "tag:Name"
|
||||
values = ["syslog-server-office"]
|
||||
}
|
||||
|
||||
attached_vpc_id = data.aws_vpc.syslog.id
|
||||
}
|
||||
|
||||
data "aws_subnet" "syslog_public" {
|
||||
vpc_id = data.aws_vpc.syslog.id
|
||||
|
||||
filter {
|
||||
name = "tag:Name"
|
||||
values = ["syslog-server-public"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_eip" "nat" {
|
||||
domain = "vpc"
|
||||
|
||||
tags = {
|
||||
Name = "file-share-nat"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_nat_gateway" "file_share" {
|
||||
allocation_id = aws_eip.nat.id
|
||||
subnet_id = data.aws_subnet.syslog_public.id
|
||||
|
||||
tags = {
|
||||
Name = "file-share"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table" "file_share" {
|
||||
vpc_id = data.aws_vpc.syslog.id
|
||||
|
||||
tags = {
|
||||
Name = "file-share"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route" "office" {
|
||||
for_each = toset(local.office_lan_cidrs)
|
||||
|
||||
route_table_id = aws_route_table.file_share.id
|
||||
destination_cidr_block = each.value
|
||||
gateway_id = data.aws_vpn_gateway.syslog.id
|
||||
}
|
||||
|
||||
resource "aws_route" "nat" {
|
||||
route_table_id = aws_route_table.file_share.id
|
||||
destination_cidr_block = "0.0.0.0/0"
|
||||
nat_gateway_id = aws_nat_gateway.file_share.id
|
||||
}
|
||||
|
||||
resource "aws_subnet" "file_share" {
|
||||
vpc_id = data.aws_vpc.syslog.id
|
||||
cidr_block = local.subnet_cidr
|
||||
availability_zone = local.subnet_az
|
||||
map_public_ip_on_launch = false
|
||||
|
||||
tags = {
|
||||
Name = "file-share"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = one(data.aws_internet_gateway.syslog.attachments[*].vpc_id) == data.aws_vpc.syslog.id
|
||||
error_message = "syslog IGW is not attached to the syslog VPC."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = data.aws_vpn_gateway.syslog.attached_vpc_id == data.aws_vpc.syslog.id
|
||||
error_message = "syslog VPN gateway is not attached to the syslog VPC."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "file_share" {
|
||||
subnet_id = aws_subnet.file_share.id
|
||||
route_table_id = aws_route_table.file_share.id
|
||||
}
|
||||
17
terraform/outputs.tf
Normal file
17
terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
output "private_ip" {
|
||||
description = "SMB (smb://ip/files), FileBrowser (http://ip:8080), and SFTP. Clients use this address, not the public IP."
|
||||
value = one(aws_instance.this[*].private_ip)
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
description = "Always empty. Egress uses the NAT gateway. Clients use private_ip."
|
||||
value = one(aws_instance.this[*].public_ip)
|
||||
}
|
||||
|
||||
output "instance_id" {
|
||||
value = one(aws_instance.this[*].id)
|
||||
}
|
||||
|
||||
output "subnet_id" {
|
||||
value = aws_subnet.file_share.id
|
||||
}
|
||||
14
terraform/providers.tf
Normal file
14
terraform/providers.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
provider "aws" {
|
||||
region = var.aws_region
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Project = local.project
|
||||
Environment = "prod"
|
||||
ManagedBy = "terraform"
|
||||
Workspace = local.hcp_workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
49
terraform/security.tf
Normal file
49
terraform/security.tf
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
resource "aws_security_group" "file_share" {
|
||||
name = "file-share"
|
||||
description = "SMB, FileBrowser, and SFTP from office LANs"
|
||||
vpc_id = data.aws_vpc.syslog.id
|
||||
|
||||
tags = {
|
||||
Name = "file-share"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "all" {
|
||||
security_group_id = aws_security_group.file_share.id
|
||||
ip_protocol = "-1"
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
description = "Outbound for package install, Secrets Manager, and SSM"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "smb" {
|
||||
for_each = toset(local.office_lan_cidrs)
|
||||
|
||||
security_group_id = aws_security_group.file_share.id
|
||||
ip_protocol = "tcp"
|
||||
from_port = 445
|
||||
to_port = 445
|
||||
cidr_ipv4 = each.value
|
||||
description = "SMB from office LAN"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "filebrowser" {
|
||||
for_each = toset(local.office_lan_cidrs)
|
||||
|
||||
security_group_id = aws_security_group.file_share.id
|
||||
ip_protocol = "tcp"
|
||||
from_port = 8080
|
||||
to_port = 8080
|
||||
cidr_ipv4 = each.value
|
||||
description = "FileBrowser from office LAN"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "sftp" {
|
||||
for_each = toset(local.office_lan_cidrs)
|
||||
|
||||
security_group_id = aws_security_group.file_share.id
|
||||
ip_protocol = "tcp"
|
||||
from_port = 22
|
||||
to_port = 22
|
||||
cidr_ipv4 = each.value
|
||||
description = "SFTP from office LAN"
|
||||
}
|
||||
111
terraform/user_data.sh.tftpl
Normal file
111
terraform/user_data.sh.tftpl
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
until lsblk -dno NAME | grep -v "$(lsblk -no PKNAME "$(findmnt -n -o SOURCE /)" 2>/dev/null || echo xvda)" | grep -q .; do
|
||||
echo "Waiting for data volume..."
|
||||
sleep 5
|
||||
done
|
||||
DATA_DEVICE="/dev/$(lsblk -dno NAME | grep -v "$(lsblk -no PKNAME "$(findmnt -n -o SOURCE /)" 2>/dev/null || echo xvda)" | head -1)"
|
||||
if ! blkid "$DATA_DEVICE"; then
|
||||
mkfs.ext4 -L file-share-data "$DATA_DEVICE"
|
||||
fi
|
||||
mkdir -p /data
|
||||
grep -q 'LABEL=file-share-data /data ' /etc/fstab || echo "LABEL=file-share-data /data ext4 defaults,nofail 0 2" >> /etc/fstab
|
||||
mount -a
|
||||
mkdir -p /data/share
|
||||
|
||||
dnf install -y samba samba-common
|
||||
|
||||
useradd --system --no-create-home --shell /sbin/nologin adam || true
|
||||
chown adam:adam /data/share
|
||||
|
||||
SMB_PASSWORD="$(aws secretsmanager get-secret-value --secret-id file-share/smb-password --query SecretString --output text --region ${aws_region})"
|
||||
(printf '%s\n' "$SMB_PASSWORD"; printf '%s\n' "$SMB_PASSWORD") | smbpasswd -s -a adam
|
||||
if passwd --help 2>&1 | grep -q -- '--stdin'; then
|
||||
printf '%s\n' "$SMB_PASSWORD" | passwd --stdin adam
|
||||
else
|
||||
printf 'adam:%s\n' "$SMB_PASSWORD" | chpasswd
|
||||
fi
|
||||
unset SMB_PASSWORD
|
||||
|
||||
cat > /etc/samba/smb.conf << 'SMBEOF'
|
||||
[global]
|
||||
workgroup = SEAHAVEN
|
||||
server string = Sea Haven File Share
|
||||
security = user
|
||||
map to guest = never
|
||||
log file = /var/log/samba/log.%m
|
||||
max log size = 1000
|
||||
server min protocol = SMB3
|
||||
|
||||
# macOS Finder optimizations
|
||||
vfs objects = catia fruit streams_xattr
|
||||
fruit:metadata = stream
|
||||
fruit:model = MacSamba
|
||||
fruit:posix_rename = yes
|
||||
fruit:veto_appledouble = no
|
||||
fruit:nfs_aces = no
|
||||
fruit:wipe_intentionally_left_blank_rfork = yes
|
||||
fruit:delete_empty_adfiles = yes
|
||||
|
||||
[files]
|
||||
path = /data/share
|
||||
browseable = yes
|
||||
writable = yes
|
||||
valid users = adam
|
||||
create mask = 0644
|
||||
directory mask = 0755
|
||||
SMBEOF
|
||||
|
||||
systemctl enable --now smb nmb
|
||||
|
||||
curl -sfL "https://github.com/filebrowser/filebrowser/releases/download/${filebrowser_version}/linux-arm64-filebrowser.tar.gz" | tar xz -C /usr/local/bin filebrowser
|
||||
chmod +x /usr/local/bin/filebrowser
|
||||
|
||||
mkdir -p /etc/filebrowser
|
||||
FB_PASSWORD="$(aws secretsmanager get-secret-value --secret-id file-share/filebrowser-password --query SecretString --output text --region ${aws_region})"
|
||||
|
||||
cat > /etc/filebrowser/config.json << 'FBEOF'
|
||||
{
|
||||
"address": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"root": "/data/share",
|
||||
"database": "/etc/filebrowser/filebrowser.db",
|
||||
"log": "/var/log/filebrowser.log"
|
||||
}
|
||||
FBEOF
|
||||
|
||||
filebrowser config init --config /etc/filebrowser/config.json
|
||||
filebrowser users add admin "$FB_PASSWORD" --config /etc/filebrowser/config.json --perm.admin
|
||||
unset FB_PASSWORD
|
||||
|
||||
cat > /etc/systemd/system/filebrowser.service << 'SVCEOF'
|
||||
[Unit]
|
||||
Description=FileBrowser
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/filebrowser --config /etc/filebrowser/config.json
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now filebrowser
|
||||
|
||||
usermod -s /bin/bash -d /data/share adam
|
||||
if ! grep -q 'Match User adam' /etc/ssh/sshd_config; then
|
||||
cat >> /etc/ssh/sshd_config << 'SSHEOF'
|
||||
|
||||
Match User adam
|
||||
ForceCommand internal-sftp
|
||||
PasswordAuthentication yes
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding no
|
||||
SSHEOF
|
||||
fi
|
||||
systemctl restart sshd
|
||||
53
terraform/variables.tf
Normal file
53
terraform/variables.tf
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
variable "aws_region" {
|
||||
description = "Region every resource in this configuration is created in."
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "ami_id" {
|
||||
description = "Pinned Amazon Linux 2023 arm64 AMI. Changing this replaces the instance. Snapshot the data volume the same day and confirm before apply."
|
||||
type = string
|
||||
default = "ami-0eb45f74aa8a20238"
|
||||
}
|
||||
|
||||
variable "filebrowser_version" {
|
||||
description = "Pinned FileBrowser release. Do not track releases/latest."
|
||||
type = string
|
||||
default = "v2.63.23"
|
||||
|
||||
validation {
|
||||
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+$", var.filebrowser_version))
|
||||
error_message = "filebrowser_version must look like v2.63.23."
|
||||
}
|
||||
}
|
||||
|
||||
variable "smb_password_secret_arn" {
|
||||
description = "Exact ARN of file-share/smb-password in this account. Set as an HCP workspace variable. Never the secret value."
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = startswith(var.smb_password_secret_arn, "arn:aws:secretsmanager:")
|
||||
error_message = "smb_password_secret_arn must be a Secrets Manager ARN."
|
||||
}
|
||||
}
|
||||
|
||||
variable "filebrowser_password_secret_arn" {
|
||||
description = "Exact ARN of file-share/filebrowser-password in this account. Set as an HCP workspace variable. Never the secret value."
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = startswith(var.filebrowser_password_secret_arn, "arn:aws:secretsmanager:")
|
||||
error_message = "filebrowser_password_secret_arn must be a Secrets Manager ARN."
|
||||
}
|
||||
}
|
||||
|
||||
variable "data_volume_id" {
|
||||
description = "Imported data volume id. Empty until cutover. Terraform attaches this volume and must not create or delete it."
|
||||
type = string
|
||||
default = ""
|
||||
|
||||
validation {
|
||||
condition = var.data_volume_id == "" || startswith(var.data_volume_id, "vol-")
|
||||
error_message = "data_volume_id must be empty or an EBS volume id."
|
||||
}
|
||||
}
|
||||
18
terraform/versions.tf
Normal file
18
terraform/versions.tf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
terraform {
|
||||
required_version = ">= 1.14.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.64"
|
||||
}
|
||||
}
|
||||
|
||||
cloud {
|
||||
organization = "seahaven"
|
||||
|
||||
workspaces {
|
||||
name = "file-share-prod"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue