Misskeyのおひとりさまサーバを建ててみた

## はじめに
六葩くるです。
いままでおひとりさまサーバつくるつくる詐欺して全然作ってなかったので、そろそろ #MisskeyInstallBattle に参戦しようかと思い作成してみました。
これはその見様見真似な構築手順の備忘録になります。
Linuxなにもわからない™のにちょっとだけめんどくさいことしているので、どこかの箇所が誰かの参考になればと。

## 今回の構成
手順やソフトウェアについては公式のインストール方法(Dockerなし)をある程度参考にした上で、Fedoraで実施している。

https://misskey-hub.net/docs/install/ubuntu-manual.html

### VPS環境
[KAGOYA CLOUD](https://www.kagoya.jp/vps/)を使用。
| 項目 | 値 |
|:-|:-|
| OS | Fedora 39 Beta |
| CPU | 2コア |
| メモリ | 2 GB |
| ストレージ | 200GB |

### DNS
[Gandi.net](https://www.gandi.net)で取得。

### 作業時点の各ソフトウェアのバージョン
Nginxは一応QUIC対応にするためビルドする。
| ソフトウェア | バージョン |
|:-|:-|
| Misskey | v2023.10.1 |
| Node.js | v20.8.1 |
| PostgreSQL | 15.4 |
| Redis | 7.2.1 |
| FFmpeg | 6.0 |
| Nginx | 1.25.2 w/ quictls |

### オブジェクトストレージ
[Wasabi](https://web.arena.ne.jp/wasabi/)を使用。

### 証明書
[ZeroSSL](https://zerossl.com/)を使用。

### メールボックス
[さくらのメールボックス](https://rs.sakura.ad.jp/mail/)を使用。

### 使用しないもの
- Docker
- Cloudflare

## OSインストール

### VPSインスタンスの作成
[KAGOYA CLOUD VPS コントロールパネル](https://vps.kagoya.com/login)にログインする。
テンプレートにないパッケージを使用するため、FedoraのISOイメージをアップロードする。
[Fedoraのダウンロードページ](https://fedoraproject.org/ja/server/download/)のISOの実URLをコピペする。
ISOイメージは1日で消えるので名前はなんでもいい。

![rezjr97uum9u96g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3580902/9a66f95f-7ab4-cc89-2df7-eb7f4f4189d9.png)

:::note warn
他のOSイメージを使用する場合
KAGOYAのイメージはBIOSで実行されるため、UEFIでしか動作しないカスタムISO(CentOS Stream 9など)は起動できません。
:::

インスタンス作成画面に移動し、ISOイメージにアップロードしたイメージを選択。
スペックにCPU 2コア / メモリ 2GB / ストレージ 200GB のものを選択。(今回はオブジェクトストレージを使用するので、大容量のものでなくてもよかったかもしれない)
virtioはONのままでOK。
コンソールログインパスワードとインスタンス名(VPS管理用のためなんでもいい)を入力し、作成する。
インスタンスが作成されたらインスタンス情報に移動し、付与されたIPv4・IPv6を確認しておく。

### DNSレコード登録
#### 正引き
[Gandi.net](https://admin.gandi.net)にログインし、対象のドメインにタイプAレコード(IPv4)とタイプAAAAレコード(IPv6)を追加する。
TTLはデフォルトのままでOK。

#### 逆引き
KAGOYAのインスタンス情報の逆引き設定のところにドメイン名を入力。

### インストレーション
コンソールを起動すると、ポップアップでコンソールが表示される。
ブラウザによってはポップアップを開く際に一度ブロックされるので、許可してあげた上でもう一度起動すること。
あとわりとすぐ画面がタイムアウトするので、都度F5やCtrl+Rでリロードするか、ウィンドウを開き直すこと。

Fedoraのインストール画面が表示されていることを確認する。

#### 言語設定
日本語にすると多分ちょっと面倒なのでEnglishのまま続行。

![hfyk9gapjgb3t23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3580902/95c4c918-f46c-6a71-6b90-705100fe7aaa.png)

#### INSTALLATION SUMMARY
以下、設定する順に記載する。

![uxxknkz79537ytd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3580902/27401b88-8356-af8c-994f-a0ce60794dea.png)

##### Keyboard
自身で使用しているキーボードに応じて English (US) または Japanese を選択。
(実際にはほぼSSHで操作するため最初のパスワード入力以外はあまり関係ない)

##### Language Support
一応、 日本語(日本) を追加する。

##### Software Selection
Fedora Server Edition が選択されていることを確認。

##### Installation Destination
Customを選択しDone。
パーティション作成画面に移動するのでまずはLVMで自動作成(Click here to create them automatically)する。
BIOS Boot, /boot, / の3つのパーティションが自動で作成される。
メモリ2GBだとMisskeyをビルドするのに足りないので、「+」でswapを作成する。今回はせっかく大容量の方のストレージにしてるのでちょっと多めに8GiBで作った。
あとは / パーティションのDesired Capacityをめっちゃ多い量で記入してUpdateすると、残りの全容量が自動で割り当てられる。

![aj86nb8ie8atm7z.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3580902/30f4a635-51e0-e200-7c20-e23434ca142a.png)

##### Network & Hostname
(Ethernet (ens3) が選択されている状態で)Configureを押下。

###### IPv4 Settings

| 項目 | 値 |
|:-|:-|
| Method | Manual |
| Address | インスタンス画面のIPv4のIP |
| Netmask | インスタンス画面のサブネット |
| Gateway | インスタンス画面のゲートウェイ |
| DNS servers | インスタンス画面のDNS、<br>またはお好きなDNSサーバのIPをカンマ(,)形式で入力 |

###### IPv6 Settings

| 項目 | 値 |
|:-|:-|
| Method | Manual |
| Address | インスタンス画面のIPv6のIP |
| Prefix | 64 |
| Gateway | IPv6の先頭4つ部分に::1を付加 (xxxx:yyyy:zzzz:wwww::1) |
| DNS servers | お好きなDNSサーバのIPをカンマ(,)形式で入力 |

入力が完了したら保存して有効化。DNSレコード登録が反映されていれば、Current host nameのところにドメイン名が表示される。
kagoyaなんちゃらが表示された場合は、使用したいドメイン名をHost Nameのところに入力してApply。

##### Time & Date
日本のあたりをクリック。Asia/Tokyoになることと、Network Timeが有効になっていることを確認。

##### User Creation
User name と Password を入力。チェックボックスに2つともチェックが入っていることを確認。

##### Begin Installation
以上の設定でBegin Installationが押せるようになるので、インストールを開始。
完了したらReboot Systemで再起動を実施。
インストールされたOSが無事上がってくることを確認する。

## SSH接続設定

手持ちのターミナルソフトでSSH接続し、セキュリティ強化のためSSHのポート番号変更と公開鍵認証への変更を行う。

### 公開鍵認証化
#### 鍵の追加

- クライアント側で鍵を作成する(手順省略)
- サーバにSCPなどを用いて作成した公開鍵をリモートに転送する(手順省略)
- 以下コマンドで`authorized_keys`に鍵を登録
```bash
mkdir ~/.ssh
cat (持ってきたファイル) >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
rm (持ってきたファイル)
```
- 公開鍵認証で対応する秘密鍵を指定し、SSH接続できることを確認する

#### パスワードログイン、rootログインの禁止
- `/etc/ssh/sshd_config.d/`内にファイルを作成し、追記
```/etc/ssh/sshd_config.d/99-sshd_config.conf
PermitRootLogin no
PasswordAuthentication no
```
- `sudo systemctl restart sshd.service`でSSHサーバを再起動し、今のセッションを維持したまま別セッションでログインできることを確認する

### ポート番号変更

#### ポート番号追加
##### sshd_config変更
- `/etc/ssh/sshd_config.d/`内のファイルに追記
```diff:/etc/ssh/sshd_config.d/99-sshd_config.conf
+ Port 22
+ Port (変更後のポート番号)
PermitRootLogin no
PasswordAuthentication no
```

##### ファイアウォール追加
- sshのサービスファイルを複製する
```bash
sudo cp /usr/lib/firewalld/services/ssh.xml /etc/firewalld/services/ssh-alt.xml
```
- `ssh-alt.xml`を変更
```diff:/etc/firewalld/services/ssh-alt.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
- <short>SSH</short>
+ <short>SSH-ALT</short>
  <description>Secure Shell (SSH) is a protocol for logging into and executing commands on remote machines. It provides secure encrypted communications. If you plan on accessin    g your machine remotely via SSH over a firewalled interface, enable this option. You need the openssh-server package installed for this option to be useful.</description>
- <port protocol="tcp" port="22"/>
+ <port protocol="tcp" port="(変更後のポート番号)"/>
</service>
```
- 以下でファイアウォールルール追加
```bash
# ssh-altを反映
sudo firewall-cmd --reload
sudo firewall-cmd --get-services | grep ssh-alt

# ssh-altをファイアウォールルールに追加
sudo firewall-cmd --permanent --add-service=ssh-alt
sudo firewall-cmd --reload

# 確認
sudo firewall-cmd --list-all
```
##### SELinuxルール追加
- 以下実行
```bash
sudo semanage port --add --type ssh_port_t --proto tcp (変更後のポート番号)
```
- `sudo systemctl restart sshd.service`でSSHサーバを再起動し、変更後のポート番号でログインできることを確認する

#### ポート番号削除
##### sshd_config変更
- `/etc/ssh/sshd_config.d/`内のファイルを変更
```diff:/etc/ssh/sshd_config.d/99-sshd_config.conf
- Port 22
Port (変更後のポート番号)
PermitRootLogin no
PasswordAuthentication no
```

##### ファイアウォール削除

- 以下でファイアウォールルール削除
```bash
# sshをファイアウォールルールから削除
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload

# 確認
sudo firewall-cmd --list-all
```
- `sudo systemctl restart sshd.service`でSSHサーバを再起動し、デフォルトのポート番号でログインできないことを確認する

## その他ファイアウォール変更
### Cockpit無効化
```bash
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload
sudo systemctl disable --now cockpit.socket
```

### HTTP通信を通せるようにする
#### ファイアウォールルール
`http3`が定義されていない場合は`443/udp`を`add-port`で通すようにする。
```bash
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-service=http3
sudo firewall-cmd --reload
```

#### SELinuxルール
```bash
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_can_network_relay 1
sudo semanage port --add --type http_port_t --proto udp 443
```

### fail2ban
一応いれておく。
```bash
sudo dnf install fail2ban
sudo systemctl enable --now fail2ban.service
```

## Nginx構築
NginxをQUIC対応にする場合、パッケージで入るnginxだと組み込みのOpenSSL(QUIC非対応)で動作するためQUICが動かない。
そのためQUIC対応のSSLパッケージを組み込んだnginxをビルドする必要がある。
また、Wasabiのストレージはパブリックアクセスができない(以下参照)ため、AWSの認証アクセス機能を何らかの方法で組み込む必要がある。

https://blog.noellabo.jp/entry/wasabi-public-access

今回はOpenSSLの代わりに[quictls](https://github.com/quictls/openssl)を使い、AWSの認証用に[ngx_aws_auth](https://github.com/anomalizer/ngx_aws_auth)モジュールを使用する。

### 基本パッケージのインストール
ビルドに必要な汎用的なパッケージを入れる。個々のビルドで必要なパッケージは都度記載する。
```bash
sudo dnf groupinstall 'Development Tools'
sudo dnf install cmake
```

### quictlsの導入
#### kTLSの有効化
TLSの処理をカーネルに任せるとパフォーマンスが向上するらしいのでちょっと入れてみる。
```bash
sudo modprobe tls
```
#### 必要なパッケージのインストール
```bash
sudo dnf install perl wget gcc gcc-arm-linux-gnu tcp_wrappers
```
#### ソースダウンロード
```bash
cd /usr/local/src
sudo git clone https://github.com/quictls/openssl.git
cd openssl
sudo git submodule update --init
```
#### インストール
```bash
sudo ./Configure enable-ktls
sudo make
sudo make install
```
#### ライブラリのパスを通す
`/usr/local/lib64`とかのパスがなんか通っていないので、通す必要がある。
- `/etc/ld.so.conf.d/`内にファイルを作成し、追記
```/etc/ld.so.conf.d/99-ld.so.conf
/usr/local/lib
/usr/local/lib64
```
- `sudo ldconfig`で反映
- `openssl version`を実行し、バージョンに`+quic`がついていることを確認する

quictlsのソースはNginxのビルド時に用いるため、残しておくこと。

### AWS proxyモジュールの用意
Wasabiはパブリックアクセスができないのに、SigV4での認証もうまくいかないことがあるらしい。というかうまくいかなかった。

https://forum.seafile.com/t/aws-v4-signatures-not-working-with-s3-compatible-backend-wasabi/11086/3

仕方がないのでSigV2で認証できるようにするため、モジュールをV2のブランチの方に変更する。

```bash
cd /usr/local/src
sudo git clone https://github.com/anomalizer/ngx_aws_auth.git
cd ngx_aws_auth
sudo git checkout AuthV2
```
ただしこれ古いプログラムなので、このままだと今のNginxのビルド時に動かない。
以下を実行して事前にコードを一部書き換えておく。
```bash
sudo sed -i -e 's/\(ngx_http_variable_unknown_header(\)\(val\)/\1r, \2/' ngx_http_aws_auth_module.c
```


### Brotli圧縮モジュールの用意
通信量が減るらしい？のでせっかくなので入れてみる。

https://twinturbo-power.com/2022/08/%E3%80%90nginx%E3%80%91brotli%E5%9C%A7%E7%B8%AE%E3%82%92%E4%BD%BF%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F.html

```bash
cd /usr/local/src
sudo git clone https://github.com/google/ngx_brotli.git
cd ngx_brotli
sudo git submodule update --init
```

### Nginxのインストール

#### ユーザー追加
```bash
sudo useradd --system --comment 'Nginx web server' --home-dir /var/local/lib/nginx --shell /sbin/nologin nginx
```

#### 必要なパッケージのインストール
無駄にconfigureオプションを盛ったため必要なパッケージが増えている。不要ならオプションを削ってもいいかも。
```bash
sudo dnf install pcre-devel libxslt-devel gd-devel GeoIP-devel gperftools
```

#### ソースダウンロード
[公式のダウンロードサイト](http://nginx.org/en/download.html)からバージョンを確認し、該当するものをダウンロードする。
```bash
cd /usr/local/src
sudo curl -O https://nginx.org/download/nginx-1.25.2.tar.gz
sudo tar xvf nginx-1.25.2.tar.gz
cd nginx-1.25.2
```

#### configure実行
試行錯誤の結果、以下のオプションで実行した。詳細の説明はあまりよくわかってないので割愛。
```bash
sudo ./configure \
--prefix=/var/local/nginx \
--sbin-path=/usr/local/sbin/nginx \
--modules-path=/usr/local/lib64/nginx/modules \
--conf-path=/usr/local/etc/nginx/nginx.conf \
--error-log-path=/var/local/log/nginx/error.log \
--http-log-path=/var/local/log/nginx/access.log \
--http-client-body-temp-path=/var/local/lib/nginx/tmp/client_body \
--http-proxy-temp-path=/var/local/lib/nginx/tmp/proxy \
--http-fastcgi-temp-path=/var/local/lib/nginx/tmp/fastcgi \
--http-uwsgi-temp-path=/var/local/lib/nginx/tmp/uwsgi \
--http-scgi-temp-path=/var/local/lib/nginx/tmp/scgi \
--pid-path=/run/nginx.pid \
--lock-path=/run/lock/nginx.lock \
--user=nginx \
--group=nginx \
--with-threads \
--with-file-aio \
--with-debug \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_v3_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_xslt_module=dynamic \
--with-http_image_filter_module=dynamic \
--with-http_geoip_module=dynamic \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_auth_request_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_degradation_module \
--with-http_slice_module \
--with-http_stub_status_module \
--with-http_perl_module=dynamic \
--with-mail=dynamic \
--with-mail_ssl_module \
--with-stream=dynamic \
--with-stream_realip_module \
--with-stream_geoip_module=dynamic \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-google_perftools_module \
--with-compat \
--with-pcre \
--with-pcre-jit \
--with-openssl=/usr/local/src/openssl \
--with-openssl-opt=enable-ktls \
--add-module=/usr/local/src/ngx_brotli \
--add-module=/usr/local/src/ngx_aws_auth \
--with-cc-opt='-g -O2 -m64 -mtune=generic -fPIC -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=3 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-all -fstack-clash-protection -fcf-protection -grecord-gcc-switches -I/usr/local/src/openssl/include' \
--with-ld-opt='-no-pie -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -Wl,-Bsymbolic-functions -L/usr/local/src/openssl/ssl'
```

#### make
ビルドしてインストール。
```bash
sudo make
sudo make install
```

### SELinuxコンテキスト修正
`unconfined_service_t`とかになっているとSELinuxを動かしている意味があんまりない気がするので、`dnf`で入れたときと同じような形になるようにコンテキストを修正する。
#### 関連フォルダの作成
```bash
sudo mkdir -p /usr/local/etc/nginx/conf.d
sudo mkdir -p /usr/local/etc/nginx/default.d
sudo mkdir -p /usr/local/lib64/nginx/modules
sudo mkdir -p /var/local/cache/nginx
sudo mkdir -p /var/local/lib/nginx/tmp/client_body
sudo mkdir -p /var/local/lib/nginx/tmp/proxy
sudo mkdir -p /var/local/lib/nginx/tmp/fastcgi
sudo mkdir -p /var/local/lib/nginx/tmp/uwsgi
sudo mkdir -p /var/local/lib/nginx/tmp/scgi
```

#### コンテキスト変更
```bash
sudo semanage fcontext -a -f f -s system_u -t httpd_exec_t '/usr/local/sbin/nginx'
sudo semanage fcontext -a -f a -s system_u -t httpd_config_t '/usr/local/etc/nginx(/.*)?'
sudo semanage fcontext -a -f a -s system_u -t httpd_sys_content_t '/var/local/nginx/html(/.*)?'
sudo semanage fcontext -a -f a -s system_u -t httpd_var_lib_t '/var/local/lib/nginx(/.*)?'
sudo semanage fcontext -a -f a -s system_u -t httpd_log_t '/var/local/log/nginx(/.*)?'
sudo semanage fcontext -a -f a -s system_u -t httpd_cache_t '/var/local/cache/nginx(/.*)?'

# 確認
sudo semanage fcontext -l | grep nginx
```

#### コンテキスト反映
```bash
sudo restorecon -vF /usr/local/sbin/nginx
sudo restorecon -vFR /usr/local/etc/nginx
sudo restorecon -vFR /var/local/nginx/html
sudo restorecon -vFR /var/local/lib/nginx
sudo restorecon -vFR /var/local/log/nginx
sudo restorecon -vFR /var/local/cache/nginx
```

### nginx.confの編集
ビルド時に生成される`nginx.conf`はパスとかがデフォルトのままなので、ちゃんと書き換えてあげないと動かない。
なんかもうめっちゃ変えた™(`dnf`で入るnginxの形式にある程度合わせただけ)のでファイルを丸々以下のものに置き換えること。

```/usr/local/etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;
error_log  /var/local/log/nginx/error.log notice;
pid  /run/nginx.pid;

# Load dynamic modules.
include  /usr/local/lib64/nginx/modules/*.conf;

events {
    worker_connections  1024;
}

http {
    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  /var/local/log/nginx/access.log  main;

    sendfile             on;
    tcp_nopush           on;
    keepalive_timeout    65;
    types_hash_max_size  4096;

    charset utf-8;

    include              /usr/local/etc/nginx/mime.types;
    default_type         application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    include  /usr/local/etc/nginx/conf.d/*.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /var/local/nginx/html;

        # Load configuration files for the default server block.
        include  /usr/local/etc/nginx/default.d/*.conf;

        #error_page  404              /404.html;
        #location = /404.html {
        #}

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
        }
    }
}
```
`sudo nginx -t`の結果が`successful`になることを確認する。

### systemdのユニットファイル作成
以下のファイルを作成する。

(🗒2023-10-22 追記: `Restart=always`を入れないとOS再起動時にこけるので下部の`misskey.service`の記載と同じものを追記しました。)

```/etc/systemd/system/nginx.service
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/local/sbin/nginx -t
ExecStart=/usr/local/sbin/nginx
ExecReload=/usr/local/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
TimeoutSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nginx
Restart=always

[Install]
WantedBy=multi-user.target
```
`sudo systemctl enable --now nginx.service`でで起動、常駐化し、ブラウザから`http://(ドメイン名)`でアクセス可能なことを確認する。

## 証明書発行
以下を参考に、ZeroSSLの証明書をlegoを用いて発行する。

https://zenn.dev/mattn/articles/b2c4c92c9116b1

### ZeroSSL API Keyの入手
- [ZeroSSL](https://zerossl.com/)のアカウントを登録する
- 登録完了後、DeveloperタブよりAPI Keyを確認する

### legoのインストール
スクリプト用にJSONプロセッサも入れる。
```bash
sudo dnf install golang-github-acme-lego jq
```

### 証明書発行スクリプトの作成
以下のスクリプトを作成する。(場所はわりとどこでもOK)
```/usr/local/bin/lego-invoke.sh
#!/bin/bash

API_KEY=(ZeroSSLで取得したAPIキー)
EMAIL=(ZeroSSLに登録したメールアドレス？)
DOMAIN=(認証したいドメイン名)
WEBROOT_PATH=/var/local/nginx/html
CERTKEY_PATH=/etc/zerossl

JSON=$(curl -s -X POST "https://api.zerossl.com/acme/eab-credentials?access_key=$API_KEY")
EAB_KID=$(echo "$JSON" | jq -r .eab_kid)
EAB_HMAC_KEY=$(echo "$JSON" | jq -r .eab_hmac_key)

lego --http --http.webroot "$WEBROOT_PATH" --path "$CERTKEY_PATH" --eab --kid "$EAB_KID" --hmac "$EAB_HMAC_KEY" --server "https://acme.zerossl.com/v2/DV90" --domains "$DOMAIN" --email "$EMAIL" --accept-tos "$1"
```

### 証明書発行
```bash
sudo chmod a+x /usr/local/bin/lego-invoke.sh
sudo lego-invoke.sh run
```

### Misskey用のnginx設定の用意
公式の[Nginxの設定](https://misskey-hub.net/docs/admin/nginx.html)をベースに、QUIC対応、kTLS対応、Brotli圧縮対応を行ったもの。
オブジェクトストレージへのプロキシの記述は後ほど追記する。

```/usr/local/etc/nginx/conf.d/misskey.conf
# For WebSocket
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

proxy_cache_path /var/local/cache/nginx levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;

server {
    listen 80;
    listen [::]:80;
    server_name (ドメイン名);

    # For SSL domain validation
    root /var/local/nginx/html;
    location /.well-known/acme-challenge/ { allow all; }
    location /.well-known/pki-validation/ { allow all; }
    location / { return 301 https://$server_name$request_uri; }
}

server {
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name (ドメイン名);

    # QUIC settings
    http3 on;
    http3_hq on;
    http2 on;
    quic_retry on;
    ssl_early_data on;

    ssl_session_timeout 1d;
    ssl_session_cache shared:ssl_session_cache:10m;
    ssl_session_tickets off;

    # To use ZeroSSL certificate
    ssl_certificate     /etc/zerossl/certificates/(ドメイン名).crt;
    ssl_certificate_key /etc/zerossl/certificates/(ドメイン名).key;

    # To use Let's Encrypt certificate
    #ssl_certificate     /etc/letsencrypt/live/example.tld/fullchain.pem;
    #ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;

    # To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
    #ssl_certificate     /etc/ssl/certs/ssl-cert-snakeoil.pem;
    #ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    # SSL protocol settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256: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_prefer_server_ciphers off;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_conf_command Options KTLS;

    # Change to your upload limit
    client_max_body_size 80m;

    # brotli setting
    brotli on;
    brotli_comp_level 7;
    brotli_static on;
    brotli_types application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml audio/mpeg font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;

    # Proxy to S3 storage

    # Proxy to Node
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_redirect off;

        # If it's behind another reverse proxy or CDN, remove the following.
        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 https;

        # For WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Cache settings
        proxy_cache cache1;
        proxy_cache_lock on;
        proxy_cache_use_stale updating;
        proxy_force_ranges on;
        add_header X-Cache $upstream_cache_status;

        # To negotiate with QUIC directly
        add_header Alt-Svc 'h3=":443"; h3-29=":443"; ma=86400';
    }
}

```
- `sudo nginx -t`の結果が`successful`になることを確認する。
- `sudo systemctl restart nginx.service`でサービスを再起動する。
- ブラウザから`https://(ドメイン名)`でアクセスし、ZeroSSLで認証されていることを確認する。(現時点ではリクエスト結果は`502`エラーでOK)

## Misskey構築準備

### ユーザー作成
```bash
sudo useradd --system --comment 'Misskey app server' --home-dir /opt/misskey --shell /bin/bash misskey
```

### Node.jsのインストール
```bash
# インストール
sudo dnf install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm
sudo dnf install nodejs --setopt=nodesource-nodejs.module_hotfixes=1

# バージョン確認
node -v

# corepack enable
sudo corepack enable
```

### PostgreSQLの導入
#### インストール
```bash
sudo dnf install postgresql postgresql-server postgresql-upgrade
sudo postgresql-setup --initdb

# バージョン確認
postgres --version

# 実行
sudo systemctl enable --now postgresql.service
```

#### ユーザーとデータベースの作成
```bash
sudo -u postgres psql
CREATE ROLE misskey LOGIN PASSWORD '(pgsqlのmisskeyユーザ用のパスワード)';
CREATE DATABASE (データベース名) OWNER misskey;
\q
```

#### 接続設定
このままだとパーミッション的にデータベースアクセスがうまくいかないので、`all all`の行の`METHOD`を`trust`に変更する。
```diff:/var/lib/pgsql/data/pg_hba.conf
# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
- local   all             all                                     peer
+ local   all             all                                     trust
# IPv4 local connections:
- host    all             all             127.0.0.1/32            ident
+ host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
- host    all             all             ::1/128                 ident
+ host    all             all             ::1/128                 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     peer
host    replication     all             127.0.0.1/32            ident
host    replication     all             ::1/128                 ident
```

#### DBのチューニング
[PGTune](https://pgtune.leopard.in.ua/)を用いることで、使用しているマシンのスペックに合わせてPostgreSQL用のチューニング設定を得ることができる。
今回は以下の通り入力し、出力結果を確認した。

| 項目 | 値 |
|:-|:-|
| DB version | 15 |
| Total Memory (RAM) | 2GB |
| Number of CPUs | 2 |
| Number of Connections | 100 |

得られた出力結果をもとに、`/var/lib/pgsql/data/postgresql.conf`の対象行を編集していく。
順序がバラバラなので注意。

| 設定値 | 行番号 |
|:-|-:|
| max_connections | 65行目 |
| shared_buffers | 127行目 |
| effective_cache_size | 403行目 |
| maintenance_work_mem | 140行目 |
| checkpoint_completion_target | 238行目 |
| wal_buffers | 226行目 |
| default_statistics_target | 426行目 |
| random_page_cost | 395行目 |
| effective_io_concurrency | 187行目 |
| work_mem | 138行目 |
| huge_pages | 129行目 |
| min_wal_size | 241行目 |
| max_wal_size | 242行目 |

編集が完了したら、`systemctl restart postgresql.service`で再起動する。


### Redisのインストール
```bash
sudo dnf install redis
sudo systemctl enable --now redis

# バージョン確認
redis-server --version
```

### FFmpegのインストール
```bash
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
sudo dnf install ffmpeg

# バージョン確認
ffmpeg -version
```

### jemallocの確認
メモリアロケータをjemallocに変えるとMisskey v13のメモリ問題が緩和されるらしいのでjemallocを使う想定にする。
以下のコマンドで入っていることを確認する。
```bash
sudo find / -name "*jemalloc*"
```
入っていない場合は`sudo dnf install jemalloc`で入れる。

## Misskeyのインストール
### Misskeyのダウンロード
```
sudo mkdir /opt/misskey
sudo chown misskey:misskey /opt/misskey
sudo -iu misskey
cd /opt
git clone --recursive https://github.com/misskey-dev/misskey.git
cd misskey
git checkout master
git submodule update --init
NODE_ENV=production pnpm install --frozen-lockfile
```


### default.ymlの設定
以下のファイルを作成する。

```/opt/misskey/.config/default.yml
# Misskey URL
url: https://(ドメイン名)/
port: 3000

# PostgreSQL setting
db:
  host: localhost
  port: 5432
  db  : (PostgreSQLのデータベース名)
  user: (PostgreSQLのユーザー名)
  pass: (PostgreSQLのパスワード)

# Redis setting
redis:
  host: localhost
  port: 6379

# ID type
id: 'aid'

# syslog setting
syslog:
  host: localhost
  port: 514

# proxy setting
proxyRemoteFiles: true

# ActivityPub setting
signToActivityPubGet: true
```

### Misskeyのビルド
```bash
cd /opt/misskey
NODE_ENV=production pnpm run build
pnpm run init
```

### 起動テスト
```bash
NODE_ENV=production pnpm run start
```
ブラウザでアクセスできることを確認する。


### systemdのユニットファイル作成
以下のファイルを作成する。
```/etc/systemd/system/misskey.service
[Unit]
Description=Misskey daemon

[Service]
Type=simple
User=misskey
ExecStart=/usr/bin/npm start
WorkingDirectory=/opt/misskey
Environment="NODE_ENV=production"
Environment="LD_PRELOAD=/usr/lib64/libjemalloc.so.2"
TimeoutSec=60
StandardOutput=journal
StandardError=journal
SyslogIdentifier=misskey
Restart=always

[Install]
WantedBy=multi-user.target
```
`sudo systemctl enable --now misskey.service`で起動、常駐化する。

## Misskeyの設定
### 初期設定
- 最初の管理ユーザーを作成
- 「コントロールパネル」→「設定」→「モデレーション」
    - 「誰でも新規登録できるようにする」のチェックを外す
    - 「アカウント登録にメールアドレスを必須にする」のチェックを入れる
    - 「利用規約URL」「プライバシーポリシーURL」を入力する
        - 使わない場合でもなんらかの値をいったん入れないと保存できないので、いったん入れて保存後に空欄にして保存し直す
    - 「予約ユーザー名」に「media」を追加
- 「コントロールパネル」→「設定」→「全般」
    - 「サーバー名」「管理者の名前」「管理者のメールアドレス」「運営者情報URL」を入力する
        - 使わない場合でも(以下略)
- 「コントロールパネル」→「設定」→「その他」
    - 「サーバーのマシン情報を公開する」のチェックを入れる
        - 反映にはMisskeyの再起動が必要


### メール配信機能

独自ドメインのがそれっぽいのでGandi.netのメールボックス機能を使おうと思っていたが、じきに無料じゃなくなるらしい。

https://news.gandi.net/en/2023/06/faq-new-mailbox-service/

有料になるくらいなら、それよりは安いさくらのメールボックスでメールサーバのみホストし、ドメインは移管せずに利用することにした。

https://def-4.com/sakuranomailserver/

#### メールアドレスの追加

- [さくらインターネット](https://secure.sakura.ad.jp/)にログインし、「契約中のサービス一覧」→「さくらのメールボックス」よりコントロールパネルを開く
- 「メール」→「メール一覧」→「新規追加」より、メール配信に使用したいユーザー名とパスワードでメールアドレスを追加する
- 「(ユーザー名)@ドメイン」が追加されることを確認

#### ドメインの追加
- 「ドメイン/SSL」→「ドメイン/SSL」→「ドメイン新規追加」を選択
- 「他社で取得したドメインを移管せずに使う」を選択
- 「他社で取得した独自ドメインの追加」のところに契約しているドメインを入力し、追加

#### DNSレコード設定
- 追加したドメインの「設定」に、「SPFレコードの使用」にチェックが入っていることを確認し、「ドメインコントロールパネル」に遷移する
- 「登録済みゾーン」→「ゾーン >」にあるTXTレコード(SPFレコードのやつ)のテキストをコピー
- Gandi.netに移動し、「DNSレコード」の「詳細表示」よりレコードを直接編集
```diff:
+ @ 10800 IN MX 0 xxx.sakura.ne.jp.(xxxはSPFレコードについているwwwなんちゃらを入れる)
+ @ 10800 IN TXT (コピーしてきたSPFレコード)
- @ 10800 IN MX 10 spool.mail.gandi.net.
- @ 10800 IN MX 50 fb.mail.gandi.net.
- @ 10800 IN TXT "v=spf1 include:_mailcust.gandi.net ?all"
- _imap._tcp 10800 IN SRV 0 0 0   .
- _imaps._tcp 10800 IN SRV 0 1 993 mail.gandi.net.
- _pop3._tcp 10800 IN SRV 0 0 0   .
- _pop3s._tcp 10800 IN SRV 10 1 995 mail.gandi.net.
- _submission._tcp 10800 IN SRV 0 1 465 mail.gandi.net.
- gm1._domainkey 10800 IN CNAME gm1.gandimail.net.
- gm2._domainkey 10800 IN CNAME gm2.gandimail.net.
- gm3._domainkey 10800 IN CNAME gm3.gandimail.net.
- webmail 10800 IN CNAME webmail.gandi.net.
```

#### Misskey側の設定
- 「コントロールパネル」→「設定」→「メールサーバー」
    - 「メール配信機能を有効化する」にチェックを入れる
- 以下の通り設定

| 項目 | 値 |
|:-|:-|
| メールアドレス | (ユーザー名)@(独自ドメイン名) |
| ホスト | (さっきのMXレコードに入れたやつ、末尾の.は不要) |
| ポート | 587 |
| ユーザー名 | (メールアドレスと同じ) |
| パスワード | (メールアカウントのパスワード) |

- 保存し、配信テストを実施し、テストメールが届くことを確認する
    - 数分くらい時間かかるかも

### オブジェクトストレージの設定

#### Wasabiの設定

(🗒2023-11-02 追記: ログ用プレフィックスでフォルダを切れるっぽいので1つのフォルダに入るよう修正しました。)

- [Wasabi コンソール](https://console.wasabisys.com/login)にログインし、Bucketを作成
    - 一応プロパティはバケットのログだけ有効にし、ログ用プレフィックスに「`log/`」を指定しておいた

:::note warn
リージョンにTokyoを指定した場合はap-northeast-1ではなく「ap-northeast-1-ntt」が、
Osakaを指定した場合はap-northeast-2ではなく「ap-northeast-2-ntt」が正しいリージョン名になる。
:::

- 「アクセスキー」→「Create Access Key」より、ルートユーザーでキーを作成
    - アクセスキーと秘密鍵の両方を控えておく

#### Nginxの設定
オブジェクトストレージへのプロキシを以下のように追記する。

```/usr/local/etc/nginx/conf.d/misskey.conf
    # Proxy to S3 storage
    location /media/ {
        rewrite /(.*) /(バケット名)/$1 break;

        # If it's behind another reverse proxy or CDN, remove the following.
        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 https;

        # AWS compatible authentication with Signature Version 2
        proxy_pass http://s3.(リージョン名).wasabisys.com;
        proxy_set_header Host s3.(リージョン名).wasabisys.com;

        aws_access_key (アクセスキー);
        aws_secret_key (秘密鍵);
        s3_bucket (バケット名);
        chop_prefix /(バケット名);

        proxy_set_header Authorization $s3_auth_token;
        proxy_set_header x-amz-date $aws_date;
    }
```
- `sudo nginx -t`の結果が`successful`になることを確認する。
- `sudo systemctl restart nginx.service`でサービスを再起動する。

#### Misskeyの設定
- 「コントロールパネル」→「設定」→「オブジェクトストレージ」より「オブジェクトストレージを使用」にチェックを入れ、以下の通り設定する。

| 項目 | 値 |
|:-|:-|
| Base URL | (Misskeyと同じURL) |
| Bucket | (バケット名) |
| Prefix | media |
| Endpoint | (proxy_passのところに書いたURL) |
| Region | (リージョン名) |
| Access Key | (アクセスキー) |
| Secret Key | (秘密鍵) |
| SSLを使用する | 有効 |
| Proxyを利用する | 有効 |
| アップロード時に'public-read'を設定する | 無効 |
| s3ForcePathStyle | 有効 |

保存後、ドライブにファイルを上げ、読み書きができることを確認する。

### その他の機能

#### ServiceWorker
以下の手順でPublic KeyとPrivate Keyを生成し、設定する。

```bash
sudo npm install web-push -g
web-push generate-vapid-keys
```

#### Botプロテクション

一般公開しないため不要な気もするが、一応[hCaptcha](https://www.hcaptcha.com/)を利用する。
- アカウントを登録し、サイトキーとシークレットキーを控えておき、Misskeyで入力

#### DeepL API

不要な気もするが、一応使ってみる。
- [DeepL](https://www.deepl.com)でアカウントを登録
- 「アカウント」→「アカウント」→「DeepL APIで使用する認証キー」のキーを控えておき、Misskeyで入力

#### プロキシアカウントとかロール割り当てとかについて

事前に何でもいいのでノートを投稿してからじゃないと検索に出てこないらしいね。

## まとめ
まだたったいまサーバを建てたばかりなので、これからおひとりさまに住む感じです。
あと運用面についてはまだ全然調べられてないので、気が向いたら追々。

## 🗒2024/10/13 追記: アップデート自動化について
おひとりさまサーバーはもう閉鎖してしまったのですが、当初書いてたアップデートのコードが1年放置してても一応動作していたっぽいので、ついでの備忘録として以下に記しておきます。ただしずっと動く保証はしません。

### 自動アプデスクリプト
以下のファイルを配置し、実行権限をつける。

```/usr/bin/local/auto-update-misskey.sh
#!/bin/bash

# enable beta version
GET_BETA=false

# rollback function
pnpm_rollback () {
  sudo -u misskey git switch --detach "refs/tags/$1"
  sudo -u misskey git submodule update --init
  env NODE_ENV=production sudo -Eu misskey pnpm install --frozen-lockfile
  sudo -u misskey pnpm run clean
  env NODE_ENV=production sudo -Eu misskey pnpm run build
  sudo -u misskey pnpm run migrate
}

# check certification renewal
/usr/local/bin/lego-invoke.sh renew

# package upgrade
if ! dnf upgrade --assumeyes; then exit 1; fi

# misskey update check
cd /opt/misskey
if ! sudo -u misskey git fetch; then exit 1; fi

LOCAL_VERSION=$(sudo -u misskey git ls-remote --tags 2>/dev/null | grep ^$(sudo -u misskey git show --no-patch --format=%H) | cut -d '/' -f 3)
if "$GET_BETA"; then
  REMOTE_VERSION=$(sudo -u misskey git for-each-ref refs/tags/ --sort='-committerdate' --count=1 --format='%(refname:short)')
else
  REMOTE_VERSION=$(sudo -u misskey git ls-remote --tags 2>/dev/null | grep ^$(sudo -u misskey git ls-remote --heads 2>/dev/null | grep refs/heads/$(sudo -u misskey git branch --contains | cut -d ' ' -f 2)\$ | awk '{print $1}') | cut -d '/' -f 3)
fi

# misskey upgrade
if [ "$LOCAL_VERSION" != "$REMOTE_VERSION" ]; then
  systemctl stop misskey
  if "$GET_BETA"; then
    sudo -u misskey git switch --detach "refs/tags/$REMOTE_VERSION"
  else
    sudo -u misskey git switch master
    sudo -u misskey git pull
  fi
  sudo -u misskey git submodule update --init

  env NODE_ENV=production sudo -Eu misskey pnpm install --frozen-lockfile
  if [ $? -eq 0 ] ; then
    sudo -u misskey pnpm run clean
    env NODE_ENV=production sudo -Eu misskey pnpm run build
    if [ $? -eq 0 ] ; then
      sudo -u misskey pnpm run migrate
    else
      pnpm_rollback "$LOCAL_VERSION"
    fi
  else
    pnpm_rollback "$LOCAL_VERSION"
  fi
  
  # restart misskey or restart os
  if ! dnf needs-restarting --reboothint; then
    systemctl reboot
  else
    systemctl restart misskey
  fi

# restart os
elif ! dnf needs-restarting --reboothint; then
  systemctl reboot
fi
```

### タイマー化
以下の2つのファイルを配置する。
```/etc/systemd/system/check-update-misskey.service
[Unit]
Description=Check Update Misskey

[Service]
Type=oneshot
Restart=no
ExecStart=/usr/local/bin/auto-update-misskey.sh

[Install]
WantedBy=multi-user.target
```

```/etc/systemd/system/check-update-misskey.timer
[Unit]
Description=Check Update Misskey Timer

[Timer]
OnCalendar=3:20
Unit=check-update-misskey.service
 
[Install]
WantedBy=timers.target
```
配置したら、以下の手順で動作確認する。
- `sudo systemctl daemon-reload` で読み込み
- `sudo systemctl enable --now check-update-misskey.timer` で有効化する
- `systemctl list-timers` で起動確認
- `journalctl -fu check-update-misskey.service` で動作確認