main.tf
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | ###################################################################### |
| 2 | # Paperclip orchestrator — AWS infrastructure |
| 3 | # |
| 4 | # Provisions ONE EC2 t3.large with: |
| 5 | # - Ubuntu 24.04 LTS (latest Canonical AMI for the chosen region) |
| 6 | # - 30 GB gp3 EBS root volume (encrypted) |
| 7 | # - IAM role with read/write access to /knowtation/paperclip/* in SSM (push-secrets on instance) |
| 8 | # - Security group: SSH from your home IP, Tailscale UDP, HTTP/HTTPS |
| 9 | # - User-data script that joins Tailscale on first boot and runs install.sh |
| 10 | # |
| 11 | # After 'terraform apply': |
| 12 | # - SSH via Tailscale: ssh ubuntu@paperclip-prod |
| 13 | # - SSM secrets pre-seeded with empty placeholders for hub URL + vault ID |
| 14 | # - Real secrets pushed later via deploy/paperclip/scripts/push-secrets.sh |
| 15 | ###################################################################### |
| 16 | |
| 17 | # Latest Ubuntu 24.04 LTS AMI for the chosen region. |
| 18 | data "aws_ami" "ubuntu" { |
| 19 | most_recent = true |
| 20 | owners = ["099720109477"] # Canonical |
| 21 | |
| 22 | filter { |
| 23 | name = "name" |
| 24 | values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"] |
| 25 | } |
| 26 | |
| 27 | filter { |
| 28 | name = "virtualization-type" |
| 29 | values = ["hvm"] |
| 30 | } |
| 31 | |
| 32 | filter { |
| 33 | name = "architecture" |
| 34 | values = ["x86_64"] |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | # Default VPC + first default subnet — keeps blast radius narrow for a single-instance deploy. |
| 39 | data "aws_vpc" "default" { |
| 40 | default = true |
| 41 | } |
| 42 | |
| 43 | data "aws_subnets" "default" { |
| 44 | filter { |
| 45 | name = "vpc-id" |
| 46 | values = [data.aws_vpc.default.id] |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | # Random suffix so multiple deploys (prod + staging) don't collide on names. |
| 51 | resource "random_id" "suffix" { |
| 52 | byte_length = 4 |
| 53 | } |
| 54 | |
| 55 | ################# |
| 56 | # SSH key pair |
| 57 | ################# |
| 58 | |
| 59 | resource "aws_key_pair" "operator" { |
| 60 | key_name = "knowtation-paperclip-${var.environment}-${random_id.suffix.hex}" |
| 61 | public_key = var.ssh_public_key |
| 62 | } |
| 63 | |
| 64 | ################# |
| 65 | # Security group |
| 66 | ################# |
| 67 | |
| 68 | resource "aws_security_group" "paperclip" { |
| 69 | name = "knowtation-paperclip-${var.environment}-${random_id.suffix.hex}" |
| 70 | description = "Paperclip orchestrator: SSH from home IP only, Tailscale, HTTP for Lets Encrypt" |
| 71 | vpc_id = data.aws_vpc.default.id |
| 72 | |
| 73 | # SSH (TCP 22) — your home IP only. Primary access is Tailscale; this is fallback. |
| 74 | ingress { |
| 75 | description = "SSH from operator home IP" |
| 76 | from_port = 22 |
| 77 | to_port = 22 |
| 78 | protocol = "tcp" |
| 79 | cidr_blocks = [var.home_ip_cidr] |
| 80 | } |
| 81 | |
| 82 | # Tailscale (UDP 41641) — outbound NAT-traversal; ingress is for direct connections. |
| 83 | ingress { |
| 84 | description = "Tailscale direct connection" |
| 85 | from_port = 41641 |
| 86 | to_port = 41641 |
| 87 | protocol = "udp" |
| 88 | cidr_blocks = ["0.0.0.0/0"] |
| 89 | } |
| 90 | |
| 91 | # HTTP — Let's Encrypt HTTP-01 challenge ONLY. Paperclip dashboard is Tailscale-only. |
| 92 | ingress { |
| 93 | description = "Lets Encrypt HTTP-01 challenge port 80" |
| 94 | from_port = 80 |
| 95 | to_port = 80 |
| 96 | protocol = "tcp" |
| 97 | cidr_blocks = ["0.0.0.0/0"] |
| 98 | } |
| 99 | |
| 100 | # HTTPS — only if you intentionally expose the Paperclip dashboard publicly. Closed by default. |
| 101 | # Uncomment to expose. Recommended: keep closed, use Tailscale Funnel for sharing. |
| 102 | # ingress { |
| 103 | # description = "HTTPS public dashboard (off by default)" |
| 104 | # from_port = 443 |
| 105 | # to_port = 443 |
| 106 | # protocol = "tcp" |
| 107 | # cidr_blocks = ["0.0.0.0/0"] |
| 108 | # } |
| 109 | |
| 110 | egress { |
| 111 | description = "All outbound (Paperclip calls DeepInfra, HeyGen, ElevenLabs, Descript, Knowtation Hub)" |
| 112 | from_port = 0 |
| 113 | to_port = 0 |
| 114 | protocol = "-1" |
| 115 | cidr_blocks = ["0.0.0.0/0"] |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | ################################## |
| 120 | # IAM role + SSM Parameter access |
| 121 | ################################## |
| 122 | |
| 123 | resource "aws_iam_role" "paperclip" { |
| 124 | name = "knowtation-paperclip-${var.environment}-${random_id.suffix.hex}" |
| 125 | |
| 126 | assume_role_policy = jsonencode({ |
| 127 | Version = "2012-10-17" |
| 128 | Statement = [{ |
| 129 | Effect = "Allow" |
| 130 | Principal = { Service = "ec2.amazonaws.com" } |
| 131 | Action = "sts:AssumeRole" |
| 132 | }] |
| 133 | }) |
| 134 | } |
| 135 | |
| 136 | resource "aws_iam_role_policy" "ssm_read" { |
| 137 | name = "ssm-paperclip-secrets-readwrite" |
| 138 | role = aws_iam_role.paperclip.id |
| 139 | |
| 140 | policy = jsonencode({ |
| 141 | Version = "2012-10-17" |
| 142 | Statement = [ |
| 143 | { |
| 144 | Effect = "Allow" |
| 145 | Action = [ |
| 146 | "ssm:GetParameter", |
| 147 | "ssm:GetParameters", |
| 148 | "ssm:GetParametersByPath", |
| 149 | "ssm:PutParameter", |
| 150 | "ssm:DeleteParameter" |
| 151 | ] |
| 152 | Resource = "arn:aws:ssm:${var.aws_region}:*:parameter/knowtation/paperclip/*" |
| 153 | }, |
| 154 | { |
| 155 | Effect = "Allow" |
| 156 | Action = [ |
| 157 | "kms:Decrypt", |
| 158 | "kms:Encrypt", |
| 159 | "kms:GenerateDataKey" |
| 160 | ] |
| 161 | Resource = "*" |
| 162 | Condition = { |
| 163 | StringEquals = { |
| 164 | "kms:ViaService" = "ssm.${var.aws_region}.amazonaws.com" |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | ] |
| 169 | }) |
| 170 | } |
| 171 | |
| 172 | resource "aws_iam_role_policy_attachment" "ssm_managed_core" { |
| 173 | role = aws_iam_role.paperclip.name |
| 174 | policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" |
| 175 | } |
| 176 | |
| 177 | resource "aws_iam_instance_profile" "paperclip" { |
| 178 | name = "knowtation-paperclip-${var.environment}-${random_id.suffix.hex}" |
| 179 | role = aws_iam_role.paperclip.name |
| 180 | } |
| 181 | |
| 182 | ############################################## |
| 183 | # SSM placeholders for non-secret config items |
| 184 | ############################################## |
| 185 | |
| 186 | # Empty placeholders — push-secrets.sh fills the real ones interactively. |
| 187 | # These exist so the install script's first boot doesn't fail on missing keys. |
| 188 | |
| 189 | resource "aws_ssm_parameter" "hub_url" { |
| 190 | count = var.knowtation_hub_url == "" ? 0 : 1 |
| 191 | name = "/knowtation/paperclip/KNOWTATION_HUB_URL" |
| 192 | description = "Knowtation hosted Hub URL" |
| 193 | type = "String" |
| 194 | value = var.knowtation_hub_url |
| 195 | tier = "Standard" |
| 196 | } |
| 197 | |
| 198 | resource "aws_ssm_parameter" "vault_id" { |
| 199 | name = "/knowtation/paperclip/KNOWTATION_VAULT_ID" |
| 200 | description = "Knowtation vault ID for the Hub MCP" |
| 201 | type = "String" |
| 202 | value = var.knowtation_vault_id |
| 203 | tier = "Standard" |
| 204 | } |
| 205 | |
| 206 | ################# |
| 207 | # EC2 instance |
| 208 | ################# |
| 209 | |
| 210 | resource "aws_instance" "paperclip" { |
| 211 | ami = data.aws_ami.ubuntu.id |
| 212 | instance_type = var.instance_type |
| 213 | subnet_id = data.aws_subnets.default.ids[0] |
| 214 | |
| 215 | vpc_security_group_ids = [aws_security_group.paperclip.id] |
| 216 | key_name = aws_key_pair.operator.key_name |
| 217 | iam_instance_profile = aws_iam_instance_profile.paperclip.name |
| 218 | |
| 219 | metadata_options { |
| 220 | http_tokens = "required" # IMDSv2 only |
| 221 | http_put_response_hop_limit = 2 |
| 222 | } |
| 223 | |
| 224 | root_block_device { |
| 225 | volume_type = "gp3" |
| 226 | volume_size = var.ebs_size_gb |
| 227 | encrypted = true |
| 228 | delete_on_termination = true |
| 229 | tags = { |
| 230 | Name = "knowtation-paperclip-${var.environment}-root" |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | user_data = templatefile("${path.module}/user-data.sh.tpl", { |
| 235 | tailscale_auth_key = var.tailscale_auth_key |
| 236 | hostname = "paperclip-${var.environment}" |
| 237 | }) |
| 238 | |
| 239 | user_data_replace_on_change = false |
| 240 | |
| 241 | tags = { |
| 242 | Name = "knowtation-paperclip-${var.environment}" |
| 243 | } |
| 244 | |
| 245 | lifecycle { |
| 246 | ignore_changes = [ |
| 247 | ami, # don't replace box just because Canonical published a new minor AMI |
| 248 | user_data, # user_data only matters first boot |
| 249 | root_block_device[0].tags, |
| 250 | ] |
| 251 | } |
| 252 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago