fix(minimal): Replace Traefik HelmChart with direct deployment
HelmChart values schema changed in newer Traefik versions causing installation failures. Replaced with direct Deployment + RBAC manifests which work reliably with Traefik v3.2. Also adds SSH public key variable for admin access. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c5b5945b46
commit
0d904fe130
8 changed files with 168 additions and 350 deletions
|
|
@ -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.<domain>",
|
||||
" - ns2.<domain>",
|
||||
" - ns3.<domain>",
|
||||
"",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
|
|||
BIN
terraform/plan.tfplan
Normal file
BIN
terraform/plan.tfplan
Normal file
Binary file not shown.
|
|
@ -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}"
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue