OpenVPN Server憑證配置與連線設定
===
###### tags: `Linux`
## 憑證配置
### OpenVPN憑證模型
OpenVPN的憑證模型算簡單的, 憑證鍊最少只需要兩層
基本的憑證結構是由CA簽署出server端跟client端的憑證

因此實作上最少只需要三份憑證(ca, server, client), 以及各自的私鑰
### 憑證的用途
舉例來說, server要如何判斷client是可連入的對象呢?
雖然server跟client沒有信任關係, 但是server信任CA(server的憑證是由CA簽發),
所以只要client的憑證是由相同的CA簽發的, 那就可以認為這個client是可信任的對象(允許連入)
反過來說, 如果client的憑證使用CA的公鑰驗證簽章失敗, 代表該client不是可信任對象(禁止連入)
因此:
CA的腳色就是server與client共同信任的對象, 只負責簽發憑證,
但會提供ca.crt讓其他腳色可以驗證要驗的對象是否被CA信任
對server來說, 需要的檔案是ca.crt, server.crt, server.key
對client來說, 需要的檔案是ca.crt, client.crt, client.key

不過在這邊會以兩份CA憑證當作範例, 其實結構也是類似的
只是CA鍊多了一張中繼憑證(intermediate), 當要簽發憑證時並不直接使用rootCA,
而是讓下一層的signingCA代為簽署

