From b00d69e1010118145841bfdeb7a54c776210914c Mon Sep 17 00:00:00 2001 From: Neil Borromeo Date: Thu, 27 Aug 2026 11:26:22 +0800 Subject: [PATCH] initial --- .forgejo/workflows/deploy.yml | 48 +++++++++++++++++ NOTES.md | 23 +++++++++ deploy/deploy.sh | 8 +++ deploy/site.yml | 34 ++++++++++++ deploy/templates/app.env.j2 | 1 + deploy/templates/nginx_web1.conf.j2 | 80 +++++++++++++++++++++++++++++ deploy/vars/vault.yml | 2 + main.tf | 46 +++++++++++++++++ 8 files changed, 242 insertions(+) create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 NOTES.md create mode 100644 deploy/deploy.sh create mode 100644 deploy/site.yml create mode 100644 deploy/templates/app.env.j2 create mode 100644 deploy/templates/nginx_web1.conf.j2 create mode 100644 deploy/vars/vault.yml create mode 100644 main.tf diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..fe60217 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,48 @@ +--- +name: Validate and deploy + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - closed + +jobs: + validate-and-deploy: + runs-on: docker + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + + steps: + - name: Check out repository + uses: https://data.forgejo.org/actions/checkout@v6 + + - name: Scan secrets + run: gitleaks detect --source . --redact --no-banner + + - name: Check Terraform formatting + run: terraform fmt -check -recursive + + - name: Initialize Terraform + run: terraform init -backend=false -input=false + + - name: Validate Terraform + run: terraform validate + + - name: Plan Terraform + run: terraform plan -input=false -no-color -out=tfplan + + - name: Lint Ansible + run: ansible-lint deploy/site.yml + + - name: Deploy + if: ${{ forgejo.event.action == 'closed' && forgejo.event.pull_request.merged == true }} + env: + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: bash deploy/deploy.sh diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..68e6147 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,23 @@ +* Changes to terraform/ansible* +I noticed an extra set of brackets that VS Code didn't flag. Running a quick terraform validate catches small syntax issues like this before they cause deployment errors. + +In main.tf, I scoped cidr_blocks down from 0.0.0.0/0 to a sample subnet (180.191.0.0/16). While still somewhat broad to accommodate dynamic ISP leases that reset every 48 hours, it significantly reduces the initial attack surface. + +Alternative Access & Security Models +Rather than leaving SSH exposed, the best practice—even for small instances—is to remove port 22 from the security group entirely. A few options to consider: + +Tailscale / Headscale (Recommended): Uses Tailscale SSH to manage access securely. Users authenticate via Identity Providers (IdPs) like Google or GitHub, while Headscale handles key rotations, ACLs, and permissions. I personally use this setup in production for small cloud workloads; it provides excellent ACL control and a clean management interface. + +AWS SSM Session Manager: Allows secure CLI and console access via the AWS agent without open inbound ports, though executing local tools like Ansible internally requires additional setup. + +Cloudflare Tunnels: Useful for routing developer traffic directly to the instance without exposing public IPs, especially in environments requiring DNS over tunnels. + +I prefer the Tailscale/Headscale combination as a modern, manageable solution that keeps port 22 completely closed to the internet. + +Handling Port 80 Ingress +I added port 80 to the ingress rules to support standard HTTP traffic. While this opens a public surface area, network risks can easily be mitigated at the Linux OS level (e.g., hardening TCP kernel parameters against SYN floods). In practice, Nginx will use port 80 primarily to issue 301 permanent redirects to HTTPS (port 443). + + +*Scenario Response: TinyCommerce App Intermittent Health Check Failures* +Part 3 — Incident note (~30 min, written) +A Scenario response for the TinyCommerce app with intermittent health check failures. Detection relies on baseline health check monitoring. The issue would likely present as a 5xx HTTP error specifically a 502 Bad Gateway indicating an application-level problem within the TinyCommerce app. Assuming the application has a reliable deployment pipeline, I would start by investigating the error logs. For example, if the app crashes whenever it attempts a file write, I would check system monitoring and host logs for clear indicators like disk space exhaustion. On small instances, unrotated application logs, Docker container logs, or bloated volumes are almost always the root cause of disk exhaustion and subsequent crashes. \ No newline at end of file diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..10026f2 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +terraform apply -input=false -auto-approve tfplan +ansible-playbook -i inventory deploy/site.yml +echo "Deployed." diff --git a/deploy/site.yml b/deploy/site.yml new file mode 100644 index 0000000..764a35d --- /dev/null +++ b/deploy/site.yml @@ -0,0 +1,34 @@ +--- +- name: Deploy TinyCommerce + hosts: app + become: true + vars_files: + - vars/vault.yml + + tasks: + - name: Install nginx + ansible.builtin.apt: + name: nginx + state: present + + - name: Write app config + ansible.builtin.template: + src: app.env.j2 + dest: /etc/tinycommerce/app.env + mode: "0600" + + - name: Write nginx config + ansible.builtin.template: + src: nginx_web1.conf.j2 + dest: /etc/nginx/sites-enabled/nginx_web1.conf + mode: "0644" + + - name: Restart nginx + ansible.builtin.service: + name: nginx + state: restarted + + - name: Restart app service + ansible.builtin.service: + name: tinycommerce + state: restarted diff --git a/deploy/templates/app.env.j2 b/deploy/templates/app.env.j2 new file mode 100644 index 0000000..11e89b0 --- /dev/null +++ b/deploy/templates/app.env.j2 @@ -0,0 +1 @@ +DB_PASSWORD={{ db_password }} diff --git a/deploy/templates/nginx_web1.conf.j2 b/deploy/templates/nginx_web1.conf.j2 new file mode 100644 index 0000000..bcd7a94 --- /dev/null +++ b/deploy/templates/nginx_web1.conf.j2 @@ -0,0 +1,80 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name www.tinycommerce.com; + set $maintenance_site_key "tinycommerce.com"; + set $base /var/www/html/tinycommerce.com; + root /var/www/html/tinycommerce.com/public; + + ssl_certificate /etc/letsencrypt/live/tinycommerce.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/tinycommerce.com/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/tinycommerce.com/chain.pem; + + include snippets/cloudflare-realip.conf; + include snippets/security.conf; + include snippets/maintenance-mode.conf; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + index index.php; + + location / { + if (-f /etc/nginx/maintenance/enabled) { + return 503; + } + try_files $uri $uri/ /index.php?$query_string; + } + + include snippets/general.conf; + include snippets/wordpress.conf; + client_max_body_size 0; + + + location ~ \.php$ { + if (-f /etc/nginx/maintenance/enabled) { + return 503; + } + fastcgi_pass unix:/var/run/php/php-wordpress.sock; + include snippets/php-wordpress-fastcgi.conf; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name tinycommerce.com; + set $maintenance_site_key "tinycommerce.com"; + + ssl_certificate /etc/letsencrypt/live/tinycommerce.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/tinycommerce.com/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/tinycommerce.com/chain.pem; + include snippets/cloudflare-realip.conf; + include snippets/maintenance-mode.conf; + + location / { + if (-f /etc/nginx/maintenance/enabled) { + return 503; + } + return 301 https://www.tinycommerce.com$request_uri; + } +} + +server { + listen 80; + listen [::]:80; + server_name tinycommerce.com www.tinycommerce.com; + set $maintenance_site_key "tinycommerce.com"; + include snippets/letsencrypt.conf; + include snippets/cloudflare-realip.conf; + include snippets/maintenance-mode.conf; + + location / { + if (-f /etc/nginx/maintenance/enabled) { + return 503; + } + return 301 https://www.tinycommerce.com$request_uri; + } +} diff --git a/deploy/vars/vault.yml b/deploy/vars/vault.yml new file mode 100644 index 0000000..77916ba --- /dev/null +++ b/deploy/vars/vault.yml @@ -0,0 +1,2 @@ +--- +db_password: "{{ lookup('ansible.builtin.env', 'DB_PASSWORD') }}" diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..ba20c54 --- /dev/null +++ b/main.tf @@ -0,0 +1,46 @@ +resource "aws_security_group" "app" { + name = "tinycommerce-app-sg" + description = "Allow app and SSH traffic" + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["180.191.0.0/16"] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_instance" "app" { + ami = "ami-0abcdef1234567890" + instance_type = "t3.small" + security_groups = [aws_security_group.app.name] + + tags = { + Name = "tinycommerce-app-01" + } + + lifecycle { + prevent_destroy = true + } +}