
OpenVPN自动化配置与访问日志
OpenVPN自动化配置与访问日志
OpenVPN 是内网访问和安全隧道的核心工具。但随着用户数的增加,手动添加、分发证书、撤销权限、记录访问等操作变得既繁琐又容易出错。
本文将分享我实现的一个 OpenVPN 自动化管理方案:
[前端表单] → [审批接口] → [后端 API] → [VPN 服务器脚本] → [.ovpn 文件 + 日志]
在内网面板前端加了一个弹窗用来收集信息到数据库
管理人员前端面板可以通过和拒绝,这部分需要自己根据情况来实现
核心实现
自动化创建用户脚本 create_ovpn.sh
主要功能:
- 生成用户证书(Expect 自动化交互)
- 分配静态 IP
- 修改 OVPN 配置参数(路由 / 协议 / MTU)
- 添加 iptables 规则记录访问日志
- 配置 rsyslog 按用户分文件记录
关键点:
- 使用
expect
模拟输入,避免人工选择菜单 - 通过
/etc/openvpn/ccd/$USERNAME
固定 IP iptables -m conntrack --ctstate NEW -m limit
限制日志频率- rsyslog 动态创建配置文件,实时生效
#!/bin/bash
USERNAME=$1
PASSWORD=$2
VPN_IP=192.168.85.$3
SCRIPT_PATH="/home/ec2-user/openvpn-install.sh"
VPN_LOG_DIR="/var/log/vpn"
RSYSLOG_CONF="/etc/rsyslog.d/$USERNAME.conf"
IPTABLES_RULES_FILE="/etc/iptables/rules.v4"
# 检查 openvpn-install.sh 是否存在
if [ ! -f "$SCRIPT_PATH" ]; then
echo "错误: $SCRIPT_PATH 不存在"
exit 1
fi
chmod +x "$SCRIPT_PATH"
# 自动生成用户 OVPN
/usr/bin/expect <<EOF
spawn "$SCRIPT_PATH"
expect "Select an option \[1-4\]:"
send "1\r"
expect "Client name:"
send "$USERNAME\r"
expect "Select an option \[1-2\]:"
send "\0252\r"
expect "Enter the password for the client:"
send "$PASSWORD\r"
expect "Verifying \- Enter PEM pass phrase:"
send "$PASSWORD\r"
expect eof
EOF
mv /root/*.ovpn /home/ec2-user/
OVPN_FILE="/home/ec2-user/$USERNAME.ovpn"
if [ -f "$OVPN_FILE" ]; then
echo "成功生成 OVPN 文件: $OVPN_FILE"
sed -i "/^verb 3$/r /dev/stdin" "$OVPN_FILE" <<EOF
route-nopull
max-routes 1000
route 172.30.0.0 255.255.0.0 vpn_gateway
route 172.31.0.0 255.255.0.0 vpn_gateway
tun-mtu 1500
mssfix 1400
EOF
sed -i 's/^proto udp/proto tcp/' "$OVPN_FILE"
sed -i 's/13\.251\.110\.51/47.129.111.202/' "$OVPN_FILE"
else
echo "错误: OVPN 文件生成失败"
exit 1
fi
# 配置静态IP
echo "ifconfig-push $VPN_IP 255.255.255.0" > /etc/openvpn/ccd/$USERNAME
# 优化后的 iptables 日志规则(仅记录新连接,并限制速率)
if ! iptables -C FORWARD -s "$VPN_IP" -m conntrack --ctstate NEW -m limit --limit 10/sec --limit-burst 20 -j LOG --log-prefix "${USERNAME}_ACCESS: " --log-level 4 2>/dev/null; then
iptables -I FORWARD -s "$VPN_IP" -m conntrack --ctstate NEW -m limit --limit 10/sec --limit-burst 20 -j LOG --log-prefix "${USERNAME}_ACCESS: " --log-level 4
echo "已添加 iptables 日志规则: $VPN_IP"
else
echo "iptables 日志规则已存在: $VPN_IP"
fi
# 保存 iptables
iptables-save > "$IPTABLES_RULES_FILE"
if [ $? -eq 0 ]; then
echo "成功保存 iptables 规则到 $IPTABLES_RULES_FILE"
else
echo "错误: 保存 iptables 规则失败"
exit 1
fi
# 创建日志目录
mkdir -p "$VPN_LOG_DIR"
# 创建 rsyslog 配置
echo '' > "$RSYSLOG_CONF"
printf ':msg, contains, "%s" %s/%s.log\n' "${USERNAME}_ACCESS" "$VPN_LOG_DIR" "$USERNAME" >> "$RSYSLOG_CONF"
echo '& stop' >> "$RSYSLOG_CONF"
# 重启 rsyslog
echo "重启 rsyslog 服务以应用新配置"
systemctl restart rsyslog
if [ $? -eq 0 ]; then
echo "成功配置 rsyslog 日志到 $VPN_LOG_DIR/$USERNAME.log"
else
echo "错误: rsyslog 服务重启失败"
exit 1
fi
自动化删除用户脚本 delete.sh
主要功能:
- Expect 自动选择菜单“撤销用户”
- 如果用户不存在,也会清理
index.txt
避免残留 - 脚本健壮性:超时 / 格式检查 / 异常提示
关键点:
- 正则匹配菜单选项找到用户索引
- 确保删除证书后清理 Easy-RSA 索引文件
#!/usr/bin/expect -f
set timeout 30
set SCRIPT "/home/ec2-user/openvpn-install.sh"
set USER_TO_REVOKE [lindex $argv 0]
# 检查是否传递了用户名参数
if { [llength $argv] == 0 } {
send_user "Usage: $argv0 <username>\n"
exit 1
}
# 验证用户名格式
if { ![regexp {^[a-zA-Z0-9_-]+$} $USER_TO_REVOKE] } {
send_user "Error: Username must consist of alphanumeric characters, underscore, or dash.\n"
exit 1
}
# 启动脚本
spawn $SCRIPT
expect {
"Welcome to OpenVPN-install!" {
send_user "OpenVPN-install started successfully.\n"
}
eof {
send_user "openvpn-install.sh terminated unexpectedly.\n"
exit 1
}
timeout {
send_user "Timeout waiting for openvpn-install.sh to start.\n"
exit 1
}
}
# 选择“Revoke existing user”
expect {
-re "What do you want to do\\?" {
send_user "Matched 'What do you want to do?' prompt.\n"
send "2\r"
}
timeout {
send_user "Timeout waiting for 'What do you want to do?' prompt.\n"
exit 1
}
}
# 处理无用户情况
expect {
"You have no existing clients!" {
send_user "No existing clients found.\n"
# 清理 index.txt 即使没有用户
send_user "Cleaning up $USER_TO_REVOKE from index.txt...\n"
set cmd "sed -i '/\\/CN=${USER_TO_REVOKE}\$/d' /etc/openvpn/easy-rsa/pki/index.txt"
exec bash -c $cmd
send_user "Cleanup completed.\n"
exit 0
}
-re "Select the existing client certificate you want to revoke" {
send_user "Client list displayed successfully.\n"
}
timeout {
send_user "Timeout waiting for client list.\n"
exit 1
}
}
# 捕获用户列表
expect {
-re "\r\n(.+)\r\nSelect" {
set user_list $expect_out(1,string)
send_user "Captured user list:\n$user_list\n"
}
timeout {
send_user "Timeout waiting for user list.\n"
exit 1
}
}
# 查找目标用户名
set user_index -1
set lines [split $user_list "\n"]
foreach line $lines {
if {[string match "*$USER_TO_REVOKE*" $line]} {
if {[regexp {^\s*(\d+)\)\s+.*$} $line match user_number]} {
set user_index $user_number
send_user "Found user '$USER_TO_REVOKE' with index $user_index\n"
}
}
}
# 如果找不到用户
if {$user_index == -1} {
send_user "User '$USER_TO_REVOKE' not found in the list.\n"
send_user "Cleaning up $USER_TO_REVOKE from index.txt...\n"
set cmd "sed -i '/\\/CN=${USER_TO_REVOKE}\$/d' /etc/openvpn/easy-rsa/pki/index.txt"
exec bash -c $cmd
send_user "Cleanup completed.\n"
exit 0
}
# 输入编号
send "$user_index\r"
# 等待撤销确认
expect {
-re "Certificate for client $USER_TO_REVOKE revoked\\." {
send_user "User '$USER_TO_REVOKE' has been revoked successfully.\n"
send_user "Cleaning up $USER_TO_REVOKE from index.txt...\n"
set cmd "sed -i '/\\/CN=${USER_TO_REVOKE}\$/d' /etc/openvpn/easy-rsa/pki/index.txt"
exec bash -c $cmd
send_user "Cleanup completed.\n"
}
timeout {
send_user "Timeout waiting for revocation confirmation.\n"
exit 1
}
}
后端 API(Flask)
提供两个主要接口:
POST /create-vpn
→ 调用create_ovpn.sh
POST /delete
→ 调用delete.sh
实现细节:
- 使用
subprocess.run
调用外部脚本,限制执行时间 - 捕获
stdout
/stderr
返回给前端 - 状态码区分(200 成功 / 400 用户已存在 / 404 用户不存在 / 500 失败)
代码核心部分:
from flask import Flask, request, jsonify
from flask_cors import CORS
import subprocess
import logging
import os
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
)
app = Flask(__name__)
# 配置 CORS
CORS(app, resources={
r"/create-vpn": {"origins": "http://localhost:4200"},
r"/delete": {"origins": "http://localhost:4200"}
})
@app.route('/create-vpn', methods=['POST'])
def create_vpn():
data = request.get_json()
logging.debug(f"收到请求体: {data}")
username = data.get('username')
password = data.get('password')
vpn_ip = data.get('vpn_ip')
if not username or not password or not vpn_ip:
logging.warning("缺少用户名、密码或 VPN IP")
return jsonify({'message': '用户名、密码和 VPN IP 是必需的'}), 400
script_path = '/root/create_ovpn.sh'
command = f"bash {script_path} {username} {password} {vpn_ip}"
logging.info(f"准备执行命令: {command}")
try:
env = os.environ.copy()
env['PATH'] = f"/usr/bin:/bin:{env.get('PATH', '')}"
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60,
cwd='/home/ec2-user',
env=env
)
logging.debug(f"stdout: {result.stdout}")
logging.debug(f"stderr: {result.stderr}")
if result.returncode == 0:
logging.info("脚本执行成功")
return jsonify({
'message': 'VPN 配置生成成功',
'vpnFilePath': f'/home/ec2-user/{username}.ovpn',
'stdout': result.stdout
}), 200
elif result.returncode == 2:
logging.warning(f"用户 {username} 已存在")
return jsonify({
'message': f'用户 {username} 已存在',
'stderr': result.stderr,
'stdout': result.stdout
}), 400
else:
logging.error("脚本执行失败")
return jsonify({
'message': '脚本执行失败',
'stderr': result.stderr,
'stdout': result.stdout
}), 500
except subprocess.TimeoutExpired:
logging.error("脚本执行超时")
return jsonify({'message': '脚本执行超时'}), 500
except Exception as e:
logging.exception("执行脚本发生异常")
return jsonify({'message': '调用脚本失败', 'error': str(e)}), 500
@app.route('/delete', methods=['POST'])
def delete_vpn():
data = request.get_json()
logging.debug(f"收到删除请求体: {data}")
username = data.get('username')
if not username:
logging.warning("缺少用户名")
return jsonify({'message': '用户名是必需的'}), 400
script_path = '/root/delete.sh'
command = f"{script_path} {username}"
logging.info(f"准备执行删除命令: {command}")
try:
env = os.environ.copy()
env['PATH'] = f"/usr/bin:/bin:{env.get('PATH', '')}"
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60,
cwd='/home/ec2-user',
env=env
)
logging.debug(f"stdout: {result.stdout}")
logging.debug(f"stderr: {result.stderr}")
if result.returncode == 0:
logging.info(f"成功删除用户 {username}")
return jsonify({
'message': f'用户 {username} 删除成功',
'stdout': result.stdout
}), 200
elif result.returncode == 2:
logging.warning(f"用户 {username} 不存在")
return jsonify({
'message': f'用户 {username} 不存在',
'stderr': result.stderr,
'stdout': result.stdout
}), 404
else:
logging.error(f"删除用户 {username} 失败")
return jsonify({
'message': f'删除用户 {username} 失败',
'stderr': result.stderr,
'stdout': result.stdout
}), 500
except subprocess.TimeoutExpired:
logging.error(f"删除用户 {username} 脚本执行超时")
return jsonify({'message': '脚本执行超时'}), 500
except Exception as e:
logging.exception(f"删除用户 {username} 发生异常")
return jsonify({'message': '调用删除脚本失败', 'error': str(e)}), 500
if __name__ == '__main__':
logging.info("VPN 服务启动中...监听 0.0.0.0:5000")
app.run(host='0.0.0.0', port=5000)
日志与审计
每个用户的访问日志会记录在:
/var/log/vpn/{username}.log
内容大概是这样
SRC是使用openvpn用户的ip,DST是他访问的地址
自动生ovpn文件后,在后端接口还可以加入scp或者其他方式从线上服务器直接将ovpn文件拉倒线下,实现自动化方式
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果