diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..822b0b4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +root = true + +[ansible/**] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 2 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = 80,120 +ij_wrap_on_typing = false + +[{*.mk,GNUmakefile,GNUmakefile.inc,makefile,makefile.inc}] +tab_width = 4 +ij_visual_guides = +insert_final_newline = true + +[{*.yaml,*.yml,*playbook.yaml,*playbook.yml,main.yaml,main.yml}] +insert_final_newline = true +ij_visual_guides = +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/ansible/Makefile b/ansible/Makefile new file mode 100644 index 0000000..f849f22 --- /dev/null +++ b/ansible/Makefile @@ -0,0 +1,20 @@ +SSH_USER ?= root +HOST_ADDR ?= your.server.ip + +PATH_TO_PRIVATE_KEY ?= path/to/private/key + +lint: + ansible-lint ./roles + +fix-lint: + ansible-lint ./roles --fix + +playbook: + ansible-playbook \ + -i ./inventory.ini \ + ./playbook.yml \ + --user $(SSH_USER) \ + --private-key $(PATH_TO_PRIVATE_KEY) + +ssh: + ssh -o "ServerAliveInterval 10" -o "TCPKeepAlive yes" -i $(PATH_TO_PRIVATE_KEY) $(SSH_USER)@$(HOST_ADDR) diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..78fd660 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,49 @@ +# cloud + +```bash +ansible-galaxy collection install community.general # for ufw +``` + +### Handy commands + +#### Lint the Ansible files + +```bash +ansible-lint ./playbook.yml +``` + +#### Deploy the Ansible's playbook + +```bash +ansible-playbook \ + -i ./inventory.ini \ + ./playbook.yml \ + --user root \ + --private-key path/to/private/key +``` + +### Troubleshooting + +#### Connect via SSH to the instance (and keep it running without freezing) + +```bash +ssh -o "ServerAliveInterval 10" -o "TCPKeepAlive yes" -i path/to/private/key root@instance-address +``` + +#### Check the systemctl service status of the other's user + +```bash +systemctl --user --machine=appuser@ status gladiator.service +``` + +#### Impersonate as other user and run bash (su for users without shell configured) + +```bash +sudo machinectl shell appuser@ /bin/bash +``` + +#### Check the Quadlet configuration (convert container definition to a systemd service) + +```bash +/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..fa9e73e --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +retry_files_enabled = False +result_format = yaml + +[connection] +pipelining = True diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000..3dc9105 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,5 @@ +[web] +your.server.ip ansible_user=your_ssh_user + +;yourserver ansible_host={{ secrets.DEPLOY_HOST }} +;yourserver ansible_host=1.2.3.4 ansible_user=ubuntu \ No newline at end of file diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 0000000..95e2e35 --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,12 @@ +--- +- name: Deploy app + hosts: all + become: true + roles: + - podman + - security + - appuser + - gladiator + - nginx +# - prometheus +# - grafana diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..afc836d --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - name: community.general diff --git a/ansible/roles/appuser/defaults/main.yml b/ansible/roles/appuser/defaults/main.yml new file mode 100644 index 0000000..adedd7c --- /dev/null +++ b/ansible/roles/appuser/defaults/main.yml @@ -0,0 +1,2 @@ +--- +appuser_name: "appuser" diff --git a/ansible/roles/appuser/tasks/main.yml b/ansible/roles/appuser/tasks/main.yml new file mode 100644 index 0000000..7081c9a --- /dev/null +++ b/ansible/roles/appuser/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Ensure appuser exists + "ansible.builtin.user": + name: "{{ appuser_name }}" + shell: /usr/sbin/nologin + create_home: true + home: "/home/{{ appuser_name }}" + +- name: Get UID of appuser + ansible.builtin.command: "id -u {{ appuser_name }}" + register: appuser_uid_cmd + changed_when: false + +- name: Export appuser UID + ansible.builtin.set_fact: + appuser_uid: "{{ appuser_uid_cmd.stdout }}" + +# Rootless services require "lingering" to allow user systemd to start at boot. +- name: Ensure linger is enabled for appuser + ansible.builtin.command: "loginctl enable-linger {{ appuser_name }}" + args: + creates: "/var/lib/systemd/linger/{{ appuser_name }}" + +- name: Set file limits for appuser + ansible.builtin.copy: + dest: /etc/security/limits.d/appuser.conf + content: | + appuser soft nofile 65535 + appuser hard nofile 65535 + mode: "0644" diff --git a/ansible/roles/gladiator/defaults/main.yml b/ansible/roles/gladiator/defaults/main.yml new file mode 100644 index 0000000..c75b1f0 --- /dev/null +++ b/ansible/roles/gladiator/defaults/main.yml @@ -0,0 +1,18 @@ +--- +gladiator_container_name: gladiator +gladiator_image: ghcr.io/dimspell/gladiator:latest +gladiator_port: 2137 + +gladiator_env: + LOG_LEVEL: "debug" + LOG_FORMAT: "json" + CONSOLE_ADDR: "0.0.0.0:2137" + CONSOLE_PUBLIC_ADDR: "https://example.com" + RELAY_ADDR: "0.0.0.0:9999" + RELAY_PUBLIC_ADDR: "example:9999" + DATABASE_TYPE: "sqlite" + SQLITE_PATH: "/data/gladiator-db.sqlite" + +gladiator_volume_container: /data + +gladiator_env_file: "/etc/containers/systemd/{{ gladiator_container_name }}.env" diff --git a/ansible/roles/gladiator/handlers/main.yml b/ansible/roles/gladiator/handlers/main.yml new file mode 100644 index 0000000..9fa5ccf --- /dev/null +++ b/ansible/roles/gladiator/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: Restart gladiator + become: true + become_user: "{{ appuser_name }}" + ansible.builtin.systemd: + name: "{{ gladiator_container_name }}.service" + state: restarted + daemon_reload: true + scope: user + environment: + XDG_RUNTIME_DIR: "/run/user/{{ appuser_uid }}" diff --git a/ansible/roles/gladiator/meta/main.yml b/ansible/roles/gladiator/meta/main.yml new file mode 100644 index 0000000..4b90895 --- /dev/null +++ b/ansible/roles/gladiator/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: podman + - role: appuser diff --git a/ansible/roles/gladiator/tasks/main.yml b/ansible/roles/gladiator/tasks/main.yml new file mode 100644 index 0000000..269dc4d --- /dev/null +++ b/ansible/roles/gladiator/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Ensure Podman and Quadlet dirs exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + owner: "{{ appuser_name }}" + group: "{{ appuser_name }}" + loop: + - "/home/{{ appuser_name }}/.config/containers/systemd/" + +- name: Deploy .env file + ansible.builtin.template: + src: gladiator.env.j2 + dest: "{{ gladiator_env_file }}" + mode: "0644" + owner: "{{ appuser_name }}" + group: "{{ appuser_name }}" + notify: Restart gladiator + +# Note: it will create the directory ~/.local/share/containers/storage/volumes/gladiator +- name: Deploy Quadlet volume unit + ansible.builtin.template: + src: gladiator.volume.j2 + dest: "/home/{{ appuser_name }}/.config/containers/systemd/{{ gladiator_container_name }}.volume" + owner: "{{ appuser_name }}" + group: "{{ appuser_name }}" + mode: "0644" + notify: Restart gladiator + +- name: Deploy Quadlet container unit + ansible.builtin.template: + src: gladiator.container.j2 + dest: "/home/{{ appuser_name }}/.config/containers/systemd/{{ gladiator_container_name }}.container" + owner: "{{ appuser_name }}" + group: "{{ appuser_name }}" + mode: "0644" + notify: Restart gladiator + +- name: Enable and start container service + become: true + become_method: ansible.builtin.su + become_user: "{{ appuser_name }}" + become_flags: "-s /bin/bash" # Specify a valid shell + ansible.builtin.systemd: + name: "{{ gladiator_container_name }}.service" + enabled: true + state: started + scope: user + daemon_reload: true + environment: + XDG_RUNTIME_DIR: "/run/user/{{ appuser_uid }}" diff --git a/ansible/roles/gladiator/templates/gladiator.container.j2 b/ansible/roles/gladiator/templates/gladiator.container.j2 new file mode 100644 index 0000000..0a450b6 --- /dev/null +++ b/ansible/roles/gladiator/templates/gladiator.container.j2 @@ -0,0 +1,40 @@ +[Unit] +Description=Gladiator Container + +[Container] +Image={{ gladiator_image }} +AutoUpdate=registry +Label=io.containers.autoupdate=registry + +ContainerName={{ gladiator_container_name }} + +# Ports +PublishPort=127.0.0.1:2137:2137 +PublishPort=0.0.0.0:9999:9999/udp + +# Volumes +Volume=gladiator:{{ gladiator_volume_container }} + +# Env file +EnvironmentFile={{ gladiator_env_file }} + +# Logging +LogDriver=journald + +# Health check +HealthCmd=curl -f http://localhost:2137/_health || exit 1 +HealthInterval=30s +HealthRetries=3 + +User={{ appuser_uid }} +ReadOnly=true +DropCapability=ALL +NoNewPrivileges=true + +Exec=console + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/ansible/roles/gladiator/templates/gladiator.env.j2 b/ansible/roles/gladiator/templates/gladiator.env.j2 new file mode 100644 index 0000000..bd0b059 --- /dev/null +++ b/ansible/roles/gladiator/templates/gladiator.env.j2 @@ -0,0 +1,3 @@ +{% for key, value in gladiator_env.items() %} +{{ key }}={{ value }} +{% endfor %} diff --git a/ansible/roles/gladiator/templates/gladiator.volume.j2 b/ansible/roles/gladiator/templates/gladiator.volume.j2 new file mode 100644 index 0000000..fffdcb2 --- /dev/null +++ b/ansible/roles/gladiator/templates/gladiator.volume.j2 @@ -0,0 +1,4 @@ +[Volume] +Type=bind +Source=/var/lib/gladiator +Options=rbind,rw diff --git a/ansible/roles/grafana/defaults/main.yml b/ansible/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..d151be0 --- /dev/null +++ b/ansible/roles/grafana/defaults/main.yml @@ -0,0 +1,5 @@ +--- +grafana_port: 3000 +grafana_admin_user: admin +grafana_admin_password: admin +grafana_prometheus_url: "http://localhost:{{ prometheus_port }}" diff --git a/ansible/roles/grafana/tasks/main.yml b/ansible/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..f1746b4 --- /dev/null +++ b/ansible/roles/grafana/tasks/main.yml @@ -0,0 +1,45 @@ +--- +- name: Add Grafana APT key + "ansible.builtin.apt_key": + url: https://packages.grafana.com/gpg.key + state: present + +- name: Add Grafana APT repository + "ansible.builtin.apt_repository": + repo: "deb https://packages.grafana.com/oss/deb stable main" + state: present + filename: grafana + +- name: Install Grafana + "ansible.builtin.apt": + name: grafana + update_cache: true + state: present + +- name: Enable and start Grafana + ansible.builtin.systemd: + name: grafana-server + enabled: true + state: started + +- name: Wait for Grafana to start + ansible.builtin.wait_for: + port: "{{ grafana_port }}" + timeout: 30 + +- name: Configure Prometheus data source + ansible.builtin.uri: + url: "http://localhost:{{ grafana_port }}/api/datasources" + method: POST + user: "{{ grafana_admin_user }}" + password: "{{ grafana_admin_password }}" + body_format: json + body: + name: "Prometheus" + type: "prometheus" + url: "{{ grafana_prometheus_url }}" + access: "proxy" + isDefault: true + status_code: 200,409 # 409 = already exists + headers: + Content-Type: "application/json" diff --git a/ansible/roles/nginx/defaults/main.yml b/ansible/roles/nginx/defaults/main.yml new file mode 100644 index 0000000..633e14d --- /dev/null +++ b/ansible/roles/nginx/defaults/main.yml @@ -0,0 +1,5 @@ +--- +nginx_domain_fqdn: "example.com" + +nginx_site_name: "gladiator" +nginx_certbot_email: "user+letsencrypt@example.com" diff --git a/ansible/roles/nginx/tasks/main.yml b/ansible/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..e7592ef --- /dev/null +++ b/ansible/roles/nginx/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: Install nginx and certbot + ansible.builtin.apt: + name: + - nginx + - python3-certbot-nginx + state: present + update_cache: true + +- name: Ensure NGINX config directory exists + ansible.builtin.file: + path: /etc/nginx/sites-available + state: directory + owner: root + group: root + mode: "0755" + +- name: Obtain Let's Encrypt certificate + ansible.builtin.command: > + certbot run --nginx --non-interactive --agree-tos --redirect --email {{ nginx_certbot_email }} -d {{ nginx_domain_fqdn }} + args: + creates: "/etc/letsencrypt/live/{{ nginx_domain_fqdn }}/fullchain.pem" + +- name: Disable NGINX default site + ansible.builtin.file: + state: absent + path: "/etc/nginx/sites-enabled/default" + register: nginx_default_site_removed_result + +- name: Reload NGINX after the deactivation of default site + ansible.builtin.service: + name: nginx + state: reloaded + when: nginx_default_site_removed_result is succeeded and nginx_default_site_removed_result.state == 'changed' + +- name: Deploy NGINX reverse proxy config + ansible.builtin.template: + src: "nginx_site.j2" + dest: "/etc/nginx/sites-available/{{ nginx_site_name }}" + owner: root + group: root + mode: "0644" + +- name: Enable NGINX site + ansible.builtin.file: + src: "/etc/nginx/sites-available/{{ nginx_site_name }}" + dest: "/etc/nginx/sites-enabled/{{ nginx_site_name }}" + state: link + force: true + +- name: Test NGINX configuration + ansible.builtin.command: nginx -t + changed_when: false + +- name: Reload NGINX + ansible.builtin.service: + name: nginx + state: reloaded + +- name: Ensure certbot systemd timer is enabled + ansible.builtin.systemd: + name: certbot.timer + enabled: true + state: started diff --git a/ansible/roles/nginx/templates/nginx_site.j2 b/ansible/roles/nginx/templates/nginx_site.j2 new file mode 100644 index 0000000..0e5b359 --- /dev/null +++ b/ansible/roles/nginx/templates/nginx_site.j2 @@ -0,0 +1,49 @@ +server { + if ($host = {{ nginx_domain_fqdn }}) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + server_name {{ nginx_domain_fqdn }}; + return 404; +} + +server { + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + server_name {{ nginx_domain_fqdn }}; + ssl_certificate /etc/letsencrypt/live/{{ nginx_domain_fqdn }}/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/{{ nginx_domain_fqdn }}/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + location /lobby { + proxy_pass http://localhost:2137/lobby; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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_read_timeout 86400; + } + +# location /grpc { +# grpc_pass grpc://localhost:2137; +# +# # Required GRPC headers +# grpc_set_header Host $host; +# grpc_set_header X-Real-IP $remote_addr; +# } + + location / { + proxy_pass http://localhost:2137; + 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-Proto $scheme; + } +} diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml new file mode 100644 index 0000000..b8b0f1e --- /dev/null +++ b/ansible/roles/podman/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Install Podman and dependencies + "ansible.builtin.apt": + name: + - uidmap + - fuse-overlayfs + - slirp4netns + - systemd-container + - podman + - passt + state: present + update_cache: true + +- name: Enable and start Podman auto-update + ansible.builtin.systemd: + name: podman-auto-update.timer + enabled: true + state: started diff --git a/ansible/roles/prometheus/defaults/main.yml b/ansible/roles/prometheus/defaults/main.yml new file mode 100644 index 0000000..8714cae --- /dev/null +++ b/ansible/roles/prometheus/defaults/main.yml @@ -0,0 +1,5 @@ +--- +prometheus_version: "2.52.0" +prometheus_port: 9090 + +prometheus_gladiator_target: "localhost:8080" diff --git a/ansible/roles/prometheus/tasks/main.yml b/ansible/roles/prometheus/tasks/main.yml new file mode 100644 index 0000000..099b9ed --- /dev/null +++ b/ansible/roles/prometheus/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Copy Prometheus config + ansible.builtin.template: + src: prometheus.yml.j2 + dest: /etc/prometheus/prometheus.yml + owner: prometheus + group: prometheus + mode: "0644" diff --git a/ansible/roles/prometheus/templates/prometheus.yml.j2 b/ansible/roles/prometheus/templates/prometheus.yml.j2 new file mode 100644 index 0000000..356c70c --- /dev/null +++ b/ansible/roles/prometheus/templates/prometheus.yml.j2 @@ -0,0 +1 @@ + - targets: ['{{ prometheus_gladiator_target }}'] diff --git a/ansible/roles/security/defaults/main.yml b/ansible/roles/security/defaults/main.yml new file mode 100644 index 0000000..f201fb8 --- /dev/null +++ b/ansible/roles/security/defaults/main.yml @@ -0,0 +1 @@ +security_install_lynis: false diff --git a/ansible/roles/security/handlers/main.yml b/ansible/roles/security/handlers/main.yml new file mode 100644 index 0000000..6665708 --- /dev/null +++ b/ansible/roles/security/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart ssh + ansible.builtin.service: + name: ssh + state: restarted diff --git a/ansible/roles/security/tasks/main.yml b/ansible/roles/security/tasks/main.yml new file mode 100644 index 0000000..d7f7b28 --- /dev/null +++ b/ansible/roles/security/tasks/main.yml @@ -0,0 +1,80 @@ +--- +- name: Ensure automatic updates are installed + ansible.builtin.apt: + name: unattended-upgrades + state: present + update_cache: true + +- name: Enable unattended upgrades + ansible.builtin.lineinfile: + dest: /etc/apt/apt.conf.d/20auto-upgrades + regexp: "^{{ item.key }}" + line: "{{ item.key }} \"{{ item.value }}\";" + loop: + - { key: APT::Periodic::Update-Package-Lists, value: "1" } + - { key: APT::Periodic::Unattended-Upgrade, value: "1" } + +# Disable for now +# - name: Disable root SSH login +# ansible.builtin.lineinfile: +# path: /etc/ssh/sshd_config +# regexp: "^PermitRootLogin" +# line: "PermitRootLogin no" +# notify: restart ssh + +- name: Disable password auth (use only keys) + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "^PasswordAuthentication" + line: "PasswordAuthentication no" + notify: Restart ssh + +- name: Install Fail2ban + ansible.builtin.apt: + name: fail2ban + state: present + +- name: Install UFW + ansible.builtin.apt: + name: ufw + state: present + +- name: Set up UFW firewall + community.general.ufw: + rule: allow + port: "{{ item }}" + loop: + - 22 + - 80 + - 443 + - 9999 + +- name: Enable UFW + community.general.ufw: + state: enabled + policy: deny + +- name: Configure kernel sysctl parameters for relay server + ansible.builtin.copy: + dest: /etc/sysctl.d/90-quic-relay.conf + content: | + net.core.rmem_max=7500000 + net.core.wmem_max=7500000 + mode: '0644' + +# TODO: Tweak systcl configuration +# - name: Configure kernel sysctl parameters to harden the OS +# ansible.builtin.copy: +# dest: /etc/sysctl.d/99-hardening.conf +# content: | +# net.ipv4.ip_forward = 0 +# net.ipv4.icmp_ignore_bogus_error_responses = 1 +# net.ipv4.conf.all.accept_source_route = 0 +# net.ipv6.conf.all.accept_source_route = 0 +# net.ipv4.conf.all.rp_filter = 1 +# kernel.kptr_restrict = 2 +# mode: '0644' + +- name: Apply sysctl settings + ansible.builtin.command: sysctl --system + changed_when: true