為求使用簡單且兼顧安全性跟高效, 憑證配置會以ECDSA取代RSA, 而且資料加密只使用AEAD算法
環境配置以TLS 1.3為目標, OpenVPN限定2.5/2.6, 實作環境為Ubuntu 24.04
且為了高度客製憑證(三層鍊+ECDSA, 還有自定義的X509v3 extensions),
因此接下來使用的憑證產生工具不使用easy-rsa, 而是使用彈性較高的openssl
### 建立憑證鏈
首先先建立一張自簽憑證rootCA, 這邊要注意的兩點:
1. basicConstraints的CA必須為TRUE
2. keyUsage必須有keyCertSign跟cRLSig兩項
```
# 建立rootCA的key-pair
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out root.key
# 建立自簽憑證root.crt
$ openssl req -x509 -new -key root.key -sha384 -days 3650 -out root.crt \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" \
-addext "authorityKeyIdentifier=keyid:always,issuer"
```
再來建立中繼憑證signingCA, 由rootCA簽發, 這邊的注意點跟rootCA相同,
但可以在basicConstraints多加pathlen=0, 代表這張憑證無法再往下簽發CA憑證
```
# 建立signingCA的key-pair
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out signing.key
# 建立CSR
$ openssl req -new -key signing.key -sha384 -out signing.csr
# 使用root的私鑰簽署CSR, 得到signing.crt
$ openssl x509 -req -in signing.csr -CA root.crt -CAkey root.key -CAcreateserial \
-days 3650 -sha384 \
-extfile <(printf "[v3_ca]\n\
basicConstraints=critical,CA:TRUE,pathlen:0\n\
keyUsage=critical,keyCertSign,cRLSign\n\
subjectKeyIdentifier=hash\n\
authorityKeyIdentifier=keyid:always,issuer\n") \
-extensions v3_ca -out signing.crt
```
rootCA跟中繼憑證都必須提供出去, 這樣才算是完整的CA chain
我們可以把兩份CA憑證都放在同一個檔案, 方便使用
順序的話, 中繼憑證先, 再接根憑證
```
# 將rootCA與signingCA合併成一個CA憑證鏈ca.crt
$ cat signing.crt root.crt > ca.crt
```
接下來要建立OpenVPN server需要的憑證, 由server端產生CSR後丟給CA簽署
注意簽署時必須加入keyUsage=digitalSignature跟extendedKeyUsage=serverAuth這兩項,
以及basicConstraints的CA必須為FALSE (不能是CA憑證)
```
# 建立server的key-pair
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out server.key
# 建立CSR
$ openssl req -new -key server.key -sha384 -out server.csr
# 使用signingCA的私鑰簽署CSR, 得到server.crt
$ openssl x509 -req -in server.csr -CA signing.crt -CAkey signing.key -CAcreateserial \
-days 825 -sha384 \
-extfile <(printf "[v3_server]\n\
basicConstraints=critical,CA:FALSE\n\
keyUsage=critical,digitalSignature\n\
extendedKeyUsage=serverAuth\n\
subjectKeyIdentifier=hash\n\
authorityKeyIdentifier=keyid,issuer\n") \
-extensions v3_server -out server.crt
```
接下來要建立OpenVPN client需要的憑證, 由client端產生CSR後丟給CA簽署
注意簽署時必須加入keyUsage=digitalSignature跟extendedKeyUsage=clientAuth這兩項,
以及basicConstraints的CA必須為FALSE (不能是CA憑證)
```
# 建立client的key-pair
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out client.key
# 建立CSR
$ openssl req -new -key client.key -sha384 -out client.csr
# 使用signingCA的私鑰簽署CSR, 得到client.crt
$ openssl x509 -req -in client.csr -CA signing.crt -CAkey signing.key -CAcreateserial \
-days 825 -sha384 \
-extfile <(printf "[v3_client]\n\
basicConstraints=critical,CA:FALSE\n\
keyUsage=critical,digitalSignature\n\
extendedKeyUsage=clientAuth\n\
subjectKeyIdentifier=hash\n\
authorityKeyIdentifier=keyid,issuer\n") \
-extensions v3_client -out client.crt
```
到這邊所需要的憑證就都準備完成了 保險一點可以加驗憑證的信任鍊
```
# 驗證server與CA的信任鍊
$ openssl verify -show_chain -CAfile ca.crt server.crt
# 驗證client與CA的信任鍊
$ openssl verify -show_chain -CAfile ca.crt client.crt
```
## 伺服器配置
### OpenVPN server configuration
OpenVPN Server的配置文件預設是放在/etc/openvpn/server/
在設定之前,我們可以考慮使用TLS Authentication
我們先來產生一把ta.key, 這把key可以讓我們把控制通道的封包整個加密,
如果沒有也沒關係, 只是在握手階段時比較容易被掃到是使用OpenVPN通訊
如果有加密, 那握手階段的封包會長得很像隨機資料, 會更難辨識或是阻擋
```
$ openvpn --genkey secret ta.key
```
接下來產生一個server.conf放在預設路徑下, conf的內容如下
值得一提的是我們使用了secp384r1, 這種ECDHE算法可以在交換金鑰後直接算出shared secret,
因此並不需要dh檔, 所以在這邊dh就設為none
data-ciphers我們也只留下AEAD算法, AEAD本身就有HMAC可以做驗證, 因此不需額外的設置
不加入AES-CBC是因為這種算法缺少了HMAC做驗證, 會需要額外設置auth的方式
(AEAD具備完整的加解密與資料驗證的功能, CBC只能加解密)
```
port 1194
proto udp4
dev tun
server 10.8.0.0 255.255.255.0
topology subnet
# CA鏈:Intermediate(中繼) + Root
ca ca.crt
cert server.crt
key server.key
# 資料通道(AEAD),相容 2.5/2.6
data-ciphers AES-256-GCM:CHACHA20-POLY1305
data-ciphers-fallback AES-256-GCM
dh none
tls-version-min 1.3
# 控制通道(TLS Auth)要使用的key
tls-crypt ta.key
# 連線穩定性 & 安全性
keepalive 10 60
persist-key
persist-tun
user nobody
group nogroup
# 推送路由,可選,需看網路環境做更改
push "route 192.168.50.0 255.255.255.0"
# UDP 關閉通知(Windows/RouterOS 客戶端友好)
explicit-exit-notify 1
# 用 systemd journal 看 log;除錯時再調整 verb
verb 3
```
接下來把需要的憑證跟key也複製到這個路徑下:
```
$ ls
ca.crt server.conf server.crt server.key ta.key
```
這樣server端的OpenVPN配置就完成了
### Linux服務與網路配置
首先安裝openvpn套件
```
sudo apt install openvpn
```
systemd service相關的指令
```
sudo systemctl start openvpn-server@server
sudo systemctl enable openvpn-server@server
sudo systemctl status openvpn-server@server
```
Linux預設不會幫你轉發封包, 如果client要送的封包對象不是server,
那傳到server時封包就會被直接丟棄
因此如果client想要存取server區網上的其他設備, 那就要讓server開啟路由的功能
以Ubuntu 24.04來說, 我們需要設定net.ipv4.ip_forward=1, 方法如下:
建立以下檔案
```
$ sudo vi /etc/sysctl.d/99-ipforward.conf
```
之後在檔案內增加以下一行
```
net.ipv4.ip_forward=1
```
存檔後重載設定讓新設定生效
```
$ sudo sysctl -p /etc/sysctl.d/99-ipforward.conf
```
可以用以下指令驗證是否開啟, 有開啟的話回傳值會是1
```
$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
```
現在封包轉發是可以了, 但還有個問題
VPN client的ip網段是10.8.0.0/24, 去程沒問題 但回程的話如果沒有特別設定路由,
當封包要往10.8.0.0/24送的時候就會不知道要往哪送了
一種解法是設定路由器的靜態路由, 在server的上層router增加對10.8.0.0/24的路由規則,
假設server的private IP是192.168.50.101, 那麼規則如下:
```
Host IP: 10.8.0.0
Netmask: 255.255.255.0
Gateway: 192.168.50.101
Interface: LAN
```
另一種解法是不修改上層router, 而是設定iptables把VPN client(10.8.0.0/24)發往區網的封包,
全部偽裝成從server的private IP(192.168.50.101)發出
```
$ sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j SNAT --to-source 192.168.50.101
```
透過這種方式, 區網其他電腦以為"是server要連我", 所以封包就會回覆給server(192.168.50.101),
然後server再把回覆轉給VPN client
不過這樣設定重開機後就會揮發掉, 要永久生效的話還需要做幾個步驟:
```
$ sudo apt install iptables-persistent
$ sudo iptables-save | sudo tee /etc/iptables/rules.v4
```
下次開機時, iptables-persistent就會自動載入/etc/iptables/rules.v4
## 使用者配置
### OpenVPN client profile
先將需要的檔案準備好, 檔案如下
ca.crt跟ta.key都是必須要與server使用的相同
```
ca.crt client.crt client.key ta.key
```
接下建立一個.ovpn的OpenVPN profile, 內容如下
檔案開頭的x.x.x.x需更換成server的public ip
檔案下方的內嵌憑證區塊就直接把對應的檔案內容複製貼上
```
client
dev tun
proto udp4
remote x.x.x.x 1194
resolv-retry infinite
nobind
persist-key
persist-tun
keepalive 10 60
remote-cert-tls server
tls-version-min 1.3
data-ciphers AES-256-GCM:CHACHA20-POLY1305
data-ciphers-fallback AES-256-GCM
tls-crypt ta.key
verb 3
<ca>
-----BEGIN CERTIFICATE-----
# ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
# client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
# client.key
-----END PRIVATE KEY-----
</key>
<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
# ta.key
-----END OpenVPN Static key V1-----
</tls-crypt>
```
設定檔配置完成後若client是Linux可以使用
```
$ sudo openvpn --config client.ovpn
```
若是Windows則是使用OpenVPN Connect for Windows將.ovpn import進去後就可連線
:diamond_shape_with_a_dot_inside:Cyui