diff --git a/terraform/dns-elastic-ips.tf b/terraform/dns-elastic-ips.tf deleted file mode 100644 index 3fcb582..0000000 --- a/terraform/dns-elastic-ips.tf +++ /dev/null @@ -1,84 +0,0 @@ -# DNS Elastic IPs for Stable Glue Records -# RFC 0046: Domain Email Migration - Phase 1 -# -# Purpose: Allocate static Elastic IPs for the DNS NLB to enable -# stable glue records at GoDaddy for NS delegation. -# -# Architecture: -# - 3 EIPs (one per AZ) for high availability -# - Attached to NLB via subnet_mapping -# - Used as glue records: ns1.domain.com, ns2.domain.com, ns3.domain.com -# -# Cost: ~$0/mo (EIPs attached to running resources are free) - -locals { - # Enable static IPs for DNS delegation - enable_dns_static_ips = var.enable_dns_static_ips - - # Domains to be delegated from GoDaddy to PowerDNS - managed_domains = [ - "superviber.com", - "muffinlabs.ai", - "letemcook.com", - "appbasecamp.com", - "thanksforborrowing.com", - "alignment.coop" - ] -} - -# Allocate Elastic IPs for DNS NLB (one per AZ) -resource "aws_eip" "dns" { - count = local.enable_dns_static_ips ? length(local.azs) : 0 - domain = "vpc" - - tags = merge(local.common_tags, { - Name = "${local.name}-dns-${count.index + 1}" - Purpose = "dns-nlb" - RFC = "0046" - Description = "Stable IP for DNS glue records - AZ ${count.index + 1}" - }) - - lifecycle { - # Prevent accidental deletion - changing these breaks glue records - prevent_destroy = true - } -} - -# Output the EIP public IPs for glue record configuration -output "dns_elastic_ips" { - description = "Elastic IP addresses for DNS NLB (use for glue records at GoDaddy)" - value = aws_eip.dns[*].public_ip -} - -output "dns_elastic_ip_allocation_ids" { - description = "Elastic IP allocation IDs for NLB subnet_mapping" - value = aws_eip.dns[*].id -} - -output "glue_record_instructions" { - description = "Instructions for configuring glue records at GoDaddy" - value = local.enable_dns_static_ips ? join("\n", [ - "================================================================================", - "GoDaddy Glue Record Configuration", - "RFC 0046: Domain Email Migration - DNS Delegation", - "================================================================================", - "", - "Domains: ${join(", ", local.managed_domains)}", - "", - "1. Custom Nameservers (set for each domain):", - " - ns1.", - " - ns2.", - " - ns3.", - "", - "2. Glue Records (Host Records):", - " ns1 -> ${try(aws_eip.dns[0].public_ip, "PENDING")}", - " ns2 -> ${try(aws_eip.dns[1].public_ip, "PENDING")}", - " ns3 -> ${try(aws_eip.dns[2].public_ip, "PENDING")}", - "", - "3. Verification Commands:", - " dig @${try(aws_eip.dns[0].public_ip, "PENDING")} superviber.com NS", - " dig @8.8.8.8 superviber.com NS", - "", - "================================================================================" - ]) : "DNS static IPs not enabled. Set enable_dns_static_ips = true to allocate EIPs." -} diff --git a/terraform/main.tf b/terraform/main.tf index 29f4d9d..c8bacb1 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -107,9 +107,9 @@ module "nlb" { vpc_id = module.vpc.vpc_id public_subnet_ids = module.vpc.public_subnet_ids - # RFC 0046: Enable static IPs for stable DNS glue records - enable_static_ips = var.enable_dns_static_ips - elastic_ip_ids = var.enable_dns_static_ips ? aws_eip.dns[*].id : [] + # Static IPs disabled for initial deployment + enable_static_ips = false + elastic_ip_ids = [] tags = local.common_tags diff --git a/terraform/minimal/main.tf b/terraform/minimal/main.tf index 090870a..5af2716 100644 --- a/terraform/minimal/main.tf +++ b/terraform/minimal/main.tf @@ -332,6 +332,7 @@ resource "aws_instance" "forgejo" { letsencrypt_email = var.letsencrypt_email ssh_port = var.admin_ssh_port s3_bucket = aws_s3_bucket.backups.id + ssh_public_key = var.ssh_public_key })) tags = { diff --git a/terraform/minimal/user-data.sh b/terraform/minimal/user-data.sh index a8bb8e7..242aa30 100644 --- a/terraform/minimal/user-data.sh +++ b/terraform/minimal/user-data.sh @@ -32,6 +32,15 @@ systemctl enable --now docker sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config systemctl restart sshd +# Add admin SSH key +if [ -n "${ssh_public_key}" ]; then + mkdir -p /home/ec2-user/.ssh + echo "${ssh_public_key}" >> /home/ec2-user/.ssh/authorized_keys + chown -R ec2-user:ec2-user /home/ec2-user/.ssh + chmod 700 /home/ec2-user/.ssh + chmod 600 /home/ec2-user/.ssh/authorized_keys +fi + # Enable automatic security updates dnf install -y dnf-automatic sed -i 's/apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.conf @@ -64,52 +73,115 @@ kind: Namespace metadata: name: traefik --- -apiVersion: helm.cattle.io/v1 -kind: HelmChart +apiVersion: v1 +kind: ServiceAccount metadata: name: traefik - namespace: kube-system + namespace: traefik +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: traefik +rules: + - apiGroups: [""] + resources: [services, endpoints, secrets, nodes, pods] + verbs: [get, list, watch] + - apiGroups: [extensions, networking.k8s.io] + resources: [ingresses, ingressclasses] + verbs: [get, list, watch] + - apiGroups: [extensions, networking.k8s.io] + resources: [ingresses/status] + verbs: [update] + - apiGroups: [traefik.io] + resources: ["*"] + verbs: [get, list, watch] + - apiGroups: [discovery.k8s.io] + resources: [endpointslices] + verbs: [get, list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: traefik +subjects: + - kind: ServiceAccount + name: traefik + namespace: traefik +roleRef: + kind: ClusterRole + name: traefik + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: traefik + namespace: traefik spec: - repo: https://traefik.github.io/charts - chart: traefik - targetNamespace: traefik - valuesContent: |- - ports: - ssh: - port: 2222 - exposedPort: 22 - expose: - default: true - protocol: TCP - web: - redirectTo: - port: websecure - websecure: - tls: - enabled: true - ingressRoute: - dashboard: - enabled: false - certificatesResolvers: - letsencrypt: - acme: - email: ${letsencrypt_email} - storage: /data/acme.json - httpChallenge: - entryPoint: web - persistence: - enabled: true - size: 128Mi - additionalArguments: - - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - - "--entrypoints.ssh.address=:2222/tcp" - service: - type: LoadBalancer + replicas: 1 + selector: + matchLabels: + app: traefik + template: + metadata: + labels: + app: traefik + spec: + serviceAccountName: traefik + containers: + - name: traefik + image: traefik:v3.2 + args: + - --api.insecure=false + - --providers.kubernetesingress + - --providers.kubernetescrd + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.ssh.address=:22/tcp + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --certificatesresolvers.letsencrypt.acme.email=${letsencrypt_email} + - --certificatesresolvers.letsencrypt.acme.storage=/data/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + ports: + - name: web + containerPort: 80 + - name: websecure + containerPort: 443 + - name: ssh + containerPort: 22 + volumeMounts: + - name: acme + mountPath: /data + volumes: + - name: acme + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: traefik + namespace: traefik +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app: traefik + ports: + - name: web + port: 80 + targetPort: 80 + - name: websecure + port: 443 + targetPort: 443 + - name: ssh + port: 22 + targetPort: 22 EOF # Wait for Traefik echo "Waiting for Traefik..." -sleep 30 +sleep 15 # ----------------------------------------------------------------------------- # Create Forgejo Namespace and Resources diff --git a/terraform/minimal/variables.tf b/terraform/minimal/variables.tf index 568d114..070bccb 100644 --- a/terraform/minimal/variables.tf +++ b/terraform/minimal/variables.tf @@ -45,3 +45,9 @@ variable "admin_cidr_blocks" { type = list(string) default = ["0.0.0.0/0"] # Restrict this in production! } + +variable "ssh_public_key" { + description = "SSH public key for admin access" + type = string + default = "" +} diff --git a/terraform/plan.tfplan b/terraform/plan.tfplan new file mode 100644 index 0000000..c021cd9 Binary files /dev/null and b/terraform/plan.tfplan differ diff --git a/terraform/vault-pki-smime.tf b/terraform/vault-pki-smime.tf deleted file mode 100644 index 365e155..0000000 --- a/terraform/vault-pki-smime.tf +++ /dev/null @@ -1,212 +0,0 @@ -# Vault PKI for S/MIME Certificates -# RFC 0041: Self-Hosted DNS and Email -# ADR 0006: Zero-Knowledge Zero-Trust (Layer 3 encryption support) -# -# This configuration creates a PKI secrets engine in Vault for issuing -# S/MIME certificates to users. S/MIME provides end-to-end email encryption -# (Layer 3) where only the sender and recipient can read the email content. - -terraform { - required_providers { - vault = { - source = "hashicorp/vault" - version = "~> 4.0" - } - } -} - -# ============================================================================= -# ROOT CA (Offline - used only to sign intermediate) -# ============================================================================= - -resource "vault_mount" "pki_root_smime" { - path = "pki-smime-root" - type = "pki" - description = "S/MIME Root CA - Offline, used only to sign intermediate" - - # Root CA has a longer lifetime - default_lease_ttl_seconds = 315360000 # 10 years - max_lease_ttl_seconds = 315360000 # 10 years -} - -resource "vault_pki_secret_backend_root_cert" "smime_root" { - backend = vault_mount.pki_root_smime.path - type = "internal" - common_name = "Alignment S/MIME Root CA" - ttl = "315360000" # 10 years - key_type = "rsa" - key_bits = 4096 - - organization = "Alignment" - country = "US" - exclude_cn_from_sans = true -} - -# ============================================================================= -# INTERMEDIATE CA (Online - used to issue user certificates) -# ============================================================================= - -resource "vault_mount" "pki_smime" { - path = "pki-smime" - type = "pki" - description = "S/MIME Intermediate CA - Issues user certificates" - - # Intermediate has shorter lifetime - default_lease_ttl_seconds = 31536000 # 1 year - max_lease_ttl_seconds = 94608000 # 3 years -} - -# Generate intermediate CSR -resource "vault_pki_secret_backend_intermediate_cert_request" "smime_intermediate" { - backend = vault_mount.pki_smime.path - type = "internal" - common_name = "Alignment S/MIME Intermediate CA" - key_type = "rsa" - key_bits = 4096 - - organization = "Alignment" - country = "US" -} - -# Sign intermediate with root -resource "vault_pki_secret_backend_root_sign_intermediate" "smime_intermediate" { - backend = vault_mount.pki_root_smime.path - common_name = "Alignment S/MIME Intermediate CA" - csr = vault_pki_secret_backend_intermediate_cert_request.smime_intermediate.csr - ttl = "157680000" # 5 years - - organization = "Alignment" - country = "US" - - # Intermediate CA permissions - permitted_dns_domains = ["alignment.dev"] -} - -# Set the signed intermediate certificate -resource "vault_pki_secret_backend_intermediate_set_signed" "smime_intermediate" { - backend = vault_mount.pki_smime.path - certificate = vault_pki_secret_backend_root_sign_intermediate.smime_intermediate.certificate -} - -# ============================================================================= -# S/MIME CERTIFICATE ROLE -# ============================================================================= - -resource "vault_pki_secret_backend_role" "smime_user" { - backend = vault_mount.pki_smime.path - name = "smime-user" - - # Certificate lifetime - ttl = 31536000 # 1 year - max_ttl = 63072000 # 2 years - - # Domain restrictions - allow_any_name = false - allowed_domains = ["alignment.dev"] - allow_subdomains = false - enforce_hostnames = false - - # Allow email addresses - allow_bare_domains = true - - # Key configuration - key_type = "rsa" - key_bits = 4096 - key_usage = ["DigitalSignature", "KeyEncipherment", "ContentCommitment"] - - # S/MIME specific - Email Protection extended key usage - ext_key_usage = ["EmailProtection"] - - # Certificate properties - require_cn = true - use_csr_common_name = true - use_csr_sans = true - - # Organization defaults - organization = ["Alignment"] - country = ["US"] - - # Don't include SANs automatically - allow_ip_sans = false - allow_localhost = false - allowed_uri_sans = [] - allowed_other_sans = [] - - # Generate certificates (not just sign CSRs) - generate_lease = true - no_store = false -} - -# ============================================================================= -# POLICY FOR S/MIME CERTIFICATE ISSUANCE -# ============================================================================= - -resource "vault_policy" "smime_user_issue" { - name = "smime-user-issue" - policy = <<-EOT - # Allow users to issue S/MIME certificates for their own email - # Users authenticate via OIDC, and their email claim is used to verify - # they can only request certificates for their own email address. - - # Issue S/MIME certificate - path "pki-smime/issue/smime-user" { - capabilities = ["create", "update"] - - # Restrict to user's own email - allowed_parameters = { - "common_name" = [] - "alt_names" = [] - "ttl" = [] - } - } - - # Read certificate chain - path "pki-smime/ca/pem" { - capabilities = ["read"] - } - - path "pki-smime/ca_chain" { - capabilities = ["read"] - } - - # List own certificates - path "pki-smime/certs" { - capabilities = ["list"] - } - EOT -} - -# ============================================================================= -# CRL CONFIGURATION -# ============================================================================= - -resource "vault_pki_secret_backend_config_urls" "smime_urls" { - backend = vault_mount.pki_smime.path - - issuing_certificates = [ - "https://vault.alignment.dev/v1/pki-smime/ca" - ] - - crl_distribution_points = [ - "https://vault.alignment.dev/v1/pki-smime/crl" - ] - - ocsp_servers = [ - "https://vault.alignment.dev/v1/pki-smime/ocsp" - ] -} - -# ============================================================================= -# OUTPUTS -# ============================================================================= - -output "smime_ca_chain" { - description = "S/MIME CA certificate chain" - value = vault_pki_secret_backend_intermediate_set_signed.smime_intermediate.certificate - sensitive = false -} - -output "smime_issue_path" { - description = "Path to issue S/MIME certificates" - value = "${vault_mount.pki_smime.path}/issue/${vault_pki_secret_backend_role.smime_user.name}" -} diff --git a/terraform/versions.tf b/terraform/versions.tf index 1f60354..e690119 100644 --- a/terraform/versions.tf +++ b/terraform/versions.tf @@ -1,11 +1,7 @@ -# Foundation Infrastructure - Terraform Configuration -# RFC 0039: ADR-Compliant Foundation Infrastructure -# ADR 0003: CockroachDB Self-Hosted FIPS -# ADR 0004: Set It and Forget It Architecture -# ADR 0005: Full-Stack Self-Hosting +# Hearth Infrastructure - Terraform Configuration terraform { - required_version = ">= 1.6.0" + required_version = ">= 1.5.0" required_providers { aws = { @@ -20,16 +16,55 @@ terraform { source = "hashicorp/helm" version = "~> 2.12" } - kubectl = { - source = "gavinbunney/kubectl" - version = "~> 1.14" - } tls = { source = "hashicorp/tls" version = "~> 4.0" } } - # Backend configuration - use S3 for state storage - # Configure in environments/production/backend.tf + backend "s3" { + bucket = "hearth-terraform-state-181640953119" + key = "hearth/terraform.tfstate" + region = "us-east-1" + dynamodb_table = "hearth-terraform-locks" + encrypt = true + profile = "hearth" + } +} + +provider "aws" { + region = var.aws_region + profile = "hearth" + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "terraform" + } + } +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name, "--profile", "hearth"] + } +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name, "--profile", "hearth"] + } + } }