# Class to install a Mastodon instance # # [*ensure*] # String defining if is present or absent # # [*hostname*] # String with the full qualified hostname of the instance. There must be a DNS record for it. # # [*mastodon_home*] # String path to Mastodon user home directory. Default /opt/mastodon # # [*mastodon_version*] # String with Mastodon version (code tag) to install. Default: v4.2.1 # # [*mastodon_user*] # String with the system user name for mastodon. Default: mastodon # # [*mastodon_group*] # String with the system group name for mastodon. Default: mastodon # # [*ruby_version*] # String with the ruby version to use. Default: 3.2.2 # # [*mastodon_config*] # Hash with the configuration to store in .env.production # # [*db_host*] # String with database host name or socket. # # [*db_name*] # String with database name. # # [*db_user*] # String with database user name. # # [*db_password*] # String with database user password. # # [*db_port*] # Integer with database port. # # [*secret_key_base*] # String with secret key base. # # [*otp_secret*] # String with OTP (One-Time-Password) secret. # # [*vapid_private_key*] # String with VAPID private key # # [*vapid_public_key*] # String with VAPID public key # # [*users*] # List of hashes with users information. # class mastodon ( String $ensure = 'present', String $hostname = 'mastodon.example.org', String $mastodon_home = '/opt/mastodon', String $db_host = '/var/run/postgresql', String $db_name = 'mastodon', String $db_user = 'mastodon', String $db_password = 'S3cr3TP4ssw0rd', Integer $db_port = 5432, String $mastodon_version = 'v4.2.1', String $ruby_version = '3.2.2', String $mastodon_user = 'mastodon', String $mastodon_group = 'mastodon', String $secret_key_base = 'S3cr3tK3i', String $otp_secret = '0tpS3cr3t', String $vapid_private_key = 'S3cr3tK3i', String $vapid_public_key = 'S3cr3tK3i', Hash $config = { 'LOCAL_DOMAIN' => 'example.com', 'REDIS_HOST' => '127.0.0.1', 'REDIS_PORT' => 6379, 'ES_ENABLED' => 'false', 'ES_HOST' => 'localhost', 'ES_PORT' => 9200, 'ES_USER' => 'elastic', 'ES_PASS' => 'password', 'SMTP_SERVER' => '', 'SMTP_PORT' => 587, 'SMTP_LOGIN' => '', 'SMTP_PASSWORD' => '', 'SMTP_FROM_ADDRESS' => 'notifications@example.com', 'S3_ENABLED' => 'false', 'S3_BUCKET' => 'files.example.com', 'AWS_ACCESS_KEY_ID' => '', 'AWS_SECRET_ACCESS_KEY' => '', 'S3_ALIAS_HOST' => 'files.example.com', 'IP_RETENTION_PERIOD' => 31556952, 'SESSION_RETENTION_PERIOD' => 31556952, }, Array $users = [], ) { case $ensure { default: { $package_ensure = 'installed' $directory_ensure = 'directory' $link_ensure = 'link' $service_ensure = 'running' $file_ensure = 'present' } /^(absent|delete|uninstall|remove|unregister)$/: { $package_ensure = 'absent' $directory_ensure = 'absent' $link_ensure = 'absent' $file_ensure = 'absent' $service_ensure = 'stopped' $cron_ensure = 'absent' } } $packages = [ 'apt-transport-https', 'autoconf', 'bison', 'build-essential', 'ca-certificates', 'certbot', 'ffmpeg', 'file', 'g++', 'gcc', 'git-core', 'gnupg', 'imagemagick', 'libffi-dev', 'libgdbm-dev', 'libicu-dev', 'libidn11-dev', 'libjemalloc-dev', 'libncurses5-dev', 'libpq-dev', 'libprotobuf-dev', 'libreadline6-dev', 'libssl-dev', 'libxml2-dev', 'libxslt1-dev', 'libyaml-dev', 'lsb-release', 'nginx', 'pkg-config', # 'postgresql-contrib', 'protobuf-compiler', 'python3-certbot-apache', # 'redis-tools', 'wget', 'zlib1g-dev', ] $packages.each | $package | { if (!defined(Package[$package])) { package { $package: ensure => $package_ensure, } } } class { 'nodejs': repo_url_suffix => '16.x', } class { 'postgresql::server': } include redis exec { 'enable_corepack': command => '/usr/bin/corepack enable', creates => '/usr/bin/yarn', require => Class['nodejs'], } exec { 'yarn_classic': command => '/usr/bin/yarn set version classic', creates => '/root/.yarnrc', require => Exec['enable_corepack'], } group { $mastodon_group: } user { $mastodon_user: gid => $mastodon_group, home => $mastodon_home, managehome => true, system => true, require => Group[$mastodon_group], } vcsrepo { 'rbenv': path => "${mastodon_home}/.rbenv", source => 'https://github.com/rbenv/rbenv.git', provider => 'git', owner => $mastodon_user, group => $mastodon_group, require => User[$mastodon_user], } exec { 'configure_rbenv': command => "${mastodon_home}/.rbenv/src/configure", user => $mastodon_user, cwd => "${mastodon_home}/.rbenv/", creates => "${mastodon_home}/.rbenv/src/Makefile", require => Vcsrepo['rbenv'], } exec { 'make_rbenv': command => '/usr/bin/make -C src', user => $mastodon_user, cwd => "${mastodon_home}/.rbenv/", creates => "${mastodon_home}/.rbenv/libexec/rbenv", require => Exec['configure_rbenv'], } file_line { 'mastodon_path': path => "${mastodon_home}/.bashrc", line => 'export PATH="$HOME/.rbenv/bin:$PATH"', match => '^export PATH="$HOME/.rbenv', require => Vcsrepo['rbenv'], } file_line { 'mastodon_rbenv_init': path => "${mastodon_home}/.bashrc", line => 'eval "$(rbenv init -)"', match => '^eval "$(rbenv init -)"', require => Vcsrepo['rbenv'], } vcsrepo { 'ruby_build': path => "${mastodon_home}/.rbenv/plugins/ruby-build", source => 'https://github.com/rbenv/ruby-build.git', provider => 'git', owner => $mastodon_user, group => $mastodon_group, require => Vcsrepo['rbenv'], } if ($db_password != '') { postgresql::server::db { $db_name: user => $db_user, password => postgresql::postgresql_password($db_user, $db_password), grant => 'ALL', } } else { postgresql::server::db { $db_name: user => $db_user, grant => 'ALL', } } postgresql::server::database_grant { "${db_user}_${db_name}" : privilege => 'ALL', db => $db_name, role => $db_user, } vcsrepo { 'mastodon_code': path => "${mastodon_home}/live", source => 'https://github.com/mastodon/mastodon.git', revision => $mastodon_version, provider => 'git', owner => $mastodon_user, group => $mastodon_group, require => User[$mastodon_user], } file { '/usr/local/bin/install_mastodon.sh': ensure => $ensure, content => template('mastodon/install_mastodon.sh.erb'), mode => '0750', owner => $mastodon_user, group => 'root', require => [ Vcsrepo['mastodon_code'], Postgresql::Server::Db[$db_name], Vcsrepo['ruby_build'], ], } exec { 'install_mastodon': command => '/usr/local/bin/install_mastodon.sh', user => $mastodon_user, group => $mastodon_group, environment => [ "HOME=${mastodon_home}", 'LANG=C.UTF-8', "USER=${mastodon_user}", ], creates => "${mastodon_home}/./.mastodon_install", path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", timeout => 0, require => File['/usr/local/bin/install_mastodon.sh'], } $built_config = { 'LOCAL_DOMAIN' => $hostname, 'DB_PASS' => $db_password, 'DB_USER' => $db_user, 'DB_NAME' => $db_name, 'DB_PORT' => $db_port, 'DB_HOST' => $db_host, 'SECRET_KEY_BASE' => $secret_key_base, 'OTP_SECRET' => $otp_secret, 'ES_ENABLED' => 'true', 'ES_HOST' => '127.0.0.1', 'ES_PORT' => '9200', 'ES_PRESET' => '', 'ES_USER' => '', 'ES_PASS' => '', } $real_config = $config + $built_config file { "${mastodon_home}/live/.env.production": ensure => $ensure, content => template('mastodon/env.production.erb'), mode => '0640', owner => $mastodon_user, group => $mastodon_group, require => [ Vcsrepo['mastodon_code'], ], } # RAILS_ENV=production rails db:setup exec { 'db_setup': command => "${mastodon_home}/live/bin/rails db:setup > ${mastodon_home}/db_setup_done", creates => "${mastodon_home}/db_setup_done", path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", environment => [ 'RAILS_ENV=production', ], user => $mastodon_user, group => $mastodon_group, cwd => "${mastodon_home}/live", timeout => 0, require => File["${mastodon_home}/live/.env.production"], } # db:create # RAILS_ENV=production rails assets:precompile exec { 'assets_precompile': command => "${mastodon_home}/live/bin/rails assets:precompile > ${mastodon_home}/assets_precompile_done", creates => "${mastodon_home}/assets_precompile_done", path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", environment => [ 'RAILS_ENV=production', ], user => $mastodon_user, group => $mastodon_group, cwd => "${mastodon_home}/live", timeout => 0, require => File["${mastodon_home}/live/.env.production"], } exec { "register-${hostname}-letsencrypt": command => "/etc/init.d/apache2 stop && /usr/bin/certbot certonly --agree-tos --email certs@susurrando.com -d ${hostname} -n --standalone && /etc/init.d/apache2 start", creates => "/etc/letsencrypt/live/${hostname}/privkey.pem", require => Package['python3-certbot-apache'], } apache::vhost { $hostname: ensure => $ensure, access_log_file => $hostname, add_listen => false, error_log_file => $hostname, docroot => "${mastodon_home}/live/public", manage_docroot => false, proxy_preserve_host => true, proxy_add_headers => true, port => 443, priority => 15, protocols => [ 'h2', 'http/1.1', ], protocols_honor_order => true, proxy_requests => false, proxy_pass => [ { 'path' => '/500.html', 'url' => '!' }, { 'path' => '/sw.js', 'url' => '!' }, { 'path' => '/robots.txt', 'url' => '!' }, { 'path' => '/manifest.json', 'url' => '!' }, { 'path' => '/browserconfig.xml', 'url' => '!' }, { 'path' => '/mask-icon.svg', 'url' => '!' }, ], custom_fragment => ' ServerSignature Off ProxyPass /api/v1/streaming ws://localhost:4000 ProxyPassReverse /api/v1/streaming ws://localhost:4000 ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ ', proxy_pass_match => [ { 'path' => '^(/.*\.(png|ico)$)', 'url' => '!' }, { 'path' => '^/(assets|avatars|emoji|headers|packs|sounds|system)', 'url' => '!' }, ], request_headers => [ 'set X-Forwarded-Proto "https"', ], headers => [ 'always set Strict-Transport-Security "max-age=31536000"', 'always set Strict-Transport-Security "max-age=15552001; includeSubDomains"', ], directories => [ { 'path' => '^/(assets|avatars|emoji|headers|packs|sounds|system)', 'provider' => 'locationmatch', 'headers' => 'always set Cache-Control "public, max-age=31536000, immutable"', 'deny' => 'from all', 'require' => 'all granted', }, { 'path' => '/', 'provider' => 'location', 'require' => 'all granted', }, ], error_documents => [ { 'error_code' => '500', 'document' => '/500' }, { 'error_code' => '501', 'document' => '/501' }, { 'error_code' => '502', 'document' => '/502' }, { 'error_code' => '503', 'document' => '/503' }, { 'error_code' => '504', 'document' => '/504' }, ], ssl => true, ssl_cert => "/etc/letsencrypt/live/${hostname}/fullchain.pem", ssl_cipher => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA', ssl_honorcipherorder => true, ssl_protocol => 'all -SSLv3 -SSLv2 -TLSv1 -TLSv1.1', ssl_key => "/etc/letsencrypt/live/${hostname}/privkey.pem", ssl_proxy_check_peer_cn => 'on', ssl_proxy_check_peer_expire => 'on', ssl_proxyengine => true, ssl_reload_on_change => true, allow_encoded_slashes => 'on', require => Exec["register-${hostname}-letsencrypt"], } apache::vhost { "${hostname}_insecure": ensure => $ensure, servername => $hostname, access_log_file => "${hostname}_insecure", error_log_file => "${hostname}_insecure", add_listen => false, ip => '0.0.0.0', port => 80, docroot => "${mastodon_home}/live/public", redirect_status => 'permanent', redirect_dest => "https://${hostname}/", custom_fragment => 'ServerSignature Off', } systemd::unit_file { 'mastodon-sidekiq.service': ensure => present, content => template('mastodon/mastodon-sidekiq.service.erb'), active => true, enable => true, } systemd::unit_file { 'mastodon-streaming.service': ensure => present, content => template('mastodon/mastodon-streaming.service.erb'), active => true, enable => true, } systemd::unit_file { 'mastodon-streaming@.service': ensure => present, content => template('mastodon/mastodon-streaming@.service.erb'), # active => true, enable => true, } systemd::unit_file { 'mastodon-web.service': ensure => present, content => template('mastodon/mastodon-web.service.erb'), active => true, enable => true, } $users.each | $user | { if ($user['confirmed']) { $confirmed = '--confirmed' } else { $confirmed = '' } exec { "create_user_${user['username']}": command => "${mastodon_home}/live/bin/tootctl accounts create '${user['username']}' --email '${user['email']}' ${confirmed} --role '${user['role']}' > '${mastodon_home}/create_user_${user['username']}'", creates => "${mastodon_home}/create_user_${user['username']}", path => "${mastodon_home}/.rbenv/shims:${mastodon_home}/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", environment => [ 'RAILS_ENV=production', ], user => $mastodon_user, group => $mastodon_group, cwd => "${mastodon_home}/live", timeout => 0, require => File["${mastodon_home}/live/.env.production"], } } include opensearch # Maintenance tasks # tootctl maintenance fix-duplicates # tootctl media remove --days 180 # tootctl media remove-orphans # tootctl preview_cards remove --days 180 }