diff --git a/.gitignore b/.gitignore index 2eea525..c612d23 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -.env \ No newline at end of file +.env + +*.key +*.pem + +venv/ \ No newline at end of file diff --git a/ansible/.ansible-lint b/ansible/.ansible-lint new file mode 100644 index 0000000..39c776c --- /dev/null +++ b/ansible/.ansible-lint @@ -0,0 +1,4 @@ +--- + +skip_list: + - name[template] diff --git a/ansible/.gitignore b/ansible/.gitignore index 6b7904c..84408d0 100644 --- a/ansible/.gitignore +++ b/ansible/.gitignore @@ -1 +1,4 @@ -github.key \ No newline at end of file +github.key +hacker_theme.zip +# todo: remove this line +challenge.py \ No newline at end of file diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..22cb645 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,110 @@ +# Deployment with Ansible + +## Getting started + +### Installation + +```bash +python3 -m pip install ansible +``` + +### Inventory + +Update the [inventories/](inventories/) folder with your SSH information. + +Configuration your ssh client configuration at `~/.ssh/config`. + +```bash +Host heroctf-ctfd + User root + Hostname XXX + Port 22 + IdentityFile /home/xanhacks/.ssh/heroctf/linode + +Host heroctf-challenge-1 + User root + Hostname XXX + Port 22 + IdentityFile /home/xanhacks/.ssh/heroctf/linode + +Host heroctf-challenge-2 + User root + Hostname XXX + Port 22 + IdentityFile /home/xanhacks/.ssh/heroctf/linode + +# [...] +``` + +Check the SSH connection is working and accept all SSH fingerprints. + +```bash +ansible -i inventories/dev -m ping all -v +ansible -i inventories/prod -m ping all -v +``` + +## Roles + +### Prerequisites + +#### Features + +1. Update APT cache & install usefull packages (`git`, `curl`, `vim`...). +2. Configure the VM timezone, hostname and create a low privileged account. +3. Install and start `docker` & `docker-compose`. + +### CTFd + +#### Features + +1. Clone CTFd at a specific version. +2. Overwrite the default `docker-compose.yml` and `http.conf` with enhanced configuration (HTTPs and better performance). +3. Upload HTTPs certificates. +4. (optional) Extract custom CTFd theme. +5. (optional) Enable first blood Discord webhook. + +> Generate your HTTPs certificates using certbot: `certbot certonly --manual --preferred-challenges dns -d 'ctf.heroctf.fr' --work-dir $(pwd) --logs-dir $(pwd) --config-dir $(pwd)` + +#### Setup & Run + +1. Configure the `ctfd` role at [./group_vars/ctfd.yml](./group_vars/ctfd.yml). +2. Add `fullchain.pem` and `privkey.pem` to [./roles/ctfd/files/certs](./roles/ctfd/files/certs). +3. Configure the [./roles/ctfd/files/.env.sample](./roles/ctfd/files/.env.sample) file to `.env`. +4. (optional) Add CTFd Theme `hacker_theme.zip` at [./roles/ctfd/files/](./roles/ctfd/files/). + +```bash +ansible-playbook ctfd.yml -i inventories/dev +ansible-playbook ctfd.yml -i inventories/prod +``` + +### Challenges + +#### Features + +1. Upload and configure the Github SSH private key to see private repositories. +2. Clone the challenge repository. + +> You can create a Github SSH key for a repository under the `Settings > Deploy keys` tab, then click on `Add deploy key`. + +#### Setup & Run + +1. Configure `challenges` role at [./group_vars/challenges.yml](./group_vars/challenges.yml). +2. Add your Github private SSH key to [./roles/challenges/files/](./roles/challenges/files/) under the name `github.key`. + +```bash +ansible-playbook challenges.yml -i inventories/dev +ansible-playbook challenges.yml -i inventories/prod +``` + +## Linter + +Use [ansible-lint](https://github.com/ansible/ansible-lint) to checks playbooks for practices and behavior that could potentially be improved. + +```bash +python3 -m pip install ansible-lint +ansible-lint +``` + +## About + +Tested on debian 12. \ No newline at end of file diff --git a/ansible/group_vars/ctfd.yml b/ansible/group_vars/ctfd.yml index 998044a..e78650f 100644 --- a/ansible/group_vars/ctfd.yml +++ b/ansible/group_vars/ctfd.yml @@ -1 +1,5 @@ ctfd_version: "3.7.3" +ctfd_install_theme: true +ctfd_theme_name: "hacker_theme.zip" +ctfd_domain: "ctf.heroctf.fr" +ctfd_install_discord_webhook: false diff --git a/ansible/roles/challenges/tasks/main.yml b/ansible/roles/challenges/tasks/main.yml index bb18158..f451372 100644 --- a/ansible/roles/challenges/tasks/main.yml +++ b/ansible/roles/challenges/tasks/main.yml @@ -23,6 +23,7 @@ - name: "Clone HeroCTF challenges' repository to '/home/{{ ctf_user }}/challenges'" ansible.builtin.git: repo: "{{ challenges_git_url }}" + version: "main" dest: "/home/{{ ctf_user }}/challenges" accept_hostkey: true force: true diff --git a/ansible/roles/ctfd/files/.env.sample b/ansible/roles/ctfd/files/.env.sample new file mode 100644 index 0000000..f16a59f --- /dev/null +++ b/ansible/roles/ctfd/files/.env.sample @@ -0,0 +1,2 @@ +SECRET_KEY=xxxxxxxxxxxxxxxxx +DISCORD_WEBHOOK_URL=https://xxxxxxxxxxxx \ No newline at end of file diff --git a/ansible/roles/ctfd/files/certs/.gitkeep b/ansible/roles/ctfd/files/certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ansible/roles/ctfd/tasks/main.yml b/ansible/roles/ctfd/tasks/main.yml new file mode 100644 index 0000000..89a1117 --- /dev/null +++ b/ansible/roles/ctfd/tasks/main.yml @@ -0,0 +1,103 @@ +--- + +- name: "Clone CTFd at version '{{ ctfd_version }}' to '/home/{{ ctf_user }}/CTFd'" + ansible.builtin.git: + repo: "https://github.com/CTFd/CTFd.git" + dest: "/home/{{ ctf_user }}/CTFd" + version: "{{ ctfd_version }}" + force: true + become: true + become_user: "{{ ctf_user }}" + +- name: "Copy '.env' to '/home/{{ ctf_user }}/CTFd/.env'" + ansible.builtin.copy: + src: "files/.env" + dest: "/home/{{ ctf_user }}/CTFd/.env" + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0644" + become: true + +- name: "Copy 'docker-compose.yml.j2' to '/home/{{ ctf_user }}/CTFd/docker-compose.yml'" + ansible.builtin.template: + src: "docker-compose.yml.j2" + dest: "/home/{{ ctf_user }}/CTFd/docker-compose.yml" + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0644" + become: true + become_user: "{{ ctf_user }}" + +- name: "Copy 'http.conf.j2' to '/home/{{ ctf_user }}/CTFd/conf/nginx/http.conf'" + ansible.builtin.template: + src: "http.conf.j2" + dest: "/home/{{ ctf_user }}/CTFd/conf/nginx/http.conf" + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0644" + become: true + +- name: "Create a 'certs' directory for HTTPs certificates" + ansible.builtin.file: + path: "/home/{{ ctf_user }}/CTFd/conf/nginx/certs/" + state: directory + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0775" + become: true + +- name: "Copy 'fullchain.pem' & 'privkey.pem' to '/home/{{ ctf_user }}/CTFd/conf/nginx/certs/'" + ansible.builtin.copy: + src: "files/certs/{{ item }}" + dest: "/home/{{ ctf_user }}/CTFd/conf/nginx/certs/{{ item }}" + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0644" + loop: + - fullchain.pem + - privkey.pem + become: true + +- name: Copy challenge.py # with first blood bot + ansible.builtin.copy: + src: "files/challenge.py" + dest: "/home/{{ ctf_user }}/CTFd/CTFd/api/v1/challenges.py" + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0644" + when: ctfd_install_discord_webhook + become: true + +- name: Extract the custom theme + ansible.builtin.unarchive: + src: "files/{{ ctfd_theme_name }}" + dest: "/home/{{ ctf_user }}/CTFd/CTFd/themes/" + become: true + become_user: "{{ ctf_user }}" + when: ctfd_install_theme + +- name: "Create a 'data' directory for docker-compose volumes" + ansible.builtin.file: + path: "/home/{{ ctf_user }}/CTFd/data/" + state: directory + owner: "{{ ctf_user }}" + group: "{{ ctf_user }}" + mode: "0777" + recurse: true + become: true + +- name: "Add 'psycopg2-binary' to requirements.txt for PostgreSQL connection" + ansible.builtin.shell: "echo 'psycopg2-binary' >> requirements.txt" + args: + chdir: "/home/{{ ctf_user }}/CTFd" + become: true + become_user: "{{ ctf_user }}" + +- name: Start CTFd docker-compose + ansible.builtin.shell: "docker compose up -d --build" + args: + chdir: "/home/{{ ctf_user }}/CTFd" + register: ctfd_docker_compose_output + changed_when: "'recreated' in ctfd_docker_compose_output.stdout or 'Pulling' in ctfd_docker_compose_output.stdout" + become: true + become_user: "{{ ctf_user }}" diff --git a/ansible/roles/ctfd/templates/docker-compose.yml.j2 b/ansible/roles/ctfd/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..a17b256 --- /dev/null +++ b/ansible/roles/ctfd/templates/docker-compose.yml.j2 @@ -0,0 +1,65 @@ +--- + +services: + ctfd: + build: . + user: root + restart: always + environment: + - UPLOAD_FOLDER=/var/uploads + - DATABASE_URL=postgresql://ctfd:ctfd@db/ctfd + - REDIS_URL=redis://redis:6379 + - WORKERS={{ ansible_processor_vcpus }} + - LOG_FOLDER=/var/log/CTFd + - ACCESS_LOG=- + - ERROR_LOG=- + - REVERSE_PROXY=true + - SECRET_KEY=${SECRET_KEY} + - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} # Discord Webhook for first blood bot + volumes: + - ./data/CTFd/logs:/var/log/CTFd + - ./data/CTFd/uploads:/var/uploads + - .:/opt/CTFd:ro + depends_on: + - db + networks: + default: + internal: + + nginx: + image: nginx:1.27-alpine + restart: always + volumes: + - ./conf/nginx/http.conf:/etc/nginx/nginx.conf + - ./conf/nginx/certs:/etc/nginx/certs + ports: + - 443:443 + depends_on: + - ctfd + networks: + default: + + db: + image: postgres:17-alpine + restart: always + environment: + - POSTGRES_USER=ctfd + - POSTGRES_PASSWORD=ctfd + - POSTGRES_DB=ctfd + volumes: + - ./data/postgres:/var/lib/postgresql/data + networks: + internal: + + redis: + image: redis:7-alpine + restart: always + volumes: + - ./data/redis:/data + networks: + internal: + +networks: + default: + internal: + internal: true diff --git a/ansible/roles/ctfd/templates/http.conf.j2 b/ansible/roles/ctfd/templates/http.conf.j2 new file mode 100644 index 0000000..c3113ad --- /dev/null +++ b/ansible/roles/ctfd/templates/http.conf.j2 @@ -0,0 +1,47 @@ +worker_processes auto; + +events { + worker_connections 2048; +} + +http { + upstream app_servers { + server ctfd:8000; + } + + server { + listen 443 ssl; + + server_name {{ ctfd_domain}}; + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + gzip on; + client_max_body_size 512m; + + # Handle Server Sent Events for Notifications + location /events { + proxy_pass http://app_servers; + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + + # Proxy connections to the application servers + location / { + proxy_pass http://app_servers; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + } + } +} diff --git a/terraform/firewall.tf b/terraform/firewall.tf index f157a3c..172e5a8 100644 --- a/terraform/firewall.tf +++ b/terraform/firewall.tf @@ -42,10 +42,10 @@ resource "linode_firewall" "ctfd_firewall" { } inbound { - label = "allow-http-https" + label = "allow-https" action = "ACCEPT" protocol = "TCP" - ports = "80,443" + ports = "443" ipv4 = ["0.0.0.0/0"] ipv6 = ["::/0"] }