Initial commit - realms platform
This commit is contained in:
parent
c590ab6d18
commit
c717c3751c
234 changed files with 74103 additions and 15231 deletions
41
devops/forgejo-server/.env.example
Normal file
41
devops/forgejo-server/.env.example
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# =============================================================================
|
||||
# Forgejo Server Environment Configuration
|
||||
# Copy to .env and fill in values
|
||||
# =============================================================================
|
||||
|
||||
# Domain for Forgejo (required)
|
||||
FORGEJO_DOMAIN=bit.realms.pub
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_USER=forgejo
|
||||
POSTGRES_PASSWORD=CHANGE_ME_SECURE_PASSWORD
|
||||
POSTGRES_DB=forgejo
|
||||
|
||||
# Forgejo Security Keys (generate with: openssl rand -hex 32)
|
||||
# SECRET_KEY: Used for encrypting data
|
||||
FORGEJO_SECRET_KEY=CHANGE_ME_GENERATE_WITH_openssl_rand_hex_32
|
||||
|
||||
# INTERNAL_TOKEN: Used for internal API authentication
|
||||
FORGEJO_INTERNAL_TOKEN=CHANGE_ME_GENERATE_WITH_openssl_rand_hex_32
|
||||
|
||||
# JWT_SECRET: Used for OAuth2 JWT tokens
|
||||
FORGEJO_JWT_SECRET=CHANGE_ME_GENERATE_WITH_openssl_rand_hex_32
|
||||
|
||||
# =============================================================================
|
||||
# Runner Configuration (set after initial setup)
|
||||
# =============================================================================
|
||||
|
||||
# Runner registration token (get from Forgejo admin panel)
|
||||
# Site Administration > Actions > Runners > Create new Runner
|
||||
# RUNNER_TOKEN=
|
||||
|
||||
# =============================================================================
|
||||
# Generate secure values with:
|
||||
#
|
||||
# # Generate all secrets at once
|
||||
# echo "FORGEJO_SECRET_KEY=$(openssl rand -hex 32)"
|
||||
# echo "FORGEJO_INTERNAL_TOKEN=$(openssl rand -hex 32)"
|
||||
# echo "FORGEJO_JWT_SECRET=$(openssl rand -hex 32)"
|
||||
# echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)"
|
||||
#
|
||||
# =============================================================================
|
||||
40
devops/forgejo-server/Caddyfile
Normal file
40
devops/forgejo-server/Caddyfile
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# =============================================================================
|
||||
# Caddy Configuration for Forgejo
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
# =============================================================================
|
||||
|
||||
{$FORGEJO_DOMAIN} {
|
||||
# Reverse proxy to Forgejo
|
||||
reverse_proxy forgejo:3000
|
||||
|
||||
# Enable compression
|
||||
encode gzip zstd
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
# HSTS
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Prevent clickjacking
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
# XSS protection
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
# Referrer policy
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# Remove server header
|
||||
-Server
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /data/access.log {
|
||||
roll_size 10mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP to HTTPS redirect (automatic with Caddy, but explicit for clarity)
|
||||
http://{$FORGEJO_DOMAIN} {
|
||||
redir https://{$FORGEJO_DOMAIN}{uri} permanent
|
||||
}
|
||||
137
devops/forgejo-server/README.md
Normal file
137
devops/forgejo-server/README.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Forgejo Server Setup
|
||||
|
||||
Git server with CI/CD for realms.india infrastructure.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Droplet with Docker and Docker Compose installed (via Terraform cloud-init)
|
||||
- Volume mounted at `/mnt/forgejo`
|
||||
- DNS A record pointing to droplet IP
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. Copy configuration files
|
||||
|
||||
```bash
|
||||
# SSH to Forgejo server via jump host
|
||||
ssh realms-forgejo
|
||||
|
||||
# Copy files to /opt/forgejo
|
||||
cd /opt/forgejo
|
||||
# (upload docker-compose.yml, Caddyfile, .env.example)
|
||||
```
|
||||
|
||||
### 2. Generate secrets and configure environment
|
||||
|
||||
```bash
|
||||
cd /opt/forgejo
|
||||
cp .env.example .env
|
||||
|
||||
# Generate secure values
|
||||
echo "FORGEJO_SECRET_KEY=$(openssl rand -hex 32)"
|
||||
echo "FORGEJO_INTERNAL_TOKEN=$(openssl rand -hex 32)"
|
||||
echo "FORGEJO_JWT_SECRET=$(openssl rand -hex 32)"
|
||||
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)"
|
||||
|
||||
# Edit .env with generated values
|
||||
vim .env
|
||||
```
|
||||
|
||||
### 3. Start Forgejo (without runner)
|
||||
|
||||
```bash
|
||||
docker compose up -d forgejo-db forgejo caddy
|
||||
docker compose logs -f forgejo
|
||||
```
|
||||
|
||||
### 4. Initial Forgejo Configuration
|
||||
|
||||
1. Visit `https://bit.realms.pub`
|
||||
2. Create admin account (first user becomes admin)
|
||||
3. Configure settings as needed
|
||||
|
||||
### 5. Register the Actions Runner
|
||||
|
||||
```bash
|
||||
# Get runner token from Forgejo
|
||||
# Site Administration > Actions > Runners > Create new Runner
|
||||
|
||||
# Register the runner
|
||||
docker compose run --rm forgejo-runner \
|
||||
forgejo-runner register \
|
||||
--instance https://bit.realms.pub \
|
||||
--token YOUR_RUNNER_TOKEN \
|
||||
--name realms-runner \
|
||||
--labels ubuntu-latest,docker \
|
||||
--no-interactive
|
||||
|
||||
# Start the runner
|
||||
docker compose up -d forgejo-runner
|
||||
```
|
||||
|
||||
### 6. Verify Setup
|
||||
|
||||
```bash
|
||||
# Check all services
|
||||
docker compose ps
|
||||
|
||||
# Check logs
|
||||
docker compose logs -f
|
||||
|
||||
# Test Git SSH
|
||||
ssh -T git@bit.realms.pub -p 2222
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### View logs
|
||||
```bash
|
||||
docker compose logs -f [service]
|
||||
```
|
||||
|
||||
### Restart services
|
||||
```bash
|
||||
docker compose restart [service]
|
||||
```
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
# Stop services
|
||||
docker compose down
|
||||
|
||||
# Backup volumes
|
||||
tar -czvf forgejo-backup-$(date +%Y%m%d).tar.gz /mnt/forgejo
|
||||
|
||||
# Restart
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Update Forgejo
|
||||
|
||||
```bash
|
||||
# Pull new image
|
||||
docker compose pull forgejo
|
||||
|
||||
# Recreate container
|
||||
docker compose up -d forgejo
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Runner won't start
|
||||
- Ensure runner is registered first
|
||||
- Check `/mnt/forgejo/runner-data/.runner` exists
|
||||
- Check logs: `docker compose logs forgejo-runner`
|
||||
|
||||
### SSL certificate issues
|
||||
- Ensure DNS is properly configured
|
||||
- Check Caddy logs: `docker compose logs caddy`
|
||||
- Caddy auto-obtains certs, may take a minute on first start
|
||||
|
||||
### Database connection issues
|
||||
- Check PostgreSQL is healthy: `docker compose ps`
|
||||
- Check logs: `docker compose logs forgejo-db`
|
||||
|
||||
### Git SSH not working
|
||||
- Verify port 2222 is open in firewall
|
||||
- Test: `ssh -T git@bit.realms.pub -p 2222 -v`
|
||||
208
devops/forgejo-server/docker-compose.yml
Normal file
208
devops/forgejo-server/docker-compose.yml
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# =============================================================================
|
||||
# Forgejo Git Server - Docker Compose Stack
|
||||
# Forgejo 11.0.8 LTS with PostgreSQL, Caddy, and Actions Runner
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgreSQL Database
|
||||
# ---------------------------------------------------------------------------
|
||||
forgejo-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: forgejo-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-forgejo}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-forgejo}
|
||||
volumes:
|
||||
- /mnt/forgejo/forgejo-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- forgejo-internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-forgejo} -d ${POSTGRES_DB:-forgejo}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Forgejo Git Server
|
||||
# Using rootless image for better security
|
||||
# ---------------------------------------------------------------------------
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:11.0.8-rootless
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
forgejo-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Database
|
||||
FORGEJO__database__DB_TYPE: postgres
|
||||
FORGEJO__database__HOST: forgejo-db:5432
|
||||
FORGEJO__database__NAME: ${POSTGRES_DB:-forgejo}
|
||||
FORGEJO__database__USER: ${POSTGRES_USER:-forgejo}
|
||||
FORGEJO__database__PASSWD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}
|
||||
|
||||
# Server configuration
|
||||
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN:?FORGEJO_DOMAIN required}
|
||||
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/
|
||||
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
|
||||
FORGEJO__server__SSH_PORT: 2222
|
||||
FORGEJO__server__SSH_LISTEN_PORT: 2222
|
||||
FORGEJO__server__START_SSH_SERVER: "true"
|
||||
FORGEJO__server__HTTP_PORT: 3000
|
||||
FORGEJO__server__LFS_START_SERVER: "true"
|
||||
|
||||
# Security
|
||||
FORGEJO__security__INSTALL_LOCK: "true"
|
||||
FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY:?FORGEJO_SECRET_KEY required}
|
||||
FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN:?FORGEJO_INTERNAL_TOKEN required}
|
||||
FORGEJO__security__PASSWORD_COMPLEXITY: "lower,upper,digit"
|
||||
FORGEJO__security__MIN_PASSWORD_LENGTH: "12"
|
||||
|
||||
# OAuth2 JWT secret
|
||||
FORGEJO__oauth2__JWT_SECRET: ${FORGEJO_JWT_SECRET:?FORGEJO_JWT_SECRET required}
|
||||
|
||||
# Service settings
|
||||
FORGEJO__service__DISABLE_REGISTRATION: "false"
|
||||
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "false"
|
||||
FORGEJO__service__ENABLE_NOTIFY_MAIL: "false"
|
||||
|
||||
# Actions (CI/CD)
|
||||
FORGEJO__actions__ENABLED: "true"
|
||||
FORGEJO__actions__DEFAULT_ACTIONS_URL: "https://code.forgejo.org"
|
||||
|
||||
# Repository settings
|
||||
FORGEJO__repository__DEFAULT_BRANCH: "main"
|
||||
FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true"
|
||||
FORGEJO__repository__ENABLE_PUSH_CREATE_ORG: "true"
|
||||
|
||||
# LFS settings
|
||||
FORGEJO__lfs__PATH: /data/lfs
|
||||
|
||||
# Webhook settings (for CI/CD)
|
||||
FORGEJO__webhook__ALLOWED_HOST_LIST: "private"
|
||||
FORGEJO__webhook__SKIP_TLS_VERIFY: "false"
|
||||
|
||||
# Log settings
|
||||
FORGEJO__log__MODE: "console"
|
||||
FORGEJO__log__LEVEL: "Info"
|
||||
|
||||
volumes:
|
||||
- /mnt/forgejo/forgejo-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- forgejo-internal
|
||||
- forgejo-public
|
||||
ports:
|
||||
# Git SSH - exposed publicly
|
||||
- "2222:2222"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caddy Reverse Proxy
|
||||
# Automatic HTTPS with Let's Encrypt
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: forgejo-caddy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
forgejo:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- forgejo-public
|
||||
environment:
|
||||
FORGEJO_DOMAIN: ${FORGEJO_DOMAIN}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Forgejo Actions Runner
|
||||
# For CI/CD pipelines
|
||||
# ---------------------------------------------------------------------------
|
||||
forgejo-runner:
|
||||
image: code.forgejo.org/forgejo/runner:6.3.1
|
||||
container_name: forgejo-runner
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
forgejo:
|
||||
condition: service_healthy
|
||||
docker-dind:
|
||||
condition: service_started
|
||||
environment:
|
||||
DOCKER_HOST: tcp://docker-dind:2376
|
||||
DOCKER_TLS_VERIFY: "1"
|
||||
DOCKER_CERT_PATH: /certs/client
|
||||
volumes:
|
||||
- /mnt/forgejo/runner-data:/data
|
||||
- dind-certs-client:/certs/client:ro
|
||||
networks:
|
||||
- forgejo-internal
|
||||
- dind-network
|
||||
command: >
|
||||
sh -c '
|
||||
if [ ! -f /data/.runner ]; then
|
||||
echo "Runner not registered. Please run registration command first."
|
||||
echo "See README for registration instructions."
|
||||
sleep infinity
|
||||
fi
|
||||
forgejo-runner daemon --config /data/config.yaml
|
||||
'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Docker-in-Docker for Runner
|
||||
# Allows building Docker images in CI/CD
|
||||
# ---------------------------------------------------------------------------
|
||||
docker-dind:
|
||||
image: docker:27-dind
|
||||
container_name: forgejo-dind
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
environment:
|
||||
DOCKER_TLS_CERTDIR: /certs
|
||||
volumes:
|
||||
- dind-certs-ca:/certs/ca
|
||||
- dind-certs-client:/certs/client
|
||||
- dind-storage:/var/lib/docker
|
||||
networks:
|
||||
- dind-network
|
||||
# Resource limits for 1GB RAM droplet
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
forgejo-internal:
|
||||
driver: bridge
|
||||
internal: true
|
||||
forgejo-public:
|
||||
driver: bridge
|
||||
dind-network:
|
||||
driver: bridge
|
||||
|
||||
# =============================================================================
|
||||
# Volumes
|
||||
# =============================================================================
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
dind-certs-ca:
|
||||
dind-certs-client:
|
||||
dind-storage:
|
||||
44
devops/terraform/.gitignore
vendored
Normal file
44
devops/terraform/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
# .tfstate files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# Crash log files
|
||||
crash.log
|
||||
crash.*.log
|
||||
|
||||
# Exclude all .tfvars files, which are likely to contain sensitive data
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
|
||||
# But include the example file
|
||||
!terraform.tfvars.example
|
||||
|
||||
# Ignore override files as they are usually used to override resources locally
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
|
||||
# Ignore CLI configuration files
|
||||
.terraformrc
|
||||
terraform.rc
|
||||
|
||||
# Ignore lock file (optional - some teams commit this)
|
||||
# .terraform.lock.hcl
|
||||
|
||||
# Secrets directory
|
||||
.secrets/
|
||||
|
||||
# SSH keys
|
||||
*.pem
|
||||
*_key
|
||||
*.key
|
||||
id_*
|
||||
!*.pub
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
106
devops/terraform/.terraform.lock.hcl
generated
Normal file
106
devops/terraform/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/digitalocean/digitalocean" {
|
||||
version = "2.72.0"
|
||||
constraints = ">= 2.34.0, ~> 2.34"
|
||||
hashes = [
|
||||
"h1:Evhic408nTkCx4bGRFOvzYKkpuHTQOWRw5UdU0tjVFE=",
|
||||
"zh:06fd036391b1e67e33b9aeb6e717b3be7f3fa358192111ac6cebc6d71e69ae17",
|
||||
"zh:159fd52b64482467994faf7fa2988f53817f7e213ddf0a5e51de3d49375d9c36",
|
||||
"zh:16b91e919a2b6f49cd6882d4241b9e05480a3838804114208e60f1b47b29b8e6",
|
||||
"zh:39dfb113e810070fe932ab8906280ea9f81e7009e165b9933c48c5f0b9b30b9d",
|
||||
"zh:432b3ef2f5b3d06821cb4cb11d705125bd365d7d6fb08db4b3ddcd0354471c5b",
|
||||
"zh:4e9e286f148df4de33dd6a656e6be5586bc3e49c744bc5b6a315f2cf179c3803",
|
||||
"zh:60a56107c1b047dc8be17a6944d6098a1fea6894929874ea70ff59a7879f7252",
|
||||
"zh:7ef58ae0830af5559f0cb9f6cff260bf7bd4c120d31781ce1c6297fe9b149d6d",
|
||||
"zh:9623850701a5a1d7840a32451b48bc6423791814e98cde8a26a112e824102845",
|
||||
"zh:a73e4eae3c510de4c3882f301a4024efe1c733b9ef20492b85d367f04c83db1a",
|
||||
"zh:a8ac85aa1bec870b88c8ff15c7d682b6239cc7fd3ec43348c71a0c25e1bc8d18",
|
||||
"zh:d72c44619d38e471a1a13809c111b459396fcb61be97058287bb883b422973a3",
|
||||
"zh:d8f2751dd548e996890dd8b91ee834fbdd64aeeb41df1ef0ffbf97b03e07ce19",
|
||||
"zh:decf393b215330f62b4f43135dd4827c0c32b3b230e5d9cf78d973e6862043a8",
|
||||
"zh:ecfe43046837abc2b9b1eeac945d103513fc485d63f0c2cc39e71b52d9c417f8",
|
||||
"zh:f27c41e46fcd5a2d9faba3aa51a4769dad3f413ac65203fb96ff7a0c68a801e6",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/cloudinit" {
|
||||
version = "2.3.7"
|
||||
constraints = ">= 2.3.0, ~> 2.3"
|
||||
hashes = [
|
||||
"h1:h1Pr6uNwq+iDEGrnQJEHzOTz+yVTW0AJgZrGXuoO4Qs=",
|
||||
"zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e",
|
||||
"zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5",
|
||||
"zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd",
|
||||
"zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1",
|
||||
"zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7",
|
||||
"zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01",
|
||||
"zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9",
|
||||
"zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a",
|
||||
"zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13",
|
||||
"zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14",
|
||||
"zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/local" {
|
||||
version = "2.6.1"
|
||||
constraints = "~> 2.4"
|
||||
hashes = [
|
||||
"h1:Ey2jPUGfuahWUs8w82yeZKBMYqh9SM3e6J+EQt7QTKk=",
|
||||
"zh:10050d08f416de42a857e4b6f76809aae63ea4ec6f5c852a126a915dede814b4",
|
||||
"zh:2df2a3ebe9830d4759c59b51702e209fe053f47453cb4688f43c063bac8746b7",
|
||||
"zh:2e759568bcc38c86ca0e43701d34cf29945736fdc8e429c5b287ddc2703c7b18",
|
||||
"zh:6a62a34e48500ab4aea778e355e162ebde03260b7a9eb9edc7e534c84fbca4c6",
|
||||
"zh:74373728ba32a1d5450a3a88ac45624579e32755b086cd4e51e88d9aca240ef6",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:8dddae588971a996f622e7589cd8b9da7834c744ac12bfb59c97fa77ded95255",
|
||||
"zh:946f82f66353bb97aefa8d95c4ca86db227f9b7c50b82415289ac47e4e74d08d",
|
||||
"zh:e9a5c09e6f35e510acf15b666fd0b34a30164cecdcd81ce7cda0f4b2dade8d91",
|
||||
"zh:eafe5b873ef42b32feb2f969c38ff8652507e695620cbaf03b9db714bee52249",
|
||||
"zh:ec146289fa27650c9d433bb5c7847379180c0b7a323b1b94e6e7ad5d2a7dbe71",
|
||||
"zh:fc882c35ce05631d76c0973b35adde26980778fc81d9da81a2fade2b9d73423b",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.7.2"
|
||||
constraints = "~> 3.6"
|
||||
hashes = [
|
||||
"h1:0hcNr59VEJbhZYwuDE/ysmyTS0evkfcLarlni+zATPM=",
|
||||
"zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f",
|
||||
"zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc",
|
||||
"zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab",
|
||||
"zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3",
|
||||
"zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212",
|
||||
"zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34",
|
||||
"zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967",
|
||||
"zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d",
|
||||
"zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62",
|
||||
"zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/tls" {
|
||||
version = "4.1.0"
|
||||
constraints = ">= 4.0.0, ~> 4.0"
|
||||
hashes = [
|
||||
"h1:y9cHrgcuaZt592In6xQzz1lx7k/B9EeWrAb8K7QqOgU=",
|
||||
"zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2",
|
||||
"zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8",
|
||||
"zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc",
|
||||
"zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc",
|
||||
"zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac",
|
||||
"zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882",
|
||||
"zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d",
|
||||
"zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298",
|
||||
"zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297",
|
||||
"zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
"zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54",
|
||||
]
|
||||
}
|
||||
107
devops/terraform/main.tf
Normal file
107
devops/terraform/main.tf
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# =============================================================================
|
||||
# realms.india - DigitalOcean Infrastructure (Phase A: Jump Host + Forgejo)
|
||||
# =============================================================================
|
||||
|
||||
locals {
|
||||
common_tags = concat([
|
||||
var.project_name,
|
||||
var.environment,
|
||||
"terraform-managed"
|
||||
], var.tags)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# VPC Module
|
||||
# =============================================================================
|
||||
|
||||
module "vpc" {
|
||||
source = "./modules/vpc"
|
||||
|
||||
name = "${var.project_name}-vpc-${var.environment}"
|
||||
region = var.region
|
||||
ip_range = var.vpc_ip_range
|
||||
description = "VPC for ${var.project_name} ${var.environment} environment"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SSH Keys Module
|
||||
# =============================================================================
|
||||
|
||||
module "ssh_keys" {
|
||||
source = "./modules/ssh_keys"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
admin_ssh_public_keys = var.admin_ssh_public_keys
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Jump Host Module (Bastion)
|
||||
# =============================================================================
|
||||
|
||||
module "jump_host" {
|
||||
source = "./modules/jump_host"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
vpc_uuid = module.vpc.vpc_id
|
||||
vpc_ip_range = var.vpc_ip_range
|
||||
ssh_keys = module.ssh_keys.all_ssh_key_ids
|
||||
droplet_size = var.jump_host_size
|
||||
droplet_image = var.jump_host_image
|
||||
ssh_port = var.jump_host_ssh_port
|
||||
enable_backups = var.enable_droplet_backups
|
||||
tags = local.common_tags
|
||||
internal_private_key = module.ssh_keys.internal_private_key
|
||||
|
||||
depends_on = [module.vpc, module.ssh_keys]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Module
|
||||
# =============================================================================
|
||||
|
||||
module "forgejo" {
|
||||
source = "./modules/forgejo"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
vpc_uuid = module.vpc.vpc_id
|
||||
vpc_ip_range = var.vpc_ip_range
|
||||
ssh_keys = module.ssh_keys.forgejo_ssh_key_ids
|
||||
droplet_size = var.forgejo_droplet_size
|
||||
droplet_image = var.forgejo_droplet_image
|
||||
volume_size = var.forgejo_volume_size
|
||||
ssh_port = var.forgejo_ssh_port
|
||||
git_ssh_port = var.forgejo_git_ssh_port
|
||||
domain = var.forgejo_domain
|
||||
enable_backups = var.enable_droplet_backups
|
||||
tags = local.common_tags
|
||||
|
||||
# DNS Configuration
|
||||
manage_dns = var.manage_dns
|
||||
dns_zone = var.dns_zone
|
||||
dns_record_name = "qbit" # Creates qbit.realms.pub
|
||||
|
||||
depends_on = [module.vpc, module.ssh_keys]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Firewalls Module
|
||||
# =============================================================================
|
||||
|
||||
module "firewalls" {
|
||||
source = "./modules/firewalls"
|
||||
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
vpc_ip_range = var.vpc_ip_range
|
||||
jump_host_droplet_id = module.jump_host.droplet_id
|
||||
jump_host_ssh_port = var.jump_host_ssh_port
|
||||
forgejo_droplet_id = module.forgejo.droplet_id
|
||||
forgejo_git_ssh_port = var.forgejo_git_ssh_port
|
||||
|
||||
depends_on = [module.jump_host, module.forgejo]
|
||||
}
|
||||
187
devops/terraform/modules/firewalls/main.tf
Normal file
187
devops/terraform/modules/firewalls/main.tf
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# =============================================================================
|
||||
# Firewalls Module
|
||||
# Defense in depth with DigitalOcean Cloud Firewalls
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Jump Host Firewall
|
||||
# Only allows SSH on non-standard port from anywhere
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "digitalocean_firewall" "jump_host" {
|
||||
name = "${var.project_name}-${var.environment}-jump-fw"
|
||||
|
||||
droplet_ids = [var.jump_host_droplet_id]
|
||||
|
||||
# Inbound: SSH on non-standard port
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = var.jump_host_ssh_port
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# Inbound: Allow all VPC traffic
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "1-65535"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
inbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "1-65535"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
inbound_rule {
|
||||
protocol = "icmp"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
# Outbound: Only necessary traffic (security hardening)
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "53"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # DNS
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "53"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # DNS
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "80"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # HTTP (apt)
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "443"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # HTTPS
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "123"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # NTP
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "icmp"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# VPC outbound (all ports for internal communication)
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "1-65535"
|
||||
destination_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "1-65535"
|
||||
destination_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Forgejo Firewall
|
||||
# Allows HTTP, HTTPS, and Git SSH from anywhere
|
||||
# System SSH only from VPC (handled by VPC rule)
|
||||
# -----------------------------------------------------------------------------
|
||||
resource "digitalocean_firewall" "forgejo" {
|
||||
name = "${var.project_name}-${var.environment}-forgejo-fw"
|
||||
|
||||
droplet_ids = [var.forgejo_droplet_id]
|
||||
|
||||
# Inbound: HTTP
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "80"
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# Inbound: HTTPS
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "443"
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# Inbound: Git SSH
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = var.forgejo_git_ssh_port
|
||||
source_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# Inbound: Allow all VPC traffic (includes system SSH on non-standard port)
|
||||
inbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "1-65535"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
inbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "1-65535"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
inbound_rule {
|
||||
protocol = "icmp"
|
||||
source_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
# Outbound: Only necessary traffic (security hardening)
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "53"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # DNS
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "53"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # DNS
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "80"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # HTTP (apt, Let's Encrypt)
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "443"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # HTTPS (Docker, webhooks)
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "123"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"] # NTP
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "icmp"
|
||||
destination_addresses = ["0.0.0.0/0", "::/0"]
|
||||
}
|
||||
|
||||
# VPC outbound (all ports for internal communication)
|
||||
outbound_rule {
|
||||
protocol = "tcp"
|
||||
port_range = "1-65535"
|
||||
destination_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
|
||||
outbound_rule {
|
||||
protocol = "udp"
|
||||
port_range = "1-65535"
|
||||
destination_addresses = [var.vpc_ip_range]
|
||||
}
|
||||
}
|
||||
13
devops/terraform/modules/firewalls/outputs.tf
Normal file
13
devops/terraform/modules/firewalls/outputs.tf
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# =============================================================================
|
||||
# Firewalls Module Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "jump_host_firewall_id" {
|
||||
description = "ID of the jump host firewall"
|
||||
value = digitalocean_firewall.jump_host.id
|
||||
}
|
||||
|
||||
output "forgejo_firewall_id" {
|
||||
description = "ID of the Forgejo firewall"
|
||||
value = digitalocean_firewall.forgejo.id
|
||||
}
|
||||
39
devops/terraform/modules/firewalls/variables.tf
Normal file
39
devops/terraform/modules/firewalls/variables.tf
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# =============================================================================
|
||||
# Firewalls Module Variables
|
||||
# =============================================================================
|
||||
|
||||
variable "project_name" {
|
||||
description = "Name of the project"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_ip_range" {
|
||||
description = "VPC IP range for internal traffic"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "jump_host_droplet_id" {
|
||||
description = "ID of the jump host droplet"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "jump_host_ssh_port" {
|
||||
description = "SSH port for jump host"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "forgejo_droplet_id" {
|
||||
description = "ID of the Forgejo droplet"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "forgejo_git_ssh_port" {
|
||||
description = "Git SSH port for Forgejo"
|
||||
type = number
|
||||
default = 2222
|
||||
}
|
||||
8
devops/terraform/modules/firewalls/versions.tf
Normal file
8
devops/terraform/modules/firewalls/versions.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = ">= 2.34"
|
||||
}
|
||||
}
|
||||
}
|
||||
590
devops/terraform/modules/forgejo/cloud-init.yaml.tpl
Normal file
590
devops/terraform/modules/forgejo/cloud-init.yaml.tpl
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
#cloud-config
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Server Cloud-Init Configuration
|
||||
# Git server with Docker for Forgejo 11.0.8 LTS - FULLY AUTOMATED
|
||||
# =============================================================================
|
||||
|
||||
# NOTE: We install packages via runcmd instead of packages: section
|
||||
# because DigitalOcean's base image cloud-init can interfere with the packages module
|
||||
|
||||
# bootcmd runs before write_files - create directories needed for write_files
|
||||
bootcmd:
|
||||
- mkdir -p /opt/forgejo/caddy
|
||||
|
||||
write_files:
|
||||
# SSH daemon configuration (non-standard port, VPC only access expected)
|
||||
- path: /etc/ssh/sshd_config.d/99-hardening.conf
|
||||
content: |
|
||||
# Non-standard port (VPC access only via jump host)
|
||||
Port ${ssh_port}
|
||||
|
||||
# Authentication hardening
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
|
||||
# Security settings
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
PermitEmptyPasswords no
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding no
|
||||
AllowAgentForwarding no
|
||||
PermitUserEnvironment no
|
||||
MaxSessions 3
|
||||
|
||||
# Session settings
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
|
||||
# Logging
|
||||
LogLevel VERBOSE
|
||||
permissions: '0644'
|
||||
|
||||
# NOTE: authorized_keys is managed by DigitalOcean's native SSH key provisioning
|
||||
# The internal key is added via digitalocean_ssh_key.internal resource
|
||||
|
||||
# Fail2ban configuration for system SSH
|
||||
- path: /etc/fail2ban/jail.d/sshd.local
|
||||
content: |
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ${ssh_port}
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
backend = systemd
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 3
|
||||
permissions: '0644'
|
||||
|
||||
# Fail2ban for Forgejo Git SSH
|
||||
- path: /etc/fail2ban/jail.d/forgejo-ssh.local
|
||||
content: |
|
||||
[forgejo-ssh]
|
||||
enabled = true
|
||||
port = ${git_ssh_port}
|
||||
filter = sshd
|
||||
logpath = /var/lib/docker/containers/*forgejo*/*-json.log
|
||||
backend = auto
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
permissions: '0644'
|
||||
|
||||
# Fail2ban filter for Forgejo HTTP auth failures
|
||||
- path: /etc/fail2ban/filter.d/forgejo.conf
|
||||
content: |
|
||||
[Definition]
|
||||
failregex = ^.*Failed authentication attempt for .* from <HOST>.*$
|
||||
^.*invalid credentials from <HOST>.*$
|
||||
ignoreregex =
|
||||
permissions: '0644'
|
||||
|
||||
# Fail2ban jail for Forgejo HTTP
|
||||
- path: /etc/fail2ban/jail.d/forgejo-http.local
|
||||
content: |
|
||||
[forgejo-http]
|
||||
enabled = true
|
||||
port = 80,443
|
||||
filter = forgejo
|
||||
logpath = /var/lib/docker/containers/*forgejo*/*-json.log
|
||||
backend = auto
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
permissions: '0644'
|
||||
|
||||
# Docker daemon configuration (hardened)
|
||||
- path: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"storage-driver": "overlay2",
|
||||
"live-restore": true,
|
||||
"no-new-privileges": true
|
||||
}
|
||||
permissions: '0644'
|
||||
|
||||
# UFW configuration script
|
||||
- path: /usr/local/bin/configure-firewall.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Reset UFW
|
||||
ufw --force reset
|
||||
|
||||
# Default policies
|
||||
ufw default deny incoming
|
||||
ufw default deny outgoing
|
||||
|
||||
# Inbound: HTTP/HTTPS for Forgejo web
|
||||
ufw allow in 80/tcp comment 'HTTP'
|
||||
ufw allow in 443/tcp comment 'HTTPS'
|
||||
|
||||
# Inbound: Git SSH (public)
|
||||
ufw allow in ${git_ssh_port}/tcp comment 'Git SSH'
|
||||
|
||||
# Inbound: VPC traffic (includes system SSH on non-standard port)
|
||||
ufw allow in from ${vpc_ip_range} comment 'VPC internal'
|
||||
|
||||
# Outbound: Only necessary traffic (matching DO firewall)
|
||||
ufw allow out 53/tcp comment 'DNS'
|
||||
ufw allow out 53/udp comment 'DNS'
|
||||
ufw allow out 80/tcp comment 'HTTP'
|
||||
ufw allow out 443/tcp comment 'HTTPS'
|
||||
ufw allow out 123/udp comment 'NTP'
|
||||
ufw allow out to ${vpc_ip_range} comment 'VPC'
|
||||
|
||||
# Enable logging for security audit
|
||||
ufw logging high
|
||||
|
||||
# Enable UFW
|
||||
ufw --force enable
|
||||
|
||||
echo "Firewall configured successfully"
|
||||
permissions: '0755'
|
||||
|
||||
# Volume mount script
|
||||
- path: /usr/local/bin/mount-volume.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VOLUME_NAME="${volume_name}"
|
||||
MOUNT_POINT="/mnt/forgejo"
|
||||
|
||||
# Create mount point
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
# Find the volume device
|
||||
# DigitalOcean volumes are typically at /dev/disk/by-id/scsi-0DO_Volume_*
|
||||
VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true)
|
||||
|
||||
if [ -z "$VOLUME_DEV" ]; then
|
||||
echo "Waiting for volume to attach..."
|
||||
sleep 10
|
||||
VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$VOLUME_DEV" ]; then
|
||||
# Check if already mounted
|
||||
if ! mountpoint -q "$MOUNT_POINT"; then
|
||||
mount "$VOLUME_DEV" "$MOUNT_POINT"
|
||||
echo "Volume mounted at $MOUNT_POINT"
|
||||
fi
|
||||
|
||||
# Add to fstab if not already there
|
||||
if ! grep -q "$VOLUME_DEV" /etc/fstab; then
|
||||
echo "$VOLUME_DEV $MOUNT_POINT ext4 defaults,nofail,discard 0 2" >> /etc/fstab
|
||||
fi
|
||||
|
||||
# Create subdirectories
|
||||
mkdir -p "$MOUNT_POINT/forgejo-data"
|
||||
mkdir -p "$MOUNT_POINT/forgejo-db"
|
||||
mkdir -p "$MOUNT_POINT/runner-data"
|
||||
|
||||
# Set permissions (UID 1000 is typically the forgejo user in container)
|
||||
chown -R 1000:1000 "$MOUNT_POINT/forgejo-data"
|
||||
chown -R 999:999 "$MOUNT_POINT/forgejo-db" # postgres user
|
||||
chown -R 1000:1000 "$MOUNT_POINT/runner-data"
|
||||
else
|
||||
echo "WARNING: Volume not found. Please attach volume manually."
|
||||
fi
|
||||
permissions: '0755'
|
||||
|
||||
# Swap configuration script (needed for 1GB RAM)
|
||||
- path: /usr/local/bin/configure-swap.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SWAP_FILE="/swapfile"
|
||||
SWAP_SIZE="2G"
|
||||
|
||||
if [ ! -f "$SWAP_FILE" ]; then
|
||||
echo "Creating $SWAP_SIZE swap file..."
|
||||
fallocate -l $SWAP_SIZE $SWAP_FILE
|
||||
chmod 600 $SWAP_FILE
|
||||
mkswap $SWAP_FILE
|
||||
swapon $SWAP_FILE
|
||||
|
||||
# Add to fstab
|
||||
echo "$SWAP_FILE none swap sw 0 0" >> /etc/fstab
|
||||
|
||||
# Adjust swappiness for better performance
|
||||
echo "vm.swappiness=10" >> /etc/sysctl.conf
|
||||
sysctl vm.swappiness=10
|
||||
|
||||
echo "Swap configured successfully"
|
||||
else
|
||||
echo "Swap file already exists"
|
||||
fi
|
||||
permissions: '0755'
|
||||
|
||||
# Unattended upgrades configuration
|
||||
- path: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
content: |
|
||||
Unattended-Upgrade::Allowed-Origins {
|
||||
"$${distro_id}:$${distro_codename}";
|
||||
"$${distro_id}:$${distro_codename}-security";
|
||||
"$${distro_id}:$${distro_codename}-updates";
|
||||
};
|
||||
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
|
||||
Unattended-Upgrade::MinimalSteps "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
permissions: '0644'
|
||||
|
||||
# Auto-upgrades configuration
|
||||
- path: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
permissions: '0644'
|
||||
|
||||
# ==========================================================================
|
||||
# FORGEJO DOCKER COMPOSE STACK
|
||||
# ==========================================================================
|
||||
|
||||
# Docker Compose file
|
||||
- path: /opt/forgejo/docker-compose.yml
|
||||
content: |
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
forgejo-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: forgejo-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: $${POSTGRES_USER:-forgejo}
|
||||
POSTGRES_PASSWORD: $${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: $${POSTGRES_DB:-forgejo}
|
||||
volumes:
|
||||
- /mnt/forgejo/forgejo-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- forgejo-internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-forgejo} -d $${POSTGRES_DB:-forgejo}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
reservations:
|
||||
memory: 128M
|
||||
|
||||
# Forgejo Git Server
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:11.0.8-rootless
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
forgejo-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
FORGEJO__database__DB_TYPE: postgres
|
||||
FORGEJO__database__HOST: forgejo-db:5432
|
||||
FORGEJO__database__NAME: $${POSTGRES_DB:-forgejo}
|
||||
FORGEJO__database__USER: $${POSTGRES_USER:-forgejo}
|
||||
FORGEJO__database__PASSWD: $${POSTGRES_PASSWORD}
|
||||
FORGEJO__server__DOMAIN: $${FORGEJO_DOMAIN}
|
||||
FORGEJO__server__ROOT_URL: https://$${FORGEJO_DOMAIN}/
|
||||
FORGEJO__server__SSH_DOMAIN: $${FORGEJO_DOMAIN}
|
||||
FORGEJO__server__SSH_PORT: ${git_ssh_port}
|
||||
FORGEJO__server__SSH_LISTEN_PORT: ${git_ssh_port}
|
||||
FORGEJO__server__START_SSH_SERVER: "true"
|
||||
FORGEJO__server__HTTP_PORT: 3000
|
||||
FORGEJO__server__LFS_START_SERVER: "true"
|
||||
FORGEJO__security__INSTALL_LOCK: "true"
|
||||
FORGEJO__security__SECRET_KEY: $${FORGEJO_SECRET_KEY}
|
||||
FORGEJO__security__INTERNAL_TOKEN: $${FORGEJO_INTERNAL_TOKEN}
|
||||
FORGEJO__security__PASSWORD_COMPLEXITY: "lower,upper,digit"
|
||||
FORGEJO__security__MIN_PASSWORD_LENGTH: "12"
|
||||
FORGEJO__oauth2__JWT_SECRET: $${FORGEJO_JWT_SECRET}
|
||||
FORGEJO__service__DISABLE_REGISTRATION: "true"
|
||||
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "false"
|
||||
FORGEJO__service__ENABLE_NOTIFY_MAIL: "false"
|
||||
FORGEJO__actions__ENABLED: "true"
|
||||
FORGEJO__actions__DEFAULT_ACTIONS_URL: "https://code.forgejo.org"
|
||||
FORGEJO__repository__DEFAULT_BRANCH: "main"
|
||||
FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true"
|
||||
FORGEJO__repository__ENABLE_PUSH_CREATE_ORG: "true"
|
||||
FORGEJO__lfs__PATH: /data/lfs
|
||||
FORGEJO__webhook__ALLOWED_HOST_LIST: "private"
|
||||
FORGEJO__webhook__SKIP_TLS_VERIFY: "false"
|
||||
FORGEJO__log__MODE: "console"
|
||||
FORGEJO__log__LEVEL: "Info"
|
||||
volumes:
|
||||
- /mnt/forgejo/forgejo-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- forgejo-internal
|
||||
- forgejo-public
|
||||
ports:
|
||||
- "${git_ssh_port}:${git_ssh_port}"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 384M
|
||||
reservations:
|
||||
memory: 192M
|
||||
|
||||
# Caddy Reverse Proxy with auto-SSL and rate limiting
|
||||
caddy:
|
||||
build:
|
||||
context: ./caddy
|
||||
dockerfile: Dockerfile
|
||||
container_name: forgejo-caddy
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
forgejo:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /opt/forgejo/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- forgejo-public
|
||||
environment:
|
||||
FORGEJO_DOMAIN: $${FORGEJO_DOMAIN}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 64M
|
||||
reservations:
|
||||
memory: 32M
|
||||
|
||||
networks:
|
||||
forgejo-internal:
|
||||
driver: bridge
|
||||
internal: true
|
||||
forgejo-public:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
permissions: '0644'
|
||||
|
||||
# Caddy Dockerfile with rate-limit plugin
|
||||
- path: /opt/forgejo/caddy/Dockerfile
|
||||
content: |
|
||||
FROM caddy:2-builder AS builder
|
||||
RUN xcaddy build --with github.com/mholt/caddy-ratelimit
|
||||
|
||||
FROM caddy:2-alpine
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
permissions: '0644'
|
||||
|
||||
# Caddyfile for reverse proxy with rate limiting
|
||||
- path: /opt/forgejo/Caddyfile
|
||||
content: |
|
||||
{
|
||||
order rate_limit before basicauth
|
||||
}
|
||||
|
||||
${domain} {
|
||||
# Rate limiting - 100 requests per minute per IP
|
||||
rate_limit {
|
||||
zone forgejo_zone {
|
||||
key {remote_host}
|
||||
events 100
|
||||
window 1m
|
||||
}
|
||||
}
|
||||
|
||||
reverse_proxy forgejo:3000
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output file /data/access.log {
|
||||
roll_size 10mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
}
|
||||
permissions: '0644'
|
||||
|
||||
# Script to create .env with Terraform-generated secrets
|
||||
- path: /usr/local/bin/setup-forgejo.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
ENV_FILE="/opt/forgejo/.env"
|
||||
|
||||
# Only create if .env doesn't exist (preserves on re-run)
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Writing Forgejo configuration..."
|
||||
|
||||
{
|
||||
echo "# Forgejo configuration (secrets from Terraform)"
|
||||
echo "FORGEJO_DOMAIN=${domain}"
|
||||
echo ""
|
||||
echo "# PostgreSQL"
|
||||
echo "POSTGRES_USER=forgejo"
|
||||
echo "POSTGRES_PASSWORD=${postgres_password}"
|
||||
echo "POSTGRES_DB=forgejo"
|
||||
echo ""
|
||||
echo "# Forgejo Security Keys"
|
||||
echo "FORGEJO_SECRET_KEY=${forgejo_secret_key}"
|
||||
echo "FORGEJO_INTERNAL_TOKEN=${forgejo_internal_token}"
|
||||
echo "FORGEJO_JWT_SECRET=${forgejo_jwt_secret}"
|
||||
} > "$ENV_FILE"
|
||||
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "Configuration saved to $ENV_FILE"
|
||||
else
|
||||
echo ".env already exists, skipping"
|
||||
fi
|
||||
permissions: '0755'
|
||||
|
||||
# DNS wait script - waits for DNS to propagate before starting Caddy
|
||||
- path: /usr/local/bin/wait-for-dns.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
DOMAIN="${domain}"
|
||||
MAX_ATTEMPTS=60
|
||||
ATTEMPT=0
|
||||
|
||||
echo "Waiting for DNS to propagate for $DOMAIN..."
|
||||
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
if host "$DOMAIN" > /dev/null 2>&1; then
|
||||
echo "DNS resolved for $DOMAIN"
|
||||
exit 0
|
||||
fi
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS - waiting 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "WARNING: DNS did not resolve after $MAX_ATTEMPTS attempts"
|
||||
echo "Let's Encrypt certificate may fail - run 'docker compose restart caddy' after DNS propagates"
|
||||
exit 0
|
||||
permissions: '0755'
|
||||
|
||||
# Message of the day
|
||||
- path: /etc/motd
|
||||
content: |
|
||||
|
||||
+---------------------------------------------------------------+
|
||||
| FORGEJO GIT SERVER |
|
||||
| |
|
||||
| Domain: ${domain}
|
||||
| Git SSH Port: ${git_ssh_port}
|
||||
| |
|
||||
| Data location: /mnt/forgejo |
|
||||
| Docker compose: /opt/forgejo |
|
||||
| |
|
||||
| Commands: |
|
||||
| cd /opt/forgejo && docker compose logs -f |
|
||||
| cd /opt/forgejo && docker compose restart |
|
||||
| |
|
||||
+---------------------------------------------------------------+
|
||||
|
||||
permissions: '0644'
|
||||
|
||||
runcmd:
|
||||
# Ensure .ssh directory exists with correct permissions
|
||||
# (authorized_keys is managed by DigitalOcean's native SSH key provisioning)
|
||||
- mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
|
||||
# Ensure sshd_config includes the .d directory (Debian 12 fix)
|
||||
- grep -q 'Include /etc/ssh/sshd_config.d' /etc/ssh/sshd_config || sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
|
||||
|
||||
# Comment out Port in main sshd_config so sshd_config.d takes precedence (Debian 12 fix)
|
||||
- sed -i 's/^Port /#Port /' /etc/ssh/sshd_config
|
||||
|
||||
# Restart SSH with new configuration
|
||||
- systemctl restart sshd
|
||||
|
||||
# Wait for any background apt processes to finish (DO images run apt on boot)
|
||||
- while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do echo "Waiting for dpkg lock..."; sleep 5; done
|
||||
|
||||
# Install prerequisites
|
||||
- apt-get update
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 install -y ca-certificates curl gnupg fail2ban ufw unattended-upgrades apt-listchanges vim git
|
||||
|
||||
# Add Docker's official GPG key and repository
|
||||
- install -m 0755 -d /etc/apt/keyrings
|
||||
- curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
- chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
|
||||
|
||||
# Install Docker from official repository
|
||||
- apt-get update
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
# Enable and start Docker
|
||||
- systemctl enable docker
|
||||
- systemctl start docker
|
||||
|
||||
# Enable and start fail2ban
|
||||
- systemctl enable fail2ban
|
||||
- systemctl restart fail2ban
|
||||
|
||||
# Configure swap (important for 1GB RAM)
|
||||
- /usr/local/bin/configure-swap.sh
|
||||
|
||||
# Mount the volume
|
||||
- /usr/local/bin/mount-volume.sh
|
||||
|
||||
# Fix ownership for Forgejo container (runs as UID 1000)
|
||||
# This must run AFTER all directories are created to ensure correct permissions
|
||||
- chown -R 1000:1000 /mnt/forgejo/forgejo-data
|
||||
|
||||
# Configure firewall
|
||||
- /usr/local/bin/configure-firewall.sh
|
||||
|
||||
# Generate secrets and create .env
|
||||
- /usr/local/bin/setup-forgejo.sh
|
||||
|
||||
# Wait for DNS to propagate before starting Caddy (for Let's Encrypt)
|
||||
- /usr/local/bin/wait-for-dns.sh
|
||||
|
||||
# Start Forgejo stack (build Caddy with rate-limit plugin, pull others)
|
||||
- cd /opt/forgejo && docker compose build caddy
|
||||
- cd /opt/forgejo && docker compose pull forgejo forgejo-db
|
||||
- cd /opt/forgejo && docker compose up -d
|
||||
|
||||
# Enable unattended upgrades
|
||||
- systemctl enable unattended-upgrades
|
||||
- systemctl start unattended-upgrades
|
||||
|
||||
final_message: "Forgejo server fully deployed after $UPTIME seconds. Visit https://${domain}"
|
||||
97
devops/terraform/modules/forgejo/main.tf
Normal file
97
devops/terraform/modules/forgejo/main.tf
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# =============================================================================
|
||||
# Secret Generation
|
||||
# =============================================================================
|
||||
|
||||
resource "random_password" "postgres" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_password" "forgejo_secret_key" {
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_password" "forgejo_internal_token" {
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_password" "forgejo_jwt_secret" {
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Volume (Block Storage)
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_volume" "forgejo" {
|
||||
name = "${var.project_name}-forgejo-${var.environment}"
|
||||
region = var.region
|
||||
size = var.volume_size
|
||||
initial_filesystem_type = "ext4"
|
||||
description = "Forgejo data volume for ${var.project_name}"
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Droplet
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_droplet" "forgejo" {
|
||||
name = "${var.project_name}-forgejo-${var.environment}"
|
||||
size = var.droplet_size
|
||||
image = var.droplet_image
|
||||
region = var.region
|
||||
vpc_uuid = var.vpc_uuid
|
||||
|
||||
ssh_keys = var.ssh_keys
|
||||
backups = var.enable_backups
|
||||
monitoring = true
|
||||
ipv6 = true
|
||||
|
||||
# Pass cloud-config directly without cloudinit_config wrapper
|
||||
# (cloudinit_config MIME multipart format was being ignored by DigitalOcean)
|
||||
user_data = templatefile("${path.module}/cloud-init.yaml.tpl", {
|
||||
ssh_port = var.ssh_port
|
||||
git_ssh_port = var.git_ssh_port
|
||||
vpc_ip_range = var.vpc_ip_range
|
||||
volume_name = "${var.project_name}-forgejo-${var.environment}"
|
||||
domain = var.domain
|
||||
postgres_password = random_password.postgres.result
|
||||
forgejo_secret_key = random_password.forgejo_secret_key.result
|
||||
forgejo_internal_token = random_password.forgejo_internal_token.result
|
||||
forgejo_jwt_secret = random_password.forgejo_jwt_secret.result
|
||||
})
|
||||
|
||||
tags = var.tags
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = false
|
||||
ignore_changes = [user_data]
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Volume Attachment
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_volume_attachment" "forgejo" {
|
||||
droplet_id = digitalocean_droplet.forgejo.id
|
||||
volume_id = digitalocean_volume.forgejo.id
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DNS Record (optional - requires domain to be managed by DigitalOcean)
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_record" "forgejo" {
|
||||
count = var.manage_dns ? 1 : 0
|
||||
|
||||
domain = var.dns_zone
|
||||
type = "A"
|
||||
name = var.dns_record_name
|
||||
value = digitalocean_droplet.forgejo.ipv4_address
|
||||
ttl = 600
|
||||
}
|
||||
34
devops/terraform/modules/forgejo/outputs.tf
Normal file
34
devops/terraform/modules/forgejo/outputs.tf
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
output "droplet_id" {
|
||||
description = "ID of the Forgejo droplet"
|
||||
value = digitalocean_droplet.forgejo.id
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
description = "Public IPv4 address of the Forgejo droplet"
|
||||
value = digitalocean_droplet.forgejo.ipv4_address
|
||||
}
|
||||
|
||||
output "private_ip" {
|
||||
description = "Private IPv4 address of the Forgejo droplet (VPC)"
|
||||
value = digitalocean_droplet.forgejo.ipv4_address_private
|
||||
}
|
||||
|
||||
output "urn" {
|
||||
description = "URN of the Forgejo droplet"
|
||||
value = digitalocean_droplet.forgejo.urn
|
||||
}
|
||||
|
||||
output "volume_id" {
|
||||
description = "ID of the Forgejo volume"
|
||||
value = digitalocean_volume.forgejo.id
|
||||
}
|
||||
|
||||
output "volume_name" {
|
||||
description = "Name of the Forgejo volume"
|
||||
value = digitalocean_volume.forgejo.name
|
||||
}
|
||||
|
||||
output "dns_record_fqdn" {
|
||||
description = "FQDN of the DNS record (if managed)"
|
||||
value = var.manage_dns ? digitalocean_record.forgejo[0].fqdn : null
|
||||
}
|
||||
99
devops/terraform/modules/forgejo/variables.tf
Normal file
99
devops/terraform/modules/forgejo/variables.tf
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "DigitalOcean region"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_uuid" {
|
||||
description = "UUID of the VPC"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_ip_range" {
|
||||
description = "IP range of the VPC (CIDR notation)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_keys" {
|
||||
description = "List of SSH key IDs to add to the droplet"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "droplet_size" {
|
||||
description = "Size slug for the droplet"
|
||||
type = string
|
||||
default = "s-1vcpu-1gb-intel"
|
||||
}
|
||||
|
||||
variable "droplet_image" {
|
||||
description = "Image slug for the droplet"
|
||||
type = string
|
||||
default = "debian-12-x64"
|
||||
}
|
||||
|
||||
variable "volume_size" {
|
||||
description = "Size of the data volume in GB"
|
||||
type = number
|
||||
default = 50
|
||||
}
|
||||
|
||||
variable "ssh_port" {
|
||||
description = "System SSH port (non-standard, VPC only)"
|
||||
type = number
|
||||
default = 52913
|
||||
}
|
||||
|
||||
variable "git_ssh_port" {
|
||||
description = "Git SSH port (public)"
|
||||
type = number
|
||||
default = 2222
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Domain name for Forgejo"
|
||||
type = string
|
||||
default = "qbit.realms.pub"
|
||||
}
|
||||
|
||||
variable "enable_backups" {
|
||||
description = "Enable automated backups"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Tags to apply to resources"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DNS Configuration (optional)
|
||||
# =============================================================================
|
||||
|
||||
variable "manage_dns" {
|
||||
description = "Whether to manage DNS record via DigitalOcean"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "dns_zone" {
|
||||
description = "DNS zone (base domain) managed by DigitalOcean (e.g., realms.pub)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "dns_record_name" {
|
||||
description = "DNS record name (subdomain, e.g., 'qbit' for qbit.realms.pub)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
12
devops/terraform/modules/forgejo/versions.tf
Normal file
12
devops/terraform/modules/forgejo/versions.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = ">= 2.34"
|
||||
}
|
||||
cloudinit = {
|
||||
source = "hashicorp/cloudinit"
|
||||
version = ">= 2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
175
devops/terraform/modules/jump_host/cloud-init.yaml.tpl
Normal file
175
devops/terraform/modules/jump_host/cloud-init.yaml.tpl
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#cloud-config
|
||||
|
||||
# =============================================================================
|
||||
# Jump Host (Bastion) Cloud-Init Configuration
|
||||
# Minimal attack surface - SSH only, no Docker
|
||||
# =============================================================================
|
||||
|
||||
# NOTE: We install packages via runcmd instead of packages: section
|
||||
# because DigitalOcean's base image cloud-init can interfere with the packages module
|
||||
|
||||
# SSH hardening configuration
|
||||
write_files:
|
||||
# SSH daemon configuration
|
||||
- path: /etc/ssh/sshd_config.d/99-hardening.conf
|
||||
content: |
|
||||
# Non-standard port
|
||||
Port ${ssh_port}
|
||||
|
||||
# Authentication hardening
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
|
||||
# Security settings
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
PermitEmptyPasswords no
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding no
|
||||
AllowAgentForwarding no
|
||||
PermitUserEnvironment no
|
||||
MaxSessions 3
|
||||
|
||||
# Session settings
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
|
||||
# Logging
|
||||
LogLevel VERBOSE
|
||||
permissions: '0644'
|
||||
|
||||
# Fail2ban SSH jail configuration
|
||||
- path: /etc/fail2ban/jail.d/sshd.local
|
||||
content: |
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ${ssh_port}
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
backend = systemd
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 3
|
||||
permissions: '0644'
|
||||
|
||||
# UFW configuration script
|
||||
- path: /usr/local/bin/configure-firewall.sh
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Reset UFW
|
||||
ufw --force reset
|
||||
|
||||
# Default policies
|
||||
ufw default deny incoming
|
||||
ufw default deny outgoing
|
||||
|
||||
# Inbound: SSH on non-standard port
|
||||
ufw allow in ${ssh_port}/tcp comment 'SSH'
|
||||
|
||||
# Inbound: VPC traffic
|
||||
ufw allow in from ${vpc_ip_range} comment 'VPC internal'
|
||||
|
||||
# Outbound: Only necessary traffic (matching DO firewall)
|
||||
ufw allow out 53/tcp comment 'DNS'
|
||||
ufw allow out 53/udp comment 'DNS'
|
||||
ufw allow out 80/tcp comment 'HTTP'
|
||||
ufw allow out 443/tcp comment 'HTTPS'
|
||||
ufw allow out 123/udp comment 'NTP'
|
||||
ufw allow out to ${vpc_ip_range} comment 'VPC'
|
||||
|
||||
# Enable logging for security audit
|
||||
ufw logging high
|
||||
|
||||
# Enable UFW
|
||||
ufw --force enable
|
||||
|
||||
echo "Firewall configured successfully"
|
||||
permissions: '0755'
|
||||
|
||||
# Unattended upgrades configuration
|
||||
- path: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
content: |
|
||||
Unattended-Upgrade::Allowed-Origins {
|
||||
"$${distro_id}:$${distro_codename}";
|
||||
"$${distro_id}:$${distro_codename}-security";
|
||||
"$${distro_id}:$${distro_codename}-updates";
|
||||
};
|
||||
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
|
||||
Unattended-Upgrade::MinimalSteps "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
permissions: '0644'
|
||||
|
||||
# Auto-upgrades configuration
|
||||
- path: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
permissions: '0644'
|
||||
|
||||
# Message of the day
|
||||
- path: /etc/motd
|
||||
content: |
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ JUMP HOST / BASTION ║
|
||||
║ ║
|
||||
║ This is a restricted access server. ║
|
||||
║ All connections are logged and monitored. ║
|
||||
║ ║
|
||||
║ Use this host to access internal infrastructure: ║
|
||||
║ ssh -p 52913 root@10.10.0.x ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
|
||||
permissions: '0644'
|
||||
|
||||
# Internal SSH private key (for accessing Forgejo and other internal servers)
|
||||
- path: /root/.ssh/id_ed25519
|
||||
content: |
|
||||
${indent(6, internal_private_key)}
|
||||
permissions: '0600'
|
||||
|
||||
# SSH config for internal servers
|
||||
- path: /root/.ssh/config
|
||||
content: |
|
||||
Host 10.10.0.*
|
||||
StrictHostKeyChecking accept-new
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
User root
|
||||
permissions: '0600'
|
||||
|
||||
runcmd:
|
||||
# Ensure sshd_config includes the .d directory (Debian 12 fix)
|
||||
- grep -q 'Include /etc/ssh/sshd_config.d' /etc/ssh/sshd_config || sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
|
||||
|
||||
# Comment out Port in main sshd_config so sshd_config.d takes precedence (Debian 12 fix)
|
||||
- sed -i 's/^Port /#Port /' /etc/ssh/sshd_config
|
||||
|
||||
# Restart SSH with new configuration
|
||||
- systemctl restart sshd
|
||||
|
||||
# Wait for any background apt processes to finish (DO images run apt on boot)
|
||||
- while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do echo "Waiting for dpkg lock..."; sleep 5; done
|
||||
|
||||
# Install all required packages (more reliable than packages: section on DO images)
|
||||
- apt-get update
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 install -y fail2ban ufw unattended-upgrades apt-listchanges curl vim
|
||||
|
||||
# Enable and start fail2ban
|
||||
- systemctl enable fail2ban
|
||||
- systemctl restart fail2ban
|
||||
|
||||
# Configure firewall
|
||||
- /usr/local/bin/configure-firewall.sh
|
||||
|
||||
# Enable unattended upgrades
|
||||
- systemctl enable unattended-upgrades
|
||||
- systemctl start unattended-upgrades
|
||||
|
||||
final_message: "Jump host initialization complete after $UPTIME seconds"
|
||||
39
devops/terraform/modules/jump_host/main.tf
Normal file
39
devops/terraform/modules/jump_host/main.tf
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# =============================================================================
|
||||
# Jump Host (Bastion) Droplet
|
||||
# =============================================================================
|
||||
|
||||
data "cloudinit_config" "jump_host" {
|
||||
gzip = false
|
||||
base64_encode = false
|
||||
|
||||
part {
|
||||
content_type = "text/cloud-config"
|
||||
content = templatefile("${path.module}/cloud-init.yaml.tpl", {
|
||||
ssh_port = var.ssh_port
|
||||
vpc_ip_range = var.vpc_ip_range
|
||||
internal_private_key = var.internal_private_key
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resource "digitalocean_droplet" "jump_host" {
|
||||
name = "${var.project_name}-jump-${var.environment}"
|
||||
size = var.droplet_size
|
||||
image = var.droplet_image
|
||||
region = var.region
|
||||
vpc_uuid = var.vpc_uuid
|
||||
|
||||
ssh_keys = var.ssh_keys
|
||||
backups = var.enable_backups
|
||||
monitoring = true
|
||||
ipv6 = true
|
||||
|
||||
user_data = data.cloudinit_config.jump_host.rendered
|
||||
|
||||
tags = var.tags
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = false
|
||||
ignore_changes = [user_data] # Don't recreate on cloud-init changes
|
||||
}
|
||||
}
|
||||
19
devops/terraform/modules/jump_host/outputs.tf
Normal file
19
devops/terraform/modules/jump_host/outputs.tf
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
output "droplet_id" {
|
||||
description = "ID of the jump host droplet"
|
||||
value = digitalocean_droplet.jump_host.id
|
||||
}
|
||||
|
||||
output "public_ip" {
|
||||
description = "Public IPv4 address of the jump host"
|
||||
value = digitalocean_droplet.jump_host.ipv4_address
|
||||
}
|
||||
|
||||
output "private_ip" {
|
||||
description = "Private IPv4 address of the jump host (VPC)"
|
||||
value = digitalocean_droplet.jump_host.ipv4_address_private
|
||||
}
|
||||
|
||||
output "urn" {
|
||||
description = "URN of the jump host droplet"
|
||||
value = digitalocean_droplet.jump_host.urn
|
||||
}
|
||||
65
devops/terraform/modules/jump_host/variables.tf
Normal file
65
devops/terraform/modules/jump_host/variables.tf
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "DigitalOcean region"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_uuid" {
|
||||
description = "UUID of the VPC"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_ip_range" {
|
||||
description = "IP range of the VPC (CIDR notation)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_keys" {
|
||||
description = "List of SSH key IDs to add to the droplet"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "droplet_size" {
|
||||
description = "Size slug for the droplet"
|
||||
type = string
|
||||
default = "s-1vcpu-512mb-10gb"
|
||||
}
|
||||
|
||||
variable "droplet_image" {
|
||||
description = "Image slug for the droplet"
|
||||
type = string
|
||||
default = "debian-12-x64"
|
||||
}
|
||||
|
||||
variable "ssh_port" {
|
||||
description = "SSH port (non-standard for security)"
|
||||
type = number
|
||||
default = 49822
|
||||
}
|
||||
|
||||
variable "enable_backups" {
|
||||
description = "Enable automated backups"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Tags to apply to the droplet"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "internal_private_key" {
|
||||
description = "Private SSH key for accessing internal servers (Forgejo, etc.)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
12
devops/terraform/modules/jump_host/versions.tf
Normal file
12
devops/terraform/modules/jump_host/versions.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = ">= 2.34"
|
||||
}
|
||||
cloudinit = {
|
||||
source = "hashicorp/cloudinit"
|
||||
version = ">= 2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
46
devops/terraform/modules/ssh_keys/main.tf
Normal file
46
devops/terraform/modules/ssh_keys/main.tf
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# =============================================================================
|
||||
# Admin SSH Keys (provided by user)
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_ssh_key" "admin" {
|
||||
for_each = var.admin_ssh_public_keys
|
||||
|
||||
name = "${var.project_name}-${var.environment}-${each.key}"
|
||||
public_key = each.value
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Deploy Key (auto-generated for Forgejo Actions CI/CD)
|
||||
# =============================================================================
|
||||
|
||||
resource "tls_private_key" "deploy" {
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
resource "digitalocean_ssh_key" "deploy" {
|
||||
name = "${var.project_name}-${var.environment}-deploy-key"
|
||||
public_key = tls_private_key.deploy.public_key_openssh
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Save deploy key locally for initial setup (optional)
|
||||
# =============================================================================
|
||||
|
||||
resource "local_sensitive_file" "deploy_private_key" {
|
||||
content = tls_private_key.deploy.private_key_openssh
|
||||
filename = "${path.root}/.secrets/deploy_key_${var.environment}"
|
||||
file_permission = "0600"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Internal VPC Key (jump host → internal servers like Forgejo)
|
||||
# =============================================================================
|
||||
|
||||
resource "tls_private_key" "internal" {
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
resource "digitalocean_ssh_key" "internal" {
|
||||
name = "${var.project_name}-${var.environment}-internal-key"
|
||||
public_key = tls_private_key.internal.public_key_openssh
|
||||
}
|
||||
54
devops/terraform/modules/ssh_keys/outputs.tf
Normal file
54
devops/terraform/modules/ssh_keys/outputs.tf
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
output "admin_ssh_key_ids" {
|
||||
description = "IDs of admin SSH keys"
|
||||
value = [for key in digitalocean_ssh_key.admin : key.id]
|
||||
}
|
||||
|
||||
output "deploy_ssh_key_id" {
|
||||
description = "ID of the deploy SSH key"
|
||||
value = digitalocean_ssh_key.deploy.id
|
||||
}
|
||||
|
||||
output "all_ssh_key_ids" {
|
||||
description = "All SSH key IDs (admin + deploy)"
|
||||
value = concat(
|
||||
[for key in digitalocean_ssh_key.admin : key.id],
|
||||
[digitalocean_ssh_key.deploy.id]
|
||||
)
|
||||
}
|
||||
|
||||
output "internal_ssh_key_id" {
|
||||
description = "ID of the internal VPC SSH key"
|
||||
value = digitalocean_ssh_key.internal.id
|
||||
}
|
||||
|
||||
output "forgejo_ssh_key_ids" {
|
||||
description = "SSH key IDs for Forgejo (internal only - access via jump host)"
|
||||
value = [digitalocean_ssh_key.internal.id]
|
||||
}
|
||||
|
||||
output "deploy_key_fingerprint" {
|
||||
description = "Fingerprint of the deploy key"
|
||||
value = digitalocean_ssh_key.deploy.fingerprint
|
||||
}
|
||||
|
||||
output "deploy_private_key" {
|
||||
description = "Private key for deployment (add to Forgejo secrets)"
|
||||
value = tls_private_key.deploy.private_key_openssh
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "deploy_public_key" {
|
||||
description = "Public key for deployment"
|
||||
value = tls_private_key.deploy.public_key_openssh
|
||||
}
|
||||
|
||||
output "internal_private_key" {
|
||||
description = "Private key for jump host → internal servers"
|
||||
value = tls_private_key.internal.private_key_openssh
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "internal_public_key" {
|
||||
description = "Public key for internal server authentication"
|
||||
value = tls_private_key.internal.public_key_openssh
|
||||
}
|
||||
15
devops/terraform/modules/ssh_keys/variables.tf
Normal file
15
devops/terraform/modules/ssh_keys/variables.tf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "admin_ssh_public_keys" {
|
||||
description = "Map of admin SSH public keys (name => public_key)"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
12
devops/terraform/modules/ssh_keys/versions.tf
Normal file
12
devops/terraform/modules/ssh_keys/versions.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = ">= 2.34"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
devops/terraform/modules/vpc/main.tf
Normal file
10
devops/terraform/modules/vpc/main.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# =============================================================================
|
||||
# VPC Resource
|
||||
# =============================================================================
|
||||
|
||||
resource "digitalocean_vpc" "main" {
|
||||
name = var.name
|
||||
region = var.region
|
||||
ip_range = var.ip_range
|
||||
description = var.description
|
||||
}
|
||||
14
devops/terraform/modules/vpc/outputs.tf
Normal file
14
devops/terraform/modules/vpc/outputs.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
output "vpc_id" {
|
||||
description = "ID of the VPC"
|
||||
value = digitalocean_vpc.main.id
|
||||
}
|
||||
|
||||
output "vpc_urn" {
|
||||
description = "URN of the VPC"
|
||||
value = digitalocean_vpc.main.urn
|
||||
}
|
||||
|
||||
output "vpc_ip_range" {
|
||||
description = "IP range of the VPC"
|
||||
value = digitalocean_vpc.main.ip_range
|
||||
}
|
||||
20
devops/terraform/modules/vpc/variables.tf
Normal file
20
devops/terraform/modules/vpc/variables.tf
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
variable "name" {
|
||||
description = "Name of the VPC"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "DigitalOcean region"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ip_range" {
|
||||
description = "IP range for the VPC (CIDR notation)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
description = "Description of the VPC"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
8
devops/terraform/modules/vpc/versions.tf
Normal file
8
devops/terraform/modules/vpc/versions.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = ">= 2.34"
|
||||
}
|
||||
}
|
||||
}
|
||||
144
devops/terraform/outputs.tf
Normal file
144
devops/terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# =============================================================================
|
||||
# VPC Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "vpc_id" {
|
||||
description = "ID of the VPC"
|
||||
value = module.vpc.vpc_id
|
||||
}
|
||||
|
||||
output "vpc_urn" {
|
||||
description = "URN of the VPC"
|
||||
value = module.vpc.vpc_urn
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Jump Host Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "jump_host_id" {
|
||||
description = "ID of the jump host droplet"
|
||||
value = module.jump_host.droplet_id
|
||||
}
|
||||
|
||||
output "jump_host_public_ip" {
|
||||
description = "Public IPv4 address of the jump host"
|
||||
value = module.jump_host.public_ip
|
||||
}
|
||||
|
||||
output "jump_host_private_ip" {
|
||||
description = "Private IPv4 address of the jump host (VPC)"
|
||||
value = module.jump_host.private_ip
|
||||
}
|
||||
|
||||
output "jump_host_ssh_port" {
|
||||
description = "SSH port for the jump host"
|
||||
value = var.jump_host_ssh_port
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "forgejo_droplet_id" {
|
||||
description = "ID of the Forgejo droplet"
|
||||
value = module.forgejo.droplet_id
|
||||
}
|
||||
|
||||
output "forgejo_public_ip" {
|
||||
description = "Public IPv4 address of the Forgejo droplet"
|
||||
value = module.forgejo.public_ip
|
||||
}
|
||||
|
||||
output "forgejo_private_ip" {
|
||||
description = "Private IPv4 address of the Forgejo droplet (VPC)"
|
||||
value = module.forgejo.private_ip
|
||||
}
|
||||
|
||||
output "forgejo_volume_id" {
|
||||
description = "ID of the Forgejo volume"
|
||||
value = module.forgejo.volume_id
|
||||
}
|
||||
|
||||
output "forgejo_ssh_port" {
|
||||
description = "System SSH port for Forgejo (VPC only)"
|
||||
value = var.forgejo_ssh_port
|
||||
}
|
||||
|
||||
output "forgejo_git_ssh_port" {
|
||||
description = "Git SSH port for Forgejo (public)"
|
||||
value = var.forgejo_git_ssh_port
|
||||
}
|
||||
|
||||
output "forgejo_domain" {
|
||||
description = "Domain name for Forgejo"
|
||||
value = var.forgejo_domain
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SSH Key Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "deploy_key_fingerprint" {
|
||||
description = "Fingerprint of the deploy key (for Forgejo Actions)"
|
||||
value = module.ssh_keys.deploy_key_fingerprint
|
||||
}
|
||||
|
||||
output "deploy_private_key" {
|
||||
description = "Private key for Forgejo Actions deployments (store securely!)"
|
||||
value = module.ssh_keys.deploy_private_key
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "internal_public_key" {
|
||||
description = "Public key for internal VPC access (jump host -> internal servers)"
|
||||
value = module.ssh_keys.internal_public_key
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Firewall Outputs
|
||||
# =============================================================================
|
||||
|
||||
output "jump_host_firewall_id" {
|
||||
description = "ID of the jump host firewall"
|
||||
value = module.firewalls.jump_host_firewall_id
|
||||
}
|
||||
|
||||
output "forgejo_firewall_id" {
|
||||
description = "ID of the Forgejo firewall"
|
||||
value = module.firewalls.forgejo_firewall_id
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Connection Info
|
||||
# =============================================================================
|
||||
|
||||
output "ssh_config" {
|
||||
description = "SSH config snippet for connecting to the infrastructure"
|
||||
value = <<-EOT
|
||||
# Add to ~/.ssh/config (Windows: C:\Users\<username>\.ssh\config)
|
||||
|
||||
Host realms-jump
|
||||
HostName ${module.jump_host.public_ip}
|
||||
Port ${var.jump_host_ssh_port}
|
||||
User root
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
|
||||
Host realms-forgejo
|
||||
HostName ${module.forgejo.private_ip}
|
||||
Port ${var.forgejo_ssh_port}
|
||||
User root
|
||||
ProxyJump realms-jump
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
EOT
|
||||
}
|
||||
|
||||
output "dns_record_info" {
|
||||
description = "DNS record to create for Forgejo"
|
||||
value = <<-EOT
|
||||
Create an A record:
|
||||
Name: qbit (for qbit.realms.pub)
|
||||
Value: ${module.forgejo.public_ip}
|
||||
TTL: 300
|
||||
EOT
|
||||
}
|
||||
76
devops/terraform/terraform.tfvars.example
Normal file
76
devops/terraform/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# =============================================================================
|
||||
# DigitalOcean Terraform Configuration
|
||||
# =============================================================================
|
||||
# Copy this file to terraform.tfvars and fill in your values
|
||||
# NEVER commit terraform.tfvars to version control!
|
||||
#
|
||||
# Set the DO token via environment variable:
|
||||
# export TF_VAR_do_token="dop_v1_your_token_here"
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Project Configuration
|
||||
# =============================================================================
|
||||
|
||||
project_name = "realms"
|
||||
environment = "production"
|
||||
region = "nyc3"
|
||||
|
||||
# =============================================================================
|
||||
# VPC Configuration
|
||||
# =============================================================================
|
||||
|
||||
vpc_ip_range = "10.10.0.0/16"
|
||||
|
||||
# =============================================================================
|
||||
# SSH Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Add your admin SSH public key(s) here
|
||||
# Generate with: ssh-keygen -t ed25519 -C "your_email@example.com"
|
||||
admin_ssh_public_keys = {
|
||||
# "admin-name" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlczdk2KKjY2CyYV1Ql8enjRn8gpBBgSLmbbCUyG5Qs admin@doom.tube"
|
||||
}
|
||||
|
||||
# SSH ports (non-standard for security)
|
||||
jump_host_ssh_port = 49822 # Jump host public SSH
|
||||
forgejo_ssh_port = 52913 # Forgejo system SSH (VPC only)
|
||||
forgejo_git_ssh_port = 2222 # Forgejo Git SSH (public)
|
||||
|
||||
# =============================================================================
|
||||
# Jump Host Configuration
|
||||
# =============================================================================
|
||||
|
||||
jump_host_size = "s-1vcpu-512mb-10gb" # $4/mo
|
||||
jump_host_image = "debian-12-x64"
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Configuration
|
||||
# =============================================================================
|
||||
|
||||
forgejo_droplet_size = "s-1vcpu-1gb-intel" # $7/mo - 1GB RAM, 1 Intel vCPU
|
||||
forgejo_droplet_image = "debian-12-x64"
|
||||
forgejo_volume_size = 50 # GB for repositories and LFS
|
||||
forgejo_domain = "qbit.realms.pub"
|
||||
|
||||
# =============================================================================
|
||||
# DNS Configuration (requires domain to be managed by DigitalOcean)
|
||||
# =============================================================================
|
||||
|
||||
# Set to true to automatically create/update A record for Forgejo
|
||||
manage_dns = true
|
||||
|
||||
# Base domain managed by DigitalOcean DNS
|
||||
dns_zone = "realms.pub"
|
||||
|
||||
# =============================================================================
|
||||
# Backup Configuration
|
||||
# =============================================================================
|
||||
|
||||
enable_droplet_backups = true
|
||||
|
||||
# =============================================================================
|
||||
# Additional Tags
|
||||
# =============================================================================
|
||||
|
||||
tags = []
|
||||
150
devops/terraform/variables.tf
Normal file
150
devops/terraform/variables.tf
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# =============================================================================
|
||||
# Provider Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "do_token" {
|
||||
description = "DigitalOcean API token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Project Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name used for resource naming"
|
||||
type = string
|
||||
default = "realms"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name (production, staging, development)"
|
||||
type = string
|
||||
default = "production"
|
||||
|
||||
validation {
|
||||
condition = contains(["production", "staging", "development"], var.environment)
|
||||
error_message = "Environment must be one of: production, staging, development."
|
||||
}
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "DigitalOcean region"
|
||||
type = string
|
||||
default = "nyc3"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# VPC Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "vpc_ip_range" {
|
||||
description = "IP range for the VPC (CIDR notation)"
|
||||
type = string
|
||||
default = "10.10.0.0/16"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SSH Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "admin_ssh_public_keys" {
|
||||
description = "Map of admin SSH public keys (name => public_key)"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "jump_host_ssh_port" {
|
||||
description = "SSH port for the jump host (non-standard for security)"
|
||||
type = number
|
||||
default = 49822
|
||||
}
|
||||
|
||||
variable "forgejo_ssh_port" {
|
||||
description = "System SSH port for Forgejo (VPC only, non-standard)"
|
||||
type = number
|
||||
default = 52913
|
||||
}
|
||||
|
||||
variable "forgejo_git_ssh_port" {
|
||||
description = "Git SSH port for Forgejo (public)"
|
||||
type = number
|
||||
default = 2222
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Jump Host Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "jump_host_size" {
|
||||
description = "Size slug for the jump host droplet"
|
||||
type = string
|
||||
default = "s-1vcpu-512mb-10gb"
|
||||
}
|
||||
|
||||
variable "jump_host_image" {
|
||||
description = "Image slug for the jump host droplet"
|
||||
type = string
|
||||
default = "debian-12-x64"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Forgejo Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "forgejo_droplet_size" {
|
||||
description = "Size slug for the Forgejo droplet"
|
||||
type = string
|
||||
default = "s-1vcpu-1gb-intel"
|
||||
}
|
||||
|
||||
variable "forgejo_droplet_image" {
|
||||
description = "Image slug for the Forgejo droplet"
|
||||
type = string
|
||||
default = "debian-12-x64"
|
||||
}
|
||||
|
||||
variable "forgejo_volume_size" {
|
||||
description = "Size of the Forgejo data volume in GB"
|
||||
type = number
|
||||
default = 50
|
||||
}
|
||||
|
||||
variable "forgejo_domain" {
|
||||
description = "Domain name for Forgejo (e.g., qbit.realms.pub)"
|
||||
type = string
|
||||
default = "qbit.realms.pub"
|
||||
}
|
||||
|
||||
variable "manage_dns" {
|
||||
description = "Whether to manage DNS records via DigitalOcean"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "dns_zone" {
|
||||
description = "DNS zone (base domain) managed by DigitalOcean (e.g., realms.pub)"
|
||||
type = string
|
||||
default = "realms.pub"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Backup Configuration
|
||||
# =============================================================================
|
||||
|
||||
variable "enable_droplet_backups" {
|
||||
description = "Enable automated droplet backups"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Tags
|
||||
# =============================================================================
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags to apply to resources"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
34
devops/terraform/versions.tf
Normal file
34
devops/terraform/versions.tf
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# =============================================================================
|
||||
# Terraform and Provider Version Constraints
|
||||
# =============================================================================
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = "~> 2.34"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = "~> 2.4"
|
||||
}
|
||||
cloudinit = {
|
||||
source = "hashicorp/cloudinit"
|
||||
version = "~> 2.3"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "digitalocean" {
|
||||
token = var.do_token
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue