mirror of
https://github.com/Sea-Haven-Industries/file-share.git
synced 2026-09-30 06:33:17 +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:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
run-tests: true
|
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
|
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:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -4,3 +4,9 @@ cdk.out/
|
||||||
*.d.ts
|
*.d.ts
|
||||||
*.js.map
|
*.js.map
|
||||||
.env
|
.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.
|
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
|
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
|
## Deploy
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
npm install
|
|
||||||
npx cdk deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## 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