diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b24358c..6023103 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,16 @@ on: workflow_dispatch: jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - name: Check out the repository + uses: actions/checkout@v4 + - name: Run ansible-lint + uses: ansible/ansible-lint@v25 + with: + args: "-c ansible-lint.yml" + encoding: runs-on: ubuntu-24.04 steps: diff --git a/Folder.DotSettings b/Folder.DotSettings new file mode 100644 index 0000000..385f3df --- /dev/null +++ b/Folder.DotSettings @@ -0,0 +1,8 @@ + + True + True + True + True + True + True + True \ No newline at end of file diff --git a/README.md b/README.md index d18859b..e287c5d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Host specification - [codingteam.org.ru][hosts/ctor] - [cthulhu-3][hosts/cthulhu-3] - [omnissiah][hosts/omnissiah] +- [xmpp2][hosts.xmpp2] Documentation ------------- @@ -40,6 +41,7 @@ The license indication in the project's sources is compliant with the [REUSE spe [codingteam.org.ru]: https://codingteam.org.ru [devops]: https://ru.wikipedia.org/wiki/DevOps [docs.license]: LICENSES/MIT.txt +[host.xmpp2]: xmpp2/README.md [hosts/cthulhu-3]: cthulhu-3/Host.md [hosts/ctor]: ctor/Host.md [hosts/omnissiah]: omnissiah/Host.md diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..7bd1cfb --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,10 @@ +version = 1 +SPDX-PackageName = "devops" +SPDX-PackageSupplier = "codingteam/devops contributors " +SPDX-PackageDownloadLocation = "https://github.com/codingteam/devops" + +[[annotations]] +path = "**.DotSettings" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Friedrich von Never " +SPDX-License-Identifier = "MIT" diff --git a/ansible-lint.yml b/ansible-lint.yml new file mode 100644 index 0000000..87c7275 --- /dev/null +++ b/ansible-lint.yml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +exclude_paths: + - .github/ # no Ansible plays in there + - xmpp2/default.yml # just a list of other files diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..5b744d9 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +collections: + - name: ansible.posix + version: 1.5.4 + - name: community.docker + version: 3.7.0 + - name: community.general + version: 8.3.0 diff --git a/xmpp2/.gitignore b/xmpp2/.gitignore new file mode 100644 index 0000000..fb781b9 --- /dev/null +++ b/xmpp2/.gitignore @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +hosts.ini + +vars/secrets.yml +vars/vars.yml diff --git a/xmpp2/README.md b/xmpp2/README.md new file mode 100644 index 0000000..5fcef69 --- /dev/null +++ b/xmpp2/README.md @@ -0,0 +1,39 @@ + + +xmpp2 host +========== +- **Provider:** Digital Ocean +- **OS**: Ubuntu 24.04 + +How to Deploy +------------- +1. Copy `hosts.example.ini` to `hosts.ini`, fix the host connection details if needed. +2. Copy `vars/vars.example.yml` to `vars/vars.yml` and adjust it accordingly. +3. Copy `vars/secrets.example.yml` to `vars/secrets.yml` and adjust it accordingly. +4. `ansible-vault encrypt vars/secrets.yml` +5. To **check the results** without applying, run `ansible-playbook --ask-vault-pass --ask-become-pass --check --diff default.yml`. + + To **deploy**, run `ansible-playbook --ask-vault-pass --ask-become-pass default.yml`. + +If on Windows, feel free to use scripts `ansible-vault.ps1`, `ansible-playbook.ps1` as a substitute to use Ansible from WSL. + +If running deployment for the first time, then run `ansible-playbook --ask-vault-pass auth.yml` to set up the user accounts and access properly. + +Standard Operating Procedures +----------------------------- + +### Dump Database Backup for LogList +```console +$ docker exec -i loglist.postgresql pg_dump -d loglist -U postgres -F custom --no-acl > loglist.dmp +``` + +### Restore Database Backup for LogList +```console +$ docker cp loglist.dmp loglist.postgresql:/loglist.dmp +$ docker exec -i loglist.postgresql pg_restore -d loglist -U loglist --clean --no-owner -1 /loglist.dmp +$ docker exec -i loglist.postgresql rm /loglist.dmp +``` diff --git a/xmpp2/ansible-playbook.ps1 b/xmpp2/ansible-playbook.ps1 new file mode 100644 index 0000000..4e40c4e --- /dev/null +++ b/xmpp2/ansible-playbook.ps1 @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +wsl --distribution Ubuntu ansible-playbook --inventory hosts.ini @args -e 'ansible_ssh_pipelining=True' diff --git a/xmpp2/ansible-vault.ps1 b/xmpp2/ansible-vault.ps1 new file mode 100644 index 0000000..f818024 --- /dev/null +++ b/xmpp2/ansible-vault.ps1 @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +wsl --distribution Ubuntu ansible-vault @args diff --git a/xmpp2/auth.yml b/xmpp2/auth.yml new file mode 100644 index 0000000..157f2bf --- /dev/null +++ b/xmpp2/auth.yml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Set up the users and authentication + hosts: xmpp2 + become: true + + vars_files: + - secrets.yml + - vars.yml + + handlers: + - name: Reload sshd + ansible.builtin.service: + name: ssh + state: reloaded + + tasks: + - name: Ensure a group exists for those who can connect with SSH + ansible.builtin.group: + name: sshuser + + - name: Ensure a user exists and can SSH into the machine + ansible.builtin.user: + name: '{{ user.name }}' + shell: /bin/bash + groups: ['sudo', 'sshuser'] + append: true + home: '/home/{{ user.name }}' + password_lock: false + password: '{{ user_secrets.password_hash }}' + + - name: Ensure the user can use SSH + ansible.posix.authorized_key: + user: '{{ user.name }}' + key: '{{ user.ssh_public_key }}' + + - name: Ensure only members of sshuser group can connect via SSH + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + line: 'AllowGroups sshuser' + validate: 'sshd -f %s -t' + notify: Reload sshd diff --git a/xmpp2/certbot.yml b/xmpp2/certbot.yml new file mode 100644 index 0000000..d89ee39 --- /dev/null +++ b/xmpp2/certbot.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Configure Certbot for certificate renewal + hosts: xmpp2 + become: true + + tasks: + - name: Install certbot + community.general.snap: + name: certbot + classic: true + + # One-time setup should be performed manually, see the documentation: + # https://certbot.eff.org/instructions?ws=nginx&os=snap&tab=standard + # + # sudo certbot --nginx -d codingteam.org.ru -d loglist.xyz -d www.loglist.xyz + # + # Verify the changes to the web server configuration files performed by this command. + # + # Further updates are done by snap.certbot.renew.timer — see `systemctl list-timers` for details. diff --git a/xmpp2/codingteam.org.ru.yml b/xmpp2/codingteam.org.ru.yml new file mode 100644 index 0000000..78e6ee0 --- /dev/null +++ b/xmpp2/codingteam.org.ru.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Main site for codingteam.org.ru + hosts: xmpp2 + become: true + + vars: + codingteam_org_ru_version: v1.2.1 + + handlers: + - name: Prune Docker + community.docker.docker_prune: + containers: true + images: true + images_filters: + dangling: false + networks: true + volumes: true + builder_cache: true + + - name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded + + tasks: + - name: Set up the Docker container + community.docker.docker_container: + name: codingteam.org.ru + image_name_mismatch: recreate + image: codingteam/codingteam.org.ru:{{ codingteam_org_ru_version }} + published_ports: + - '5000:5000' + restart_policy: unless-stopped + default_host_ip: '' + env: + ASPNETCORE_URLS: "http://+:5000" # otherwise, it can't be reached (listens to "localhost" only?) + notify: Prune Docker + + - name: Set up the nginx configuration file + ansible.builtin.copy: + src: nginx/conf.d/codingteam.org.ru.conf + dest: /etc/nginx/conf.d/codingteam.org.ru.conf + mode: "u=rx,go=rx" + notify: Reload nginx + + - name: Create a directory for the old logs # uploaded manually + ansible.builtin.file: + path: /opt/codingteam/old-logs + state: directory + owner: www-data + group: www-data + mode: "u=rx,go=rx" diff --git a/xmpp2/default.yml b/xmpp2/default.yml new file mode 100644 index 0000000..69564ed --- /dev/null +++ b/xmpp2/default.yml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +- import_playbook: auth.yml +- import_playbook: nginx.yml +- import_playbook: docker.yml +- import_playbook: codingteam.org.ru.yml +- import_playbook: loglist.yml +- import_playbook: certbot.yml diff --git a/xmpp2/docker.yml b/xmpp2/docker.yml new file mode 100644 index 0000000..2eb9f84 --- /dev/null +++ b/xmpp2/docker.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Install Docker + hosts: xmpp2 + become: true + + vars_files: + - vars.yml + + tasks: + - name: Install the Docker package + ansible.builtin.apt: + cache_valid_time: 86400 + name: docker.io + state: present + + - name: Add the admin user to docker group + ansible.builtin.user: + name: '{{ user.name }}' + groups: docker + append: true diff --git a/xmpp2/files/loglist/application.conf b/xmpp2/files/loglist/application.conf new file mode 100644 index 0000000..b8d5dd9 --- /dev/null +++ b/xmpp2/files/loglist/application.conf @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +play.http.secret.key = ${HTTP_SECRET_KEY} +play.i18n.langs = ["en"] + +feed.limit = 30 + +db.default.driver = org.postgresql.Driver +db.default.url = ${DATABASE_URL} + +play.evolutions.autocommit = false +play.evolutions.db.default.autoApply = ${APPLY_EVOLUTIONS_SILENTLY} + +recaptcha.publickey = ${RECAPTCHA_PUBLIC_KEY} +recaptcha.privatekey = ${RECAPTCHA_PRIVATE_KEY} + +basicAuth.username = ${BASIC_AUTH_USERNAME} +basicAuth.password = ${BASIC_AUTH_PASSWORD} + +approval.smtpHost = ${APPROVAL_SMTP_HOST} +approval.email = ${APPROVAL_EMAIL} +approval.emailPassword = ${APPROVAL_EMAIL_PASSWORD} + +play.modules.enabled += "scalikejdbc.PlayModule" + +play.filters.enabled += play.filters.hosts.AllowedHostsFilter +play.filters.hosts { + allowed = ["loglist.xyz"] +} diff --git a/xmpp2/files/loglist/init_db.sql b/xmpp2/files/loglist/init_db.sql new file mode 100644 index 0000000..24c1713 --- /dev/null +++ b/xmpp2/files/loglist/init_db.sql @@ -0,0 +1,5 @@ +-- SPDX-FileCopyrightText: 2025 Friedrich von Never +-- +-- SPDX-License-Identifier: MIT + +CREATE EXTENSION pgcrypto; diff --git a/xmpp2/files/nginx/conf.d/codingteam.org.ru.conf b/xmpp2/files/nginx/conf.d/codingteam.org.ru.conf new file mode 100644 index 0000000..b6e5af8 --- /dev/null +++ b/xmpp2/files/nginx/conf.d/codingteam.org.ru.conf @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2016-2025 codingteam/devops contributors +# +# SPDX-License-Identifier: MIT + +server { + listen 443 ssl http2; + server_name codingteam.org.ru; + include /etc/nginx/ssl.conf; + + location /old-logs/ { + alias /opt/codingteam/old-logs/; + index index.html; + } + + location / { + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host codingteam.org.ru; + proxy_http_version 1.1; + proxy_pass http://localhost:5000/; + } +} + +server { + listen 80; + server_name codingteam.org.ru; + + location / { + rewrite ^(.*)$ https://codingteam.org.ru$1 permanent; + } +} diff --git a/xmpp2/files/nginx/conf.d/loglist.conf b/xmpp2/files/nginx/conf.d/loglist.conf new file mode 100644 index 0000000..5440a9d --- /dev/null +++ b/xmpp2/files/nginx/conf.d/loglist.conf @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2016-2025 codingteam/devops contributors +# +# SPDX-License-Identifier: MIT + +server { + listen 443 ssl http2; + server_name loglist.xyz; + include /etc/nginx/ssl.conf; + + location / { + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host loglist.xyz; + proxy_http_version 1.1; + proxy_pass http://localhost:9000/; + } +} + +server { + listen 443 ssl http2; + server_name *.loglist.xyz; + include /etc/nginx/ssl.conf; + + location / { + return 301 https://loglist.xyz$request_uri; + } +} + +server { + listen 80; + server_name loglist.xyz; + + location / { + rewrite ^(.*)$ https://loglist.xyz$1 permanent; + } +} + +server { + listen 80; + server_name *.loglist.xyz; + location / { + return 301 https://loglist.xyz$request_uri; + } +} diff --git a/xmpp2/files/nginx/nginx.conf b/xmpp2/files/nginx/nginx.conf new file mode 100644 index 0000000..f9b0c9f --- /dev/null +++ b/xmpp2/files/nginx/nginx.conf @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2016-2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +user www-data; +worker_processes auto; +pid /run/nginx.pid; +error_log /var/log/nginx/error.log; + +events { + worker_connections 768; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 60; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + gzip on; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/xmpp2/files/nginx/ssl.conf b/xmpp2/files/nginx/ssl.conf new file mode 100644 index 0000000..5e80eab --- /dev/null +++ b/xmpp2/files/nginx/ssl.conf @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2017-2025 codingteam/devops contributors +# +# SPDX-License-Identifier: MIT + +ssl_certificate /etc/letsencrypt/live/codingteam.org.ru/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/codingteam.org.ru/privkey.pem; +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_ciphers "HIGH:!aNULL:!MD5:!kEDH"; +add_header Strict-Transport-Security 'max-age=15552000'; diff --git a/xmpp2/hosts.example.ini b/xmpp2/hosts.example.ini new file mode 100644 index 0000000..512c65d --- /dev/null +++ b/xmpp2/hosts.example.ini @@ -0,0 +1,6 @@ +; SPDX-FileCopyrightText: 2025 Friedrich von Never +; +; SPDX-License-Identifier: MIT + +[xmpp2] +xmpp2 ansible_user=mario ansible_ssh_private_key_file=/home/mario/.ssh/xmpp2 diff --git a/xmpp2/loglist.yml b/xmpp2/loglist.yml new file mode 100644 index 0000000..f25e6fe --- /dev/null +++ b/xmpp2/loglist.yml @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Application service for loglist.xyz + hosts: xmpp2 + become: true + + vars: + # Container versions: + postgresql_version: 9.3 + loglist_version: 2.0.1 + + # Paths on host: + host_db_init_scripts_dir: /opt/codingteam/loglist/init_db_scripts + host_data_dir: /opt/codingteam/loglist/data + host_config_dir: /opt/codingteam/loglist/config + + # Paths in containers: + container_data_dir: /data + + vars_files: + - secrets.yml + + handlers: + - name: Prune Docker + community.docker.docker_prune: + containers: true + images: true + images_filters: + dangling: false + networks: true + volumes: true + builder_cache: true + + - name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded + + tasks: + - name: Create directories + ansible.builtin.file: + path: '{{ item }}' + state: directory + mode: 'u=rx,g,o=r' + loop: + - '{{ host_db_init_scripts_dir }}' + - '{{ host_data_dir }}' + - '{{ host_config_dir }}' + + - name: Create the Docker network + community.docker.docker_network: + name: loglist + + - name: Copy the database initialization script + ansible.builtin.copy: + src: loglist/init_db.sql + dest: '{{ host_db_init_scripts_dir }}/init_db.sql' + mode: 'u,g,o=rx' + + - name: Set up the database container + community.docker.docker_container: + name: loglist.postgresql + image_name_mismatch: recreate + image: postgres:{{ postgresql_version }} + published_ports: + - '5423' + env: + POSTGRES_DB: loglist + POSTGRES_USER: loglist + POSTGRES_PASSWORD: '{{ loglist_secrets.db_password }}' + PGDATA: '{{ container_data_dir }}' + volumes: + - '{{ host_db_init_scripts_dir }}/:/docker-entrypoint-initdb.d/' + - '{{ host_data_dir }}/:/{{ container_data_dir }}/' + networks: + - name: loglist + default_host_ip: '' + notify: Prune Docker + + - name: Copy the application configuration file + ansible.builtin.copy: + src: loglist/application.conf + dest: '{{ host_config_dir }}/application.conf' + mode: 'u,g,o=r' + + - name: Set up the application container + community.docker.docker_container: + name: loglist.app + image_name_mismatch: recreate + image: codingteam/loglist:{{ loglist_version }} + published_ports: + - '9000:9000' + env: + APPLY_EVOLUTIONS_SILENTLY: 'true' + APPROVAL_EMAIL: '{{ loglist_secrets.approval_email.name }}' + APPROVAL_EMAIL_PASSWORD: '{{ loglist_secrets.approval_email.password }}' + APPROVAL_SMTP_HOST: '{{ loglist_secrets.approval_email.smtp_host }}' + BASIC_AUTH_PASSWORD: '{{ loglist_secrets.basic_auth.password }}' + BASIC_AUTH_USERNAME: '{{ loglist_secrets.basic_auth.username }}' + DATABASE_URL: 'jdbc:postgresql://loglist.postgresql/loglist?user=loglist&password={{ loglist_secrets.db_password }}' + JAVA_OPTS: '-Xmx200m -Xss512k -XX:+UseCompressedOops' + RECAPTCHA_PRIVATE_KEY: '{{ loglist_secrets.recaptcha.private_key }}' + RECAPTCHA_PUBLIC_KEY: '{{ loglist_secrets.recaptcha.public_key }}' + HTTP_SECRET_KEY: '{{ loglist_secrets.http_secret_key }}' + volumes: + - '{{ host_db_init_scripts_dir }}/:/docker-entrypoint-initdb.d/' + - '{{ host_data_dir }}/:/{{ container_data_dir }}/' + - '{{ host_config_dir }}/application.conf:/app/conf/application.conf' + networks: + - name: loglist + default_host_ip: '' + notify: Prune Docker + + - name: Set up the nginx configuration file + ansible.builtin.copy: + src: nginx/conf.d/loglist.conf + dest: /etc/nginx/conf.d/loglist.conf + mode: "u=rx,go=rx" + notify: Reload nginx diff --git a/xmpp2/nginx.yml b/xmpp2/nginx.yml new file mode 100644 index 0000000..23b30a4 --- /dev/null +++ b/xmpp2/nginx.yml @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +--- +- name: Install and configure Nginx + hosts: xmpp2 + become: true + + handlers: + - name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded + + tasks: + - name: Install nginx package + ansible.builtin.apt: + cache_valid_time: 86400 + name: nginx + state: present + + - name: Remove the *-enabled and *-available directories + ansible.builtin.file: + path: '/etc/nginx/{{ item }}' + state: absent + loop: + - modules-available + - modules-enabled + - sites-available + - sites-enabled + + - name: Set up the main nginx configuration file + ansible.builtin.copy: + src: 'nginx/{{ item }}' + dest: '/etc/nginx/{{ item }}' + mode: 'u=rw,go=r' + loop: + - nginx.conf + - ssl.conf + notify: Reload nginx diff --git a/xmpp2/vars/secrets.example.yml b/xmpp2/vars/secrets.example.yml new file mode 100644 index 0000000..6310a41 --- /dev/null +++ b/xmpp2/vars/secrets.example.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +user_secrets: + password_hash: '' # Use `mkpasswd --method=sha-512` to generate. + +loglist_secrets: + db_password: '' + http_secret_key: '' + + approval_email: + name: '' + password: '' + smtp_host: '' + + basic_auth: + username: '' + password: '' + + recaptcha: + private_key: '' + public_key: '' diff --git a/xmpp2/vars/vars.example.yml b/xmpp2/vars/vars.example.yml new file mode 100644 index 0000000..111a554 --- /dev/null +++ b/xmpp2/vars/vars.example.yml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Friedrich von Never +# +# SPDX-License-Identifier: MIT + +user: + name: mario + ssh_public_key: 'ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/XXXXXXXXXX/XXX username1@hostname'