summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--inventory.yaml.example15
-rw-r--r--main.yaml11
-rw-r--r--tasks/apt_packages.yaml11
-rw-r--r--tasks/harden.yaml161
-rw-r--r--tasks/hugo_setup.yaml24
-rw-r--r--tasks/icecast2_setup.yaml38
-rw-r--r--tasks/nginx_setup.yaml47
-rw-r--r--tasks/preflight.yaml16
-rw-r--r--templates/hugo.toml.j213
-rw-r--r--templates/icecast2/icecast.xml.j265
-rw-r--r--templates/icecast2/ices-playlist.xml.j244
-rw-r--r--templates/icecast2/mp3-to-ogg.sh.j232
-rw-r--r--templates/nginx/nginx.conf.j2118
-rw-r--r--templates/systemd/system/ices2.service17
-rw-r--r--vars/main.yaml14
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