#cloud-config
users:
  # Create webmaster account
  - default
  - name: webmin
    gecos: "Website Administrator"
    shell: /bin/bash
    homedir: /home/webmin
    lock_passwd: true
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
  # Create a rootless container user
  - name: web
    system: true
    shell: /bin/false
    homedir: /data/web

write_files:
  - content: |
      #!/usr/sbin/nft -f

      flush ruleset

      define WAN = enp1s0

      table inet global {
          chain inbound_public {
              # Basic DoS protection
              tcp flags syn limit rate 100/second burst 150 packets accept

              # ICMP/ICMPv6 rate limiting & essential types
              icmp type echo-request limit rate 5/second accept
              icmpv6 type echo-request limit rate 5/second accept
              icmpv6 type {
                  nd-neighbor-solicit,
                  nd-neighbor-advert,
                  nd-router-advert,
                  nd-router-solicit,
                  parameter-problem,
                  packet-too-big
              } accept

              # ICMPv4
              icmp type { destination-unreachable, time-exceeded, parameter-problem } accept

              tcp dport { 80, 443 } accept
              tcp dport 22222 accept
              tcp dport 22223 accept
          }

          chain inbound {
              type filter hook input priority filter; policy drop;

              # Connection tracking - Allow established/related, drop invalid
              ct state vmap { established : accept, related : accept, invalid : drop }

              # Anti-spoofing rules: IPv4
              iifname $WAN ip saddr {
                  127.0.0.0/8,     # Localhost
                  0.0.0.0/8,       # Invalid source
                  10.0.0.0/8,      # RFC1918
                  172.16.0.0/12,   # RFC1918
                  192.168.0.0/16,  # RFC1918
                  169.254.0.0/16,  # Link Local
                  224.0.0.0/4      # Multicast
              } drop

              # Anti-spoofing rules: IPv6
              iifname $WAN ip6 saddr {
                  ::1/128,         # Localhost
                  fe80::/10,       # Link Local
                  ff00::/8,        # Multicast
                  fd00::/8         # ULA
              } drop

              # Drop invalid TCP flags
              tcp flags & (fin|syn) == (fin|syn) drop
              tcp flags & (syn|rst) == (syn|rst) drop
              tcp flags & (fin|syn|rst|psh|ack|urg) == 0 drop
              tcp flags & (fin|syn|rst|psh|ack|urg) < (syn) drop
              tcp flags syn tcp option maxseg size <= 48 drop

              # Drop IPv4 broadcast explicitly
              iifname $WAN ip daddr 255.255.255.255 counter drop

              # Allow all traffic on loopback interface
              iifname lo accept

              # Jump to public chain for WAN interface traffic
              iifname $WAN jump inbound_public

              # Log and drop anything else that reaches here
              log prefix "[nftables] Inbound Denied: " counter drop
          }

          chain outbound {
              type filter hook output priority filter; policy accept;
          }

          # No need to forward anything, this is a web server
          chain forward {
              type filter hook forward priority filter; policy drop;
          }
      }
    path: /etc/nftables/main.nft
    permissions: '0600'
    owner: root:root
  - content: |
      net.ipv4.ip_unprivileged_port_start=80
    path: /etc/sysctl.d/99-rootless-ports.conf
    permissions: '0600'
    owner: root:root
  - content: |
      user root;
      worker_processes auto;

      pid /run/nginx.pid;

      events {
        worker_connections 1024;
        multi_accept on;
      }

      http {
        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
        
        access_log /dev/stdout main;
        error_log /dev/stderr warn;

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;

        keepalive_timeout 65;
        #keepalive_requests 100;

        types_hash_max_size 2048;
        
        server_tokens off;

        client_max_body_size 50M;
        client_body_buffer_size 128K;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;

        ssl_dhparam /etc/nginx/dhparam.pem;

        gzip on;
        gzip_disable "msie6";
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 16 8k;
        gzip_http_version 1.1;
        gzip_types
          application/atom+xml application/javascript application/json application/ld+json
          application/manifest+json application/rss+xml application/vnd.geo+json
          application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json
          application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml
          image/x-icon text/cache-manifest text/css text/plain text/vcard
          text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
  
        open_file_cache max=1000 inactive=20s;
        open_file_cache_valid 30s;
        open_file_cache_min_uses 2;
        open_file_cache_errors on;

        include /etc/nginx/conf.d/*;
      }
    path: /data/web/nginx/conf/nginx.conf
    permissions: '0644'
    owner: root:root
  - content: |
      server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/default;
        index index.html index.htm;

        server_name _;

        location / {
                try_files $uri $uri/ =404;
        }
      }
    path: /data/web/nginx/conf/conf.d/default
    permissions: '0644'
    owner: root:root
  - content: |
      <!DOCTYPE html>
      <html>
      <head>
      <title>Service Up and Running.</title>
      <style>
        body {
          width: 35em;
          margin: 0 auto;
          font-family: Tahoma, Verdana, Arial, sans-serif;
        }
      </style>
      </head>
      <body>
      <h1>Service Up and Running.</h1>
      </body>
      </html>
    path: /data/web/nginx/www/default/index.html
    permissions: '0644'
    owner: root:root
  - content: |
      [Unit]
      Description=Podman Nginx Container
      Wants=network-online.target
      After=network-online.target
      RequiresMountsFor=%h

      [Service]
      Environment=PODMAN_SYSTEMD_UNIT=%n
      Restart=on-failure
      TimeoutStopSec=70
      ExecStartPre=-/usr/bin/podman rm -f %N
      ExecStart=/usr/bin/podman run \
        --name %N \
        --replace \
        --rm \
        -p 80:80 \
        -p 443:443 \
        --volume %h/nginx/www:/var/www:ro,z \
        --volume %h/nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro,z \
        --volume %h/nginx/conf/dhparam.pem:/etc/nginx/dhparam.pem:ro,z \
        --volume %h/nginx/conf/conf.d:/etc/nginx/conf.d:ro,z \
        --volume %h/acme/etc:/etc/letsencrypt:ro,z \
        --volume %h/acme/www:/var/www/certbot:ro,z \
        docker.io/library/nginx:stable-alpine

      # Use podman exec to reload config gracefully
      ExecReload=/usr/bin/podman exec %N nginx -s reload
      ExecStop=/usr/bin/podman stop -t 10 %N
      ExecStopPost=-/usr/bin/podman rm -f %N

      [Install]
      WantedBy=default.target
    path: /data/web/.config/systemd/user/nginx.service
    permissions: '0644'
    owner: root:root
  - content: |
      [Unit]
      Description=Podman SSH Container
      Wants=network-online.target
      After=network-online.target
      RequiresMountsFor=%h

      [Service]
      Environment=PODMAN_SYSTEMD_UNIT=%n
      Restart=on-failure
      TimeoutStopSec=70
      ExecStartPre=-/usr/bin/podman rm -f %N
      ExecStart=/usr/bin/podman run \
        --name %N \
        --replace \
        --rm \
        -p 22223:22223 \
        --volume %h/ssh/etc:/etc/ssh:z \
        --volume %h/ssh/root:/root:z \
        --volume %h/nginx/www:/var/www:z \
        docker.io/library/alpine:latest \
          sh -c "apk add openssh rsync; (if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then ssh-keygen -A; fi); /usr/sbin/sshd -D -e -p 22223"

      # Use podman exec to reload config gracefully
      ExecStop=/usr/bin/podman stop -t 10 %N
      ExecStopPost=-/usr/bin/podman rm -f %N

      [Install]
      WantedBy=default.target
    path: /data/web/.config/systemd/user/sshd.service
    permissions: '0644'
    owner: root:root
  - content: |
      [Unit]
      Description=Renew Let's Encrypt certificates (Certbot)

      [Timer]
      OnCalendar=daily
      RandomizedDelaySec=12h
      Persistent=true

      [Install]
      WantedBy=timers.target
    path: /data/web/.config/systemd/user/certbot-renew.timer
    permissions: '0644'
    owner: root:root
  - content: |
      [Unit]
      Description=Renew Let's Encrypt certificates (Certbot)

      [Service]
      Type=oneshot
      ExecStart=/usr/bin/podman run \
        --rm \
        --name certbot-renew \
        --volume %h/acme/etc:/etc/letsencrypt:z \
        --volume %h/acme/lib:/var/lib/letsencrypt:z \
        --volume %h/acme/www:/var/www/certbot:z \
        docker.io/certbot/certbot:latest \
          renew \
          --webroot -w /var/www/certbot \
          --quiet

      ExecStartPost=/usr/bin/systemctl --user reload nginx.service
    path: /data/web/.config/systemd/user/certbot-renew.service
    permissions: '0644'
    owner: root:root

# Run server configuration commands
runcmd:
  - |
    # Install packages here because for some reason it runs out of memory if using the packages module
    dnf -y update
    dnf -y install podman passt curl wget git nftables policycoreutils-python-utils 

    # Configure SSH port, disable root login, disable password login
    sed -i '/^PermitRootLogin yes$/d' /etc/ssh/sshd_config
    sed -i 's/#Port\ 22/Port\ 22222/g' /etc/ssh/sshd_config
    sed -i 's/#PermitRootLogin\ yes/PermitRootLogin\ no/g' /etc/ssh/sshd_config
    sed -i 's/#PasswordAuthentication\ yes/PasswordAuthentication\ no/g' /etc/ssh/sshd_config
    semanage port -a -t ssh_port_t -p tcp 22222
    systemctl restart sshd
    
    # Configure nftables
    sed -i 's/#include/include/g' /etc/sysconfig/nftables.conf

    # Disable firewalld, enable nftables
    systemctl disable --now firewalld
    systemctl mask firewalld
    systemctl enable --now nftables

    # Enable rootless container accounts to run services < 1024
    sysctl --system

    # Make sure that web owns everything as setting permissions with the users didn't work in testing
    chown -R web:web /data/web

    # Prepare rootless container account
    export SERVICE="web"
    loginctl enable-linger "${SERVICE}"
    DIRS=("acme/etc" "acme/www" "acme/lib" "ssh/etc" "ssh/root")
    for DIR in ${DIRS[@]}; do su -s /bin/bash web -c "mkdir -p ~/${DIR}"; done
    semanage fcontext -a -t user_home_dir_t "/data/${SERVICE}(/.+)?"
    restorecon -Frv /data/"${SERVICE}"
    NEW_SUBUID=$(($(tail -1 /etc/subuid |awk -F ":" '{print $2}')+65536))
    NEW_SUBGID=$(($(tail -1 /etc/subgid |awk -F ":" '{print $2}')+65536))
    usermod --add-subuids ${NEW_SUBUID}-$((${NEW_SUBUID}+65535)) --add-subgids ${NEW_SUBGID}-$((${NEW_SUBGID}+65535)) "${SERVICE}"

    # Enable SSH access for webmin
    cp -r /root/.ssh /home/webmin
    chown -R webmin:webmin /home/webmin
    
    # Enable SSH access for rootless container account
    cp -r /root/.ssh /data/web/ssh/root
    chown -R web:web /data/web/ssh/root

    # Generate DH Params for web server
    su -s /bin/bash web -c 'openssl dhparam -out /data/web/nginx/conf/dhparam.pem 2048'
    su -s /bin/bash web -c 'chmod 600 /data/web/nginx/conf/dhparam.pem'

    # Configure web server
    su -s /bin/bash web -c 'echo "export XDG_RUNTIME_DIR=/run/user/$(id -u);" > ~/.bashrc'
    su -s /bin/bash web -c '
      systemctl --user enable --now nginx.service;
      systemctl --user enable --now sshd.service;
    '
    sync
    systemctl reboot
