diff options
author | heqnx <root@heqnx.com> | 2025-07-05 12:21:29 +0300 |
---|---|---|
committer | heqnx <root@heqnx.com> | 2025-07-05 12:21:29 +0300 |
commit | 9457306a881cfe476ee0abbdba4f6f1eaa80db9d (patch) | |
tree | a0051e68286eb93381a398d2681ec5a14f5c035e | |
parent | 730705affa4407a9dee7c52e5deb825020da110d (diff) | |
download | ansible-icecast2-9457306a881cfe476ee0abbdba4f6f1eaa80db9d.tar.gz ansible-icecast2-9457306a881cfe476ee0abbdba4f6f1eaa80db9d.zip |
initial commit on a working icecast2 setup
-rw-r--r-- | .gitignore | 10 | ||||
-rw-r--r-- | inventory.yaml.example | 15 | ||||
-rw-r--r-- | main.yaml | 11 | ||||
-rw-r--r-- | tasks/apt_packages.yaml | 11 | ||||
-rw-r--r-- | tasks/harden.yaml | 161 | ||||
-rw-r--r-- | tasks/hugo_setup.yaml | 24 | ||||
-rw-r--r-- | tasks/icecast2_setup.yaml | 38 | ||||
-rw-r--r-- | tasks/nginx_setup.yaml | 47 | ||||
-rw-r--r-- | tasks/preflight.yaml | 16 | ||||
-rw-r--r-- | templates/hugo.toml.j2 | 13 | ||||
-rw-r--r-- | templates/icecast2/icecast.xml.j2 | 65 | ||||
-rw-r--r-- | templates/icecast2/ices-playlist.xml.j2 | 44 | ||||
-rw-r--r-- | templates/icecast2/mp3-to-ogg.sh.j2 | 32 | ||||
-rw-r--r-- | templates/nginx/nginx.conf.j2 | 118 | ||||
-rw-r--r-- | templates/systemd/system/ices2.service | 17 | ||||
-rw-r--r-- | vars/main.yaml | 14 |
16 files changed, 636 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4adce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +inventory.ini +inventory.yaml +inventory.yml +playbook.yaml +playbook.yml +*rsa* +*ed25519* +*venv* +.hugo_build.lock +hugo/hugo.toml diff --git a/inventory.yaml.example b/inventory.yaml.example new file mode 100644 index 0000000..5872643 --- /dev/null +++ b/inventory.yaml.example @@ -0,0 +1,15 @@ +all: + hosts: + server01: + ansible_host: 10.11.12.13 + ansible_user: root + ansible_ssh_private_key_file: id_rsa + email: root@example.com + radio_name: "my radio" + radio_genre: "my genre" + radio_description: "my description" + radio_url: "https://test123.com" + children: + servers: + hosts: + server01: {} diff --git a/main.yaml b/main.yaml new file mode 100644 index 0000000..bd4f9bc --- /dev/null +++ b/main.yaml @@ -0,0 +1,11 @@ +- name: setup server01 + hosts: server01 + gather_facts: true + vars_files: + - vars/main.yaml + tasks: + - import_tasks: tasks/preflight.yaml + - import_tasks: tasks/apt_packages.yaml + - import_tasks: tasks/harden.yaml + - import_tasks: tasks/icecast2_setup.yaml + - import_tasks: tasks/nginx_setup.yaml diff --git a/tasks/apt_packages.yaml b/tasks/apt_packages.yaml new file mode 100644 index 0000000..c67f043 --- /dev/null +++ b/tasks/apt_packages.yaml @@ -0,0 +1,11 @@ +- name: upgrade apt packages + apt: + upgrade: dist + +- name: install apt packages + apt: + name: "{{ apt_packages }}" + state: present + update_cache: true + environment: + DEBIAN_FRONTEND: noninteractive diff --git a/tasks/harden.yaml b/tasks/harden.yaml new file mode 100644 index 0000000..3fad047 --- /dev/null +++ b/tasks/harden.yaml @@ -0,0 +1,161 @@ +- name: clear /etc/issue and /etc/motd + copy: + content: "" + dest: "{{ item }}" + loop: + - /etc/issue + - /etc/motd + +- name: check if /etc/update-motd.d directory exists + stat: + path: /etc/update-motd.d + register: motd_dir + +- name: find files in /etc/update-motd.d + find: + paths: /etc/update-motd.d + file_type: file + register: motd_files + when: motd_dir.stat.exists + +- name: remove execute permissions from all files in /etc/update-motd.d + file: + path: "{{ item.path }}" + mode: u-x,g-x,o-x + loop: "{{ motd_files.files }}" + when: motd_dir.stat.exists + +- name: enforce root-only cron/at + file: + path: "{{ item }}" + state: touch + owner: root + group: root + mode: '0600' + loop: + - /etc/cron.allow + - /etc/at.allow + +- name: remove deny files for cron and at + file: + path: "{{ item }}" + state: absent + loop: + - /etc/cron.deny + - /etc/at.deny + +- name: backup sshd_config + copy: + src: /etc/ssh/sshd_config + dest: "/etc/ssh/sshd_config.bak_{{ ansible_date_time.iso8601_basic }}" + remote_src: true + +- name: harden sshd_config + copy: + dest: /etc/ssh/sshd_config + content: | + Port 22 + Banner /etc/issue + UsePAM yes + Protocol 2 + Subsystem sftp /usr/lib/openssh/sftp-server + LogLevel verbose + PrintMotd no + #AcceptEnv LANG LC_* + MaxSessions 5 + StrictModes yes + Compression no + MaxAuthTries 3 + IgnoreRhosts yes + PrintLastLog yes + AddressFamily inet + X11Forwarding no + PermitRootLogin yes + AllowTcpForwarding yes + ClientAliveInterval 1200 + AllowAgentForwarding no + PermitEmptyPasswords no + ClientAliveCountMax 0 + GSSAPIAuthentication no + KerberosAuthentication no + IgnoreUserKnownHosts yes + PermitUserEnvironment no + ChallengeResponseAuthentication no + MACs hmac-sha2-512,hmac-sha2-256 + Ciphers aes128-ctr,aes192-ctr,aes256-ctr + +- name: regenerate SSH host keys + shell: | + rm -f /etc/ssh/ssh_host_*key* + ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" + ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N "" + args: + creates: /etc/ssh/ssh_host_ed25519_key + +- name: restart ssh + systemd: + name: ssh + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' + +- name: enable unattended-upgrades + shell: dpkg-reconfigure --priority=low unattended-upgrades + args: + creates: /etc/apt/apt.conf.d/50unattended-upgrades + +- name: restart unattended-upgrades + systemd: + name: unattended-upgrades + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' + +- name: disable ipv6 in grub + lineinfile: + path: /etc/default/grub + regexp: '^GRUB_CMDLINE_LINUX=' + line: 'GRUB_CMDLINE_LINUX="ipv6.disable=1"' + +- name: update grub + command: update-grub + +- name: create sshd fail2ban jail + copy: + src: fail2ban/jail.d/sshd.local + dest: "{{ fail2ban_jail_dir }}/sshd.local" + owner: root + group: root + mode: '0644' + +- name: copy fail2ban jail configuration + copy: + src: /etc/fail2ban/jail.conf + dest: /etc/fail2ban/jail.local + remote_src: true + mode: '0644' + +- name: allow ssh port and enable ufw + ufw: + rule: allow + port: 22 + proto: tcp + +- name: restart fail2ban + systemd: + name: fail2ban + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' + +- name: enable ufw + ufw: + state: enabled + policy: deny + +- name: restart ufw + systemd: + name: ufw + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' diff --git a/tasks/hugo_setup.yaml b/tasks/hugo_setup.yaml new file mode 100644 index 0000000..475dc8c --- /dev/null +++ b/tasks/hugo_setup.yaml @@ -0,0 +1,24 @@ +- name: download latest hugo .deb + shell: | + curl -sSL https://api.github.com/repos/gohugoio/hugo/releases | + awk -F '"' '/browser_download_url/ {print $4}' | + grep -i 'extended' | + grep -i 'linux-amd64' | + head -1 + register: latest_hugo_url + +- name: download hugo .deb package + get_url: + url: "{{ latest_hugo_url.stdout }}" + dest: /tmp/hugo.deb + mode: '0644' + +- name: install hugo package + apt: + deb: /tmp/hugo.deb + state: present + +- name: remove hugo .deb package + file: + path: /tmp/hugo.deb + state: absent diff --git a/tasks/icecast2_setup.yaml b/tasks/icecast2_setup.yaml new file mode 100644 index 0000000..ee8342d --- /dev/null +++ b/tasks/icecast2_setup.yaml @@ -0,0 +1,38 @@ +- name: ensure {{ radio_music_dir }} directory exists + file: + path: "{{ radio_music_dir }}" + state: directory + owner: icecast2 + group: icecast + mode: '0700' + +- name: deploy icecast.xml from template + template: + src: icecast2/icecast.xml.j2 + dest: /etc/icecast2/icecast.xml + owner: root + group: root + mode: '0644' + +- name: deploy ices-playlist.xml.j2 from template + template: + src: icecast2/ices-playlist.xml.j2 + dest: /etc/icecast2/ices-playlist.xml + owner: root + group: root + mode: '0644' + +- name: deploy mp3-to-ogg.sh from template + template: + src: icecast2/mp3-to-ogg.sh.j2 + dest: /etc/icecast2/mp3-to-ogg.sh + owner: root + group: root + mode: '0744' + +- name: restart icecast2 + systemd: + name: icecast2 + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' diff --git a/tasks/nginx_setup.yaml b/tasks/nginx_setup.yaml new file mode 100644 index 0000000..604db6d --- /dev/null +++ b/tasks/nginx_setup.yaml @@ -0,0 +1,47 @@ +- name: remove /etc/nginx/sites-enabled directory + file: + path: /etc/nginx/sites-enabled + state: absent + +- name: remove /etc/nginx/sites-available directory + file: + path: /etc/nginx/sites-available + state: absent + +- name: ensure /var/www/html directory exists and is empty + file: + path: /var/www/html + state: directory + mode: '0755' + owner: www-data + group: www-data + +- name: clean /var/www/html contents + file: + path: /var/www/html + state: absent + become: true + ignore_errors: true + +- name: recreate /var/www/html directory + file: + path: /var/www/html + state: directory + mode: '0755' + owner: www-data + group: www-data + +- name: deploy nginx.conf from template + template: + src: nginx/nginx.conf.j2 + dest: /etc/nginx/nginx.conf + owner: root + group: root + mode: '0644' + +- name: restart nginx + systemd: + name: nginx + state: restarted + enabled: true + when: ansible_service_mgr == 'systemd' diff --git a/tasks/preflight.yaml b/tasks/preflight.yaml new file mode 100644 index 0000000..3358d46 --- /dev/null +++ b/tasks/preflight.yaml @@ -0,0 +1,16 @@ +- name: ensure script is run as root + assert: + that: + - ansible_effective_user_id == 0 + fail_msg: "this playbook must be run as root" + +- name: check if system is debian-based + command: dpkg -l + register: dpkg_check + changed_when: false + failed_when: false + +- name: fail if not debian-based + fail: + msg: "distribution not Debian-based" + when: dpkg_check.rc != 0 diff --git a/templates/hugo.toml.j2 b/templates/hugo.toml.j2 new file mode 100644 index 0000000..85c0e3a --- /dev/null +++ b/templates/hugo.toml.j2 @@ -0,0 +1,13 @@ +baseURL = "https://{{ domain }}" +languageCode = "en-us" +title = "{{ username }}" +theme = "dos-theme" +publishDir = "/var/www/html/docs" +disableKinds = ["taxonomy", "RSS", "sitemap"] +disablePathToLower = true +disableHugoGeneratorInject = true + +[params] + contacts = [ + "{{ radio_email }}", + ] diff --git a/templates/icecast2/icecast.xml.j2 b/templates/icecast2/icecast.xml.j2 new file mode 100644 index 0000000..4c665b7 --- /dev/null +++ b/templates/icecast2/icecast.xml.j2 @@ -0,0 +1,65 @@ +<icecast> + <location>Earth</location> + <admin>{{ email }}</admin> + + <limits> + <clients>1000</clients> + <sources>2</sources> + <queue-size>524288</queue-size> + <client-timeout>30</client-timeout> + <header-timeout>15</header-timeout> + <source-timeout>10</source-timeout> + <burst-on-connect>1</burst-on-connect> + <burst-size>65535</burst-size> + </limits> + + <authentication> + <source-password></source-password> + <relay-password>disabled</relay-password> + <admin-user>admin</admin-user> + <admin-password>password</admin-password> + </authentication> + + <hostname>localhost</hostname> + + <listen-socket> + <port>8000</port> + <bind-address>127.0.0.1</bind-address> + </listen-socket> + + <http-headers> + <header name="Access-Control-Allow-Origin" value="*" /> + </http-headers> + + <mount> + <mount-name>/stream</mount-name> + <max-listeners>1000</max-listeners> + <public>1</public> + <no-yp>1</no-yp> + </mount> + + <fileserve>1</fileserve> + + <paths> + <basedir>/usr/share/icecast2</basedir> + <logdir>/var/log/icecast2</logdir> + <webroot>/usr/share/icecast2/web</webroot> + <adminroot>/usr/share/icecast2/admin</adminroot> + <alias source="/" destination="/status.xsl"/> + </paths> + + <logging> + <accesslog>access.log</accesslog> + <errorlog>error.log</errorlog> + <loglevel>2</loglevel> + <logsize>10000</logsize> + </logging> + + <security> + <chroot>1</chroot> + <changeowner> + <user>icecast2</user> + <group>icecast</group> + </changeowner> + </security> +</icecast> diff --git a/templates/icecast2/ices-playlist.xml.j2 b/templates/icecast2/ices-playlist.xml.j2 new file mode 100644 index 0000000..ae9a6be --- /dev/null +++ b/templates/icecast2/ices-playlist.xml.j2 @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<ices> + <background>1</background> + <logpath>/var/log/ices</logpath> + <logfile>ices.log</logfile> + <loglevel>4</loglevel> + <consolelog>1</consolelog> + + <!-- <pidfile>/home/ices/ices.pid</pidfile> --> + + <stream> + <metadata> + <name>Example stream name</name> + <genre>Example genre</genre> + <description>A short description of your stream</description> + </metadata> + + <input> + <module>playlist</module> + <param name="type">basic</param> + <param name="file">playlist.txt</param> + <param name="random">1</param> + <param name="restart-after-reread">0</param> + <param name="once">0</param> + </input> + + <instance> + <hostname>localhost</hostname> + <port>8000</port> + <password>password</password> + <mount>/stream</mount> + <yp>0</yp> + <reconnectdelay>60</reconnectdelay> + <reconnectattempts>10</reconnectattempts> + <maxqueuelength>80</maxqueuelength> + + <!--<encode> + <nominal-bitrate>64000</nominal-bitrate> + <samplerate>44100</samplerate> + <channels>2</channels> + </encode>--> + </instance> + </stream> +</ices> diff --git a/templates/icecast2/mp3-to-ogg.sh.j2 b/templates/icecast2/mp3-to-ogg.sh.j2 new file mode 100644 index 0000000..b37f5af --- /dev/null +++ b/templates/icecast2/mp3-to-ogg.sh.j2 @@ -0,0 +1,32 @@ +#!/bin/bash + +if ! command -v ffmpeg &>/dev/null; then + printf "%s\n" "[err] ffmpeg not found" + exit 1 +fi + +DIR="{{ radio_music_dir }}" + +shopt -s nullglob +for mp3file in "${DIR}"/*.mp3; do + oggfile="${mp3file%.mp3}.ogg" + + printf "%s\n" "[inf] converting ${mp3file} to ${oggfile}" + + if ffmpeg -loglevel error -y -i "${mp3file}" -acodec libvorbis -q:a 5 "${oggfile}"; then + printf "%s\n" "[inf] conversion successful, removing ${mp3file}" + rm -f "${mp3file}" + else + printf "%s\n" "[err] conversion failed for ${mp3file}" + fi +done + +ls "${DIR}"/*.ogg > "${DIR}/playlist.txt" + +if id -u icecast2 >/dev/null 2>&1 && getent group icecast >/dev/null 2>&1; then + chown -R icecast2:icecast "$DIR" + printf "%s\n" "[inf] chowned ${DIR} with icecast2:icecast" +else + printf "%s\n" "[err] user or group icecast2:icecast does not exist, skipping chown" +fi + diff --git a/templates/nginx/nginx.conf.j2 b/templates/nginx/nginx.conf.j2 new file mode 100644 index 0000000..7f1ac2b --- /dev/null +++ b/templates/nginx/nginx.conf.j2 @@ -0,0 +1,118 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 256; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + text/javascript + image/svg+xml; + + server { + listen 80; + server_name {{ domain }}; + return 301 https://{{ domain }}$request_uri; + } + + server { + listen 443 ssl http2; + server_name {{ domain }}; + + ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; + #ssl_trusted_certificate /etc/letsencrypt/live/{{ domain }}/chain.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; connect-src *; media-src * blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + root /var/www/html; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + location /stream { + proxy_pass http://localhost:8000/stream; + 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; + + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + + proxy_read_timeout 3600s; + chunked_transfer_encoding on; + + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, OPTIONS" always; + add_header Access-Control-Allow-Headers "Range" always; + add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always; + } + + location /admin { + deny all; + return 403; + } + + location /admin/ { + deny all; + return 403; + } + + location ~ ^/(status|statistics|server|webadmin) { + deny all; + return 403; + } + + access_log /var/log/nginx/icecast-access.log; + error_log /var/log/nginx/icecast-error.log warn; + } +} + diff --git a/templates/systemd/system/ices2.service b/templates/systemd/system/ices2.service new file mode 100644 index 0000000..d3a4c2f --- /dev/null +++ b/templates/systemd/system/ices2.service @@ -0,0 +1,17 @@ +[Unit] +Description=ICES2 Icecast source client +After=network.target icecast2.service +Requires=icecast2.service + +[Service] +Type=simple +User=icecast2 +Group=icecast +WorkingDirectory={{ radio_music_dir }} +ExecStart=/usr/bin/ices2 /etc/icecast2/ices-playlist.xml +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target + diff --git a/vars/main.yaml b/vars/main.yaml new file mode 100644 index 0000000..66e7e27 --- /dev/null +++ b/vars/main.yaml @@ -0,0 +1,14 @@ +apt_packages: + - curl + - ca-certificates + - fail2ban + - ffmpeg + - iptables + - icecast2 + - ices2 + - nginx + - ufw + - unattended-upgrades + - vim + +fail2ban_jail_dir: /etc/fail2ban/jail.d |