本章概述

本章将深入探讨 OpenVPN 的认证与授权机制,包括各种认证方法的实现、用户权限管理、多因素认证的配置以及与外部认证系统的集成。通过本章学习,您将能够为 OpenVPN 部署构建安全、灵活的认证授权体系。

flowchart TD
    A[认证与授权机制] --> B[基础认证方法]
    A --> C[用户权限管理]
    A --> D[多因素认证]
    A --> E[外部认证集成]
    A --> F[证书管理高级技巧]
    A --> G[认证授权最佳实践]
    
    B --> B1[静态密钥认证]
    B --> B2[TLS证书认证]
    B --> B3[用户名密码认证]
    
    C --> C1[访问控制列表]
    C --> C2[CCD客户端配置目录]
    C --> C3[IP分配与路由控制]
    
    D --> D1[双因素认证配置]
    D --> D2[Google Authenticator集成]
    D --> D3[硬件令牌集成]
    
    E --> E1[LDAP/Active Directory集成]
    E --> E2[RADIUS服务器集成]
    E --> E3[OAuth2认证集成]
    
    F --> F1[证书吊销管理]
    F --> F2[证书自动更新]
    F --> F3[证书分发系统]
    
    G --> G1[企业级认证方案]
    G --> G2[安全审计与合规]
    G --> G3[零信任架构实现]

基础认证方法

静态密钥认证

静态密钥认证是 OpenVPN 最简单的认证方式,适用于点对点连接或小型部署。

静态密钥生成与配置

#!/bin/bash
# 静态密钥认证配置脚本
# 文件名: static_key_auth.sh

set -e
echo "OpenVPN 静态密钥认证配置工具"
echo "==============================="

# 配置变量
OVPN_DIR="/etc/openvpn"
KEY_FILE="${OVPN_DIR}/static.key"
SERVER_CONF="${OVPN_DIR}/server_static.conf"
CLIENT_CONF="${OVPN_DIR}/client_static.conf"
SERVER_IP="your_server_ip"
PORT="1194"

# 创建OpenVPN目录
mkdir -p ${OVPN_DIR}

# 生成静态密钥
echo "[1] 生成静态密钥..."
openvpn --genkey --secret ${KEY_FILE}
echo "✅ 静态密钥已生成: ${KEY_FILE}"

# 创建服务器配置
echo "[2] 创建服务器配置文件..."
cat > ${SERVER_CONF} << EOF
# OpenVPN静态密钥服务器配置
dev tun
proto udp
port ${PORT}
ifconfig 10.8.0.1 10.8.0.2
secret ${KEY_FILE}
keepalive 10 120
comp-lzo
user nobody
group nogroup
persist-key
persist-tun
status ${OVPN_DIR}/openvpn-status.log
log-append ${OVPN_DIR}/openvpn.log
verb 3
EOF
echo "✅ 服务器配置已创建: ${SERVER_CONF}"

# 创建客户端配置
echo "[3] 创建客户端配置文件..."
cat > ${CLIENT_CONF} << EOF
# OpenVPN静态密钥客户端配置
dev tun
proto udp
remote ${SERVER_IP} ${PORT}
ifconfig 10.8.0.2 10.8.0.1
secret ${KEY_FILE}
keepalive 10 120
comp-lzo
user nobody
group nogroup
persist-key
persist-tun
status ${OVPN_DIR}/openvpn-status.log
log-append ${OVPN_DIR}/openvpn.log
verb 3
EOF
echo "✅ 客户端配置已创建: ${CLIENT_CONF}"

# 配置防火墙
echo "[4] 配置防火墙规则..."
if command -v ufw > /dev/null; then
    ufw allow ${PORT}/udp
    echo "✅ UFW防火墙规则已添加"
elif command -v firewall-cmd > /dev/null; then
    firewall-cmd --permanent --add-port=${PORT}/udp
    firewall-cmd --reload
    echo "✅ Firewalld防火墙规则已添加"
else
    echo "⚠️ 未检测到支持的防火墙系统,请手动配置防火墙规则"
fi

# 启用IP转发
echo "[5] 启用IP转发..."
echo 1 > /proc/sys/net/ipv4/ip_forward
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p
echo "✅ IP转发已启用"

# 创建systemd服务
echo "[6] 创建OpenVPN服务..."
cat > /etc/systemd/system/openvpn-static.service << EOF
[Unit]
Description=OpenVPN Static Key Connection
After=network.target

[Service]
ExecStart=/usr/sbin/openvpn --config ${SERVER_CONF}
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable openvpn-static.service
echo "✅ OpenVPN服务已创建并启用"

# 启动服务
echo "[7] 启动OpenVPN服务..."
systemctl start openvpn-static.service
echo "✅ OpenVPN服务已启动"

# 显示状态信息
echo "\n静态密钥认证配置完成!"
echo "==============================="
echo "服务器配置文件: ${SERVER_CONF}"
echo "客户端配置文件: ${CLIENT_CONF}"
echo "静态密钥文件: ${KEY_FILE}"
echo "\n要在客户端使用,请安全地传输客户端配置文件和静态密钥文件"
echo "警告: 静态密钥认证适用于简单场景,对于多用户环境,建议使用TLS证书认证"

静态密钥认证的优缺点

优点: - 配置简单,易于部署 - 无需PKI基础设施 - 适合临时或点对点连接

缺点: - 不支持多客户端(只能一对一连接) - 密钥分发存在安全风险 - 无法实现细粒度的访问控制 - 密钥轮换复杂

TLS证书认证

TLS证书认证是OpenVPN最常用、最安全的认证方式,支持多客户端连接和细粒度访问控制。

高级证书管理系统

#!/usr/bin/env python3
# OpenVPN高级证书管理系统
# 文件名: openvpn_cert_manager.py

import os
import sys
import argparse
import subprocess
import datetime
import shutil
import json
from pathlib import Path

class OpenVPNCertManager:
    """OpenVPN证书管理系统,提供证书的创建、吊销、更新和状态管理"""
    
    def __init__(self, easy_rsa_path="/etc/openvpn/easy-rsa", 
                 pki_path="/etc/openvpn/easy-rsa/pki",
                 config_file="/etc/openvpn/cert_manager_config.json"):
        """初始化证书管理器"""
        self.easy_rsa_path = Path(easy_rsa_path)
        self.pki_path = Path(pki_path)
        self.config_file = Path(config_file)
        self.config = self.load_config()
        
        # 确保目录存在
        if not self.easy_rsa_path.exists():
            print(f"错误: Easy-RSA目录不存在: {self.easy_rsa_path}")
            print("请先安装Easy-RSA并初始化PKI")
            sys.exit(1)
    
    def load_config(self):
        """加载配置文件,如果不存在则创建默认配置"""
        default_config = {
            "cert_expiry_days": 365,
            "cert_key_size": 2048,
            "ca_expiry_days": 3650,
            "cert_digest": "sha256",
            "country": "CN",
            "province": "Beijing",
            "city": "Beijing",
            "org": "My Organization",
            "email": "admin@example.com",
            "ou": "IT Department",
            "notification_days": 30,  # 证书过期前多少天发送通知
            "auto_renew": True,      # 是否自动更新即将过期的证书
            "backup_dir": "/etc/openvpn/cert_backups"
        }
        
        if not self.config_file.exists():
            # 创建配置目录
            self.config_file.parent.mkdir(parents=True, exist_ok=True)
            
            # 写入默认配置
            with open(self.config_file, 'w') as f:
                json.dump(default_config, f, indent=4)
            print(f"已创建默认配置文件: {self.config_file}")
            return default_config
        
        # 加载现有配置
        try:
            with open(self.config_file, 'r') as f:
                config = json.load(f)
                # 确保所有默认配置项都存在
                for key, value in default_config.items():
                    if key not in config:
                        config[key] = value
                return config
        except Exception as e:
            print(f"加载配置文件时出错: {e}")
            print("使用默认配置")
            return default_config
    
    def run_easy_rsa_cmd(self, cmd, *args):
        """运行Easy-RSA命令"""
        full_cmd = ["./easyrsa"] + [cmd] + list(args)
        try:
            result = subprocess.run(
                full_cmd,
                cwd=self.easy_rsa_path,
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )
            return result.stdout
        except subprocess.CalledProcessError as e:
            print(f"命令执行失败: {' '.join(full_cmd)}")
            print(f"错误: {e.stderr}")
            return None
    
    def init_pki(self):
        """初始化PKI结构"""
        print("初始化PKI结构...")
        if self.pki_path.exists():
            print(f"PKI目录已存在: {self.pki_path}")
            return True
        
        result = self.run_easy_rsa_cmd("init-pki")
        if result:
            print("PKI初始化成功")
            return True
        return False
    
    def build_ca(self, batch=True):
        """创建CA证书"""
        print("创建CA证书...")
        ca_cert = self.pki_path / "ca.crt"
        if ca_cert.exists():
            print("CA证书已存在")
            return True
        
        args = []
        if batch:
            args.append("nopass")
        
        result = self.run_easy_rsa_cmd("build-ca", *args)
        if result:
            print("CA证书创建成功")
            return True
        return False
    
    def build_server_cert(self, server_name="server", batch=True):
        """创建服务器证书"""
        print(f"创建服务器证书: {server_name}...")
        server_cert = self.pki_path / "issued" / f"{server_name}.crt"
        if server_cert.exists():
            print(f"服务器证书已存在: {server_name}")
            return True
        
        args = [server_name]
        if batch:
            args.insert(0, "nopass")
        
        result = self.run_easy_rsa_cmd("build-server-full", *args)
        if result:
            print(f"服务器证书创建成功: {server_name}")
            return True
        return False
    
    def build_client_cert(self, client_name, batch=True, password=None):
        """创建客户端证书"""
        print(f"创建客户端证书: {client_name}...")
        client_cert = self.pki_path / "issued" / f"{client_name}.crt"
        if client_cert.exists():
            print(f"客户端证书已存在: {client_name}")
            return True
        
        if batch and not password:
            args = ["nopass", client_name]
        elif password:
            # 如果提供了密码,需要通过环境变量传递
            os.environ["EASYRSA_PASSOUT"] = f"pass:{password}"
            args = [client_name]
        else:
            args = [client_name]
        
        result = self.run_easy_rsa_cmd("build-client-full", *args)
        
        # 清除环境变量
        if "EASYRSA_PASSOUT" in os.environ:
            del os.environ["EASYRSA_PASSOUT"]
            
        if result:
            print(f"客户端证书创建成功: {client_name}")
            return True
        return False
    
    def revoke_cert(self, name):
        """吊销证书"""
        print(f"吊销证书: {name}...")
        result = self.run_easy_rsa_cmd("revoke", name)
        if result:
            print(f"证书已吊销: {name}")
            # 更新CRL
            self.gen_crl()
            return True
        return False
    
    def gen_crl(self):
        """生成证书吊销列表"""
        print("生成证书吊销列表(CRL)...")
        result = self.run_easy_rsa_cmd("gen-crl")
        if result:
            print("CRL生成成功")
            # 复制CRL到OpenVPN目录
            crl_file = self.pki_path / "crl.pem"
            if crl_file.exists():
                shutil.copy(crl_file, "/etc/openvpn/crl.pem")
                print("CRL已复制到OpenVPN目录")
            return True
        return False
    
    def list_certs(self):
        """列出所有证书及其状态"""
        print("证书列表:")
        print("-" * 80)
        print(f"{'名称':<20} {'类型':<10} {'状态':<10} {'过期日期':<20} {'剩余天数':<10}")
        print("-" * 80)
        
        # 检查CA证书
        ca_cert = self.pki_path / "ca.crt"
        if ca_cert.exists():
            ca_info = self.get_cert_info(ca_cert)
            print(f"{'CA':<20} {'CA':<10} {'有效':<10} {ca_info['expiry_date']:<20} {ca_info['days_left']:<10}")
        
        # 检查服务器证书
        issued_dir = self.pki_path / "issued"
        if issued_dir.exists():
            for cert_file in issued_dir.glob("*.crt"):
                name = cert_file.stem
                cert_info = self.get_cert_info(cert_file)
                cert_type = "服务器" if self.is_server_cert(cert_file) else "客户端"
                status = "有效"
                
                # 检查是否被吊销
                if self.is_revoked(name):
                    status = "已吊销"
                # 检查是否过期
                elif cert_info['days_left'] < 0:
                    status = "已过期"
                # 检查是否即将过期
                elif cert_info['days_left'] < self.config['notification_days']:
                    status = "即将过期"
                
                print(f"{name:<20} {cert_type:<10} {status:<10} {cert_info['expiry_date']:<20} {cert_info['days_left']:<10}")
        
        print("-" * 80)
    
    def get_cert_info(self, cert_path):
        """获取证书信息"""
        try:
            cmd = ["openssl", "x509", "-in", str(cert_path), "-noout", "-enddate"]
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
            end_date_str = result.stdout.strip().split("=")[1]
            end_date = datetime.datetime.strptime(end_date_str, "%b %d %H:%M:%S %Y %Z")
            
            today = datetime.datetime.now()
            days_left = (end_date - today).days
            
            return {
                "expiry_date": end_date.strftime("%Y-%m-%d"),
                "days_left": days_left
            }
        except Exception as e:
            print(f"获取证书信息时出错: {e}")
            return {"expiry_date": "未知", "days_left": 0}
    
    def is_server_cert(self, cert_path):
        """检查是否为服务器证书"""
        try:
            cmd = ["openssl", "x509", "-in", str(cert_path), "-noout", "-purpose"]
            result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
            return "SSL server : Yes" in result.stdout
        except Exception:
            return False
    
    def is_revoked(self, name):
        """检查证书是否被吊销"""
        index_file = self.pki_path / "index.txt"
        if not index_file.exists():
            return False
        
        try:
            with open(index_file, 'r') as f:
                for line in f:
                    if line.startswith("R") and f"/CN={name}" in line:
                        return True
            return False
        except Exception:
            return False
    
    def renew_cert(self, name):
        """更新证书"""
        print(f"更新证书: {name}...")
        
        # 备份旧证书
        self.backup_cert(name)
        
        # 吊销旧证书
        self.revoke_cert(name)
        
        # 确定证书类型并创建新证书
        is_server = False
        cert_file = self.pki_path / "issued" / f"{name}.crt"
        if cert_file.exists():
            is_server = self.is_server_cert(cert_file)
        
        if is_server:
            return self.build_server_cert(name)
        else:
            return self.build_client_cert(name)
    
    def backup_cert(self, name):
        """备份证书"""
        backup_dir = Path(self.config['backup_dir']) / datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        backup_dir.mkdir(parents=True, exist_ok=True)
        
        # 备份证书和密钥
        cert_file = self.pki_path / "issued" / f"{name}.crt"
        key_file = self.pki_path / "private" / f"{name}.key"
        
        if cert_file.exists():
            shutil.copy(cert_file, backup_dir / f"{name}.crt")
        
        if key_file.exists():
            shutil.copy(key_file, backup_dir / f"{name}.key")
        
        print(f"证书已备份到: {backup_dir}")
    
    def check_expiring_certs(self):
        """检查即将过期的证书"""
        print("检查即将过期的证书...")
        expiring_certs = []
        
        issued_dir = self.pki_path / "issued"
        if issued_dir.exists():
            for cert_file in issued_dir.glob("*.crt"):
                name = cert_file.stem
                if self.is_revoked(name):
                    continue
                    
                cert_info = self.get_cert_info(cert_file)
                if 0 <= cert_info['days_left'] <= self.config['notification_days']:
                    expiring_certs.append({
                        "name": name,
                        "days_left": cert_info['days_left'],
                        "expiry_date": cert_info['expiry_date'],
                        "is_server": self.is_server_cert(cert_file)
                    })
        
        if expiring_certs:
            print(f"发现{len(expiring_certs)}个即将过期的证书:")
            for cert in expiring_certs:
                cert_type = "服务器" if cert['is_server'] else "客户端"
                print(f"  - {cert['name']} ({cert_type}): 将在{cert['days_left']}天后过期 ({cert['expiry_date']})")
                
                # 如果配置了自动更新,则更新证书
                if self.config['auto_renew']:
                    print(f"自动更新证书: {cert['name']}")
                    self.renew_cert(cert['name'])
        else:
            print("没有即将过期的证书")
    
    def export_client_config(self, client_name, output_dir="/etc/openvpn/client_configs"):
        """导出客户端配置文件(.ovpn)"""
        print(f"导出客户端配置: {client_name}...")
        
        # 确保客户端证书存在
        client_cert = self.pki_path / "issued" / f"{client_name}.crt"
        client_key = self.pki_path / "private" / f"{client_name}.key"
        ca_cert = self.pki_path / "ca.crt"
        ta_key = Path("/etc/openvpn/ta.key")
        
        if not client_cert.exists():
            print(f"错误: 客户端证书不存在: {client_name}")
            return False
        
        if not client_key.exists():
            print(f"错误: 客户端密钥不存在: {client_name}")
            return False
        
        if not ca_cert.exists():
            print("错误: CA证书不存在")
            return False
        
        # 创建输出目录
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)
        
        # 读取证书和密钥内容
        with open(client_cert, 'r') as f:
            client_cert_content = f.read()
        
        with open(client_key, 'r') as f:
            client_key_content = f.read()
        
        with open(ca_cert, 'r') as f:
            ca_cert_content = f.read()
        
        ta_key_content = ""
        if ta_key.exists():
            with open(ta_key, 'r') as f:
                ta_key_content = f.read()
        
        # 创建客户端配置文件
        client_config = f"""\
# OpenVPN客户端配置 - {client_name}  
client
dev tun
proto udp
remote your_server_ip 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
verb 3

<ca>
{ca_cert_content}</ca>

<cert>
{client_cert_content}</cert>

<key>
{client_key_content}</key>
"""
        
        # 如果存在TLS认证密钥,添加到配置中
        if ta_key_content:
            client_config += f"""
<tls-auth>
{ta_key_content}</tls-auth>
key-direction 1
"""
        
        # 写入配置文件
        output_file = output_path / f"{client_name}.ovpn"
        with open(output_file, 'w') as f:
            f.write(client_config)
        
        print(f"客户端配置已导出: {output_file}")
        return True

    def run(self):
        """运行交互式菜单"""
        while True:
            print("\nOpenVPN证书管理系统")
            print("====================")
            print("1. 初始化PKI结构")
            print("2. 创建CA证书")
            print("3. 创建服务器证书")
            print("4. 创建客户端证书")
            print("5. 吊销证书")
            print("6. 更新证书吊销列表(CRL)")
            print("7. 列出所有证书")
            print("8. 检查即将过期的证书")
            print("9. 更新证书")
            print("10. 导出客户端配置")
            print("11. 编辑配置")
            print("0. 退出")
            
            choice = input("\n请选择操作 [0-11]: ")
            
            if choice == "0":
                break
            elif choice == "1":
                self.init_pki()
            elif choice == "2":
                batch = input("是否使用无密码模式? (y/n): ").lower() == "y"
                self.build_ca(batch)
            elif choice == "3":
                server_name = input("服务器名称 [server]: ") or "server"
                batch = input("是否使用无密码模式? (y/n): ").lower() == "y"
                self.build_server_cert(server_name, batch)
            elif choice == "4":
                client_name = input("客户端名称: ")
                if not client_name:
                    print("错误: 客户端名称不能为空")
                    continue
                    
                use_password = input("是否为私钥设置密码? (y/n): ").lower() == "y"
                if use_password:
                    password = input("请输入私钥密码: ")
                    self.build_client_cert(client_name, False, password)
                else:
                    self.build_client_cert(client_name)
            elif choice == "5":
                name = input("要吊销的证书名称: ")
                if name:
                    self.revoke_cert(name)
            elif choice == "6":
                self.gen_crl()
            elif choice == "7":
                self.list_certs()
            elif choice == "8":
                self.check_expiring_certs()
            elif choice == "9":
                name = input("要更新的证书名称: ")
                if name:
                    self.renew_cert(name)
            elif choice == "10":
                client_name = input("要导出配置的客户端名称: ")
                if client_name:
                    output_dir = input("输出目录 [/etc/openvpn/client_configs]: ") or "/etc/openvpn/client_configs"
                    self.export_client_config(client_name, output_dir)
            elif choice == "11":
                print("当前配置:")
                for key, value in self.config.items():
                    print(f"  {key}: {value}")
                
                key = input("\n要编辑的配置项 (留空返回): ")
                if key in self.config:
                    value = input(f"新值 (当前: {self.config[key]}): ")
                    # 转换值类型
                    if isinstance(self.config[key], bool):
                        self.config[key] = value.lower() in ["true", "yes", "y", "1"]
                    elif isinstance(self.config[key], int):
                        try:
                            self.config[key] = int(value)
                        except ValueError:
                            print("错误: 请输入有效的整数")
                    else:
                        self.config[key] = value
                    
                    # 保存配置
                    with open(self.config_file, 'w') as f:
                        json.dump(self.config, f, indent=4)
                    print("配置已更新")
            else:
                print("无效的选择,请重试")

# 命令行接口
def main():
    parser = argparse.ArgumentParser(description="OpenVPN高级证书管理系统")
    parser.add_argument("--easy-rsa", default="/etc/openvpn/easy-rsa", help="Easy-RSA目录路径")
    parser.add_argument("--pki", default="/etc/openvpn/easy-rsa/pki", help="PKI目录路径")
    parser.add_argument("--config", default="/etc/openvpn/cert_manager_config.json", help="配置文件路径")
    
    subparsers = parser.add_subparsers(dest="command", help="命令")
    
    # init-pki命令
    subparsers.add_parser("init-pki", help="初始化PKI结构")
    
    # build-ca命令
    ca_parser = subparsers.add_parser("build-ca", help="创建CA证书")
    ca_parser.add_argument("--batch", action="store_true", help="使用无密码模式")
    
    # build-server命令
    server_parser = subparsers.add_parser("build-server", help="创建服务器证书")
    server_parser.add_argument("--name", default="server", help="服务器名称")
    server_parser.add_argument("--batch", action="store_true", help="使用无密码模式")
    
    # build-client命令
    client_parser = subparsers.add_parser("build-client", help="创建客户端证书")
    client_parser.add_argument("name", help="客户端名称")
    client_parser.add_argument("--batch", action="store_true", help="使用无密码模式")
    client_parser.add_argument("--password", help="私钥密码")
    
    # revoke命令
    revoke_parser = subparsers.add_parser("revoke", help="吊销证书")
    revoke_parser.add_argument("name", help="要吊销的证书名称")
    
    # gen-crl命令
    subparsers.add_parser("gen-crl", help="生成证书吊销列表")
    
    # list命令
    subparsers.add_parser("list", help="列出所有证书")
    
    # check-expiring命令
    subparsers.add_parser("check-expiring", help="检查即将过期的证书")
    
    # renew命令
    renew_parser = subparsers.add_parser("renew", help="更新证书")
    renew_parser.add_argument("name", help="要更新的证书名称")
    
    # export-client命令
    export_parser = subparsers.add_parser("export-client", help="导出客户端配置")
    export_parser.add_argument("name", help="客户端名称")
    export_parser.add_argument("--output-dir", default="/etc/openvpn/client_configs", help="输出目录")
    
    # 解析参数
    args = parser.parse_args()
    
    # 创建证书管理器
    cert_manager = OpenVPNCertManager(
        easy_rsa_path=args.easy_rsa,
        pki_path=args.pki,
        config_file=args.config
    )
    
    # 执行命令
    if args.command == "init-pki":
        cert_manager.init_pki()
    elif args.command == "build-ca":
        cert_manager.build_ca(args.batch)
    elif args.command == "build-server":
        cert_manager.build_server_cert(args.name, args.batch)
    elif args.command == "build-client":
        cert_manager.build_client_cert(args.name, args.batch, args.password)
    elif args.command == "revoke":
        cert_manager.revoke_cert(args.name)
    elif args.command == "gen-crl":
        cert_manager.gen_crl()
    elif args.command == "list":
        cert_manager.list_certs()
    elif args.command == "check-expiring":
        cert_manager.check_expiring_certs()
    elif args.command == "renew":
        cert_manager.renew_cert(args.name)
    elif args.command == "export-client":
        cert_manager.export_client_config(args.name, args.output_dir)
    else:
        # 如果没有指定命令,运行交互式菜单
        cert_manager.run()

if __name__ == "__main__":
    main()

TLS证书认证的优缺点

优点: - 高安全性,基于PKI基础设施 - 支持多客户端连接 - 可实现细粒度的访问控制 - 证书可以独立管理(创建、吊销) - 支持双向认证

缺点: - 配置相对复杂 - 需要维护PKI基础设施 - 证书分发和管理需要额外工作

用户名密码认证

用户名密码认证通常与TLS证书认证结合使用,提供双重验证机制。

PAM认证集成

#!/bin/bash
# OpenVPN PAM认证集成脚本
# 文件名: openvpn_pam_auth.sh

set -e
echo "OpenVPN PAM认证集成工具"
echo "=========================="

# 配置变量
OVPN_DIR="/etc/openvpn"
AUTH_DIR="${OVPN_DIR}/auth"
SERVER_CONF="${OVPN_DIR}/server.conf"
PAM_CONF="/etc/pam.d/openvpn"
USER_PASS_FILE="${AUTH_DIR}/user_pass.txt"

# 检查OpenVPN是否安装
if ! command -v openvpn >/dev/null 2>&1; then
    echo "错误: OpenVPN未安装,请先安装OpenVPN"
    exit 1
fi

# 创建认证目录
mkdir -p ${AUTH_DIR}
chmod 700 ${AUTH_DIR}

# 创建PAM配置文件
echo "[1] 创建PAM配置文件..."
cat > ${PAM_CONF} << EOF
# OpenVPN PAM配置
auth    required        pam_unix.so shadow nodelay
account required        pam_unix.so
EOF
chmod 644 ${PAM_CONF}
echo "✅ PAM配置文件已创建: ${PAM_CONF}"

# 创建认证脚本
echo "[2] 创建认证脚本..."
cat > ${AUTH_DIR}/auth-pam.sh << 'EOF'
#!/bin/bash
# PAM认证脚本

# 获取用户名和密码
username="$1"
password="$2"

# 记录认证尝试
logger -t "openvpn-auth" "认证尝试: $username"

# 使用expect进行PAM认证
expect << EOD
spawn su - $username
expect "Password: "
send "$password\r"
expect eof
catch wait result
exit [lindex \$result 3]
EOD

# 获取认证结果
auth_result=$?

# 记录认证结果
if [ $auth_result -eq 0 ]; then
    logger -t "openvpn-auth" "认证成功: $username"
    exit 0
else
    logger -t "openvpn-auth" "认证失败: $username"
    exit 1
fi
EOF

chmod 700 ${AUTH_DIR}/auth-pam.sh
echo "✅ 认证脚本已创建: ${AUTH_DIR}/auth-pam.sh"

# 安装依赖
echo "[3] 安装依赖包..."
if command -v apt-get >/dev/null 2>&1; then
    apt-get update
    apt-get install -y expect libpam-dev
elif command -v yum >/dev/null 2>&1; then
    yum install -y expect pam-devel
else
    echo "⚠️ 未检测到支持的包管理器,请手动安装expect和PAM开发包"
fi
echo "✅ 依赖包已安装"

# 创建用户密码文件
echo "[4] 创建用户密码文件..."
cat > ${USER_PASS_FILE} << EOF
# OpenVPN用户密码文件
# 格式: 用户名,密码
# 示例:
# user1,password1
# user2,password2
EOF
chmod 600 ${USER_PASS_FILE}
echo "✅ 用户密码文件已创建: ${USER_PASS_FILE}"

# 创建用户管理脚本
echo "[5] 创建用户管理脚本..."
cat > ${AUTH_DIR}/manage_users.sh << 'EOF'
#!/bin/bash
# OpenVPN用户管理脚本

USER_PASS_FILE="/etc/openvpn/auth/user_pass.txt"

# 显示帮助信息
show_help() {
    echo "OpenVPN用户管理工具"
    echo "用法: $0 [选项] [参数]"
    echo "选项:"
    echo "  -a, --add <用户名> <密码>    添加新用户"
    echo "  -d, --delete <用户名>       删除用户"
    echo "  -c, --change <用户名> <新密码> 修改用户密码"
    echo "  -l, --list                 列出所有用户"
    echo "  -h, --help                 显示帮助信息"
}

# 添加用户
add_user() {
    local username=$1
    local password=$2
    
    # 检查用户是否已存在
    if grep -q "^$username," "$USER_PASS_FILE"; then
        echo "错误: 用户 '$username' 已存在"
        return 1
    fi
    
    # 添加用户
    echo "$username,$password" >> "$USER_PASS_FILE"
    echo "用户 '$username' 已添加"
    return 0
}

# 删除用户
delete_user() {
    local username=$1
    
    # 检查用户是否存在
    if ! grep -q "^$username," "$USER_PASS_FILE"; then
        echo "错误: 用户 '$username' 不存在"
        return 1
    fi
    
    # 删除用户
    sed -i "/^$username,/d" "$USER_PASS_FILE"
    echo "用户 '$username' 已删除"
    return 0
}

# 修改用户密码
change_password() {
    local username=$1
    local new_password=$2
    
    # 检查用户是否存在
    if ! grep -q "^$username," "$USER_PASS_FILE"; then
        echo "错误: 用户 '$username' 不存在"
        return 1
    fi
    
    # 修改密码
    sed -i "s/^$username,.*/$username,$new_password/" "$USER_PASS_FILE"
    echo "用户 '$username' 的密码已修改"
    return 0
}

# 列出所有用户
list_users() {
    echo "OpenVPN用户列表:"
    echo "------------------"
    
    # 检查文件是否为空
    if [ ! -s "$USER_PASS_FILE" ] || ! grep -v "^#" "$USER_PASS_FILE" | grep -q "."; then
        echo "没有用户"
        return 0
    fi
    
    # 显示用户列表
    grep -v "^#" "$USER_PASS_FILE" | cut -d',' -f1 | while read -r username; do
        if [ -n "$username" ]; then
            echo "- $username"
        fi
    done
    
    return 0
}

# 解析命令行参数
if [ $# -eq 0 ]; then
    show_help
    exit 0
fi

case "$1" in
    -a|--add)
        if [ $# -lt 3 ]; then
            echo "错误: 添加用户需要指定用户名和密码"
            show_help
            exit 1
        fi
        add_user "$2" "$3"
        ;;
    -d|--delete)
        if [ $# -lt 2 ]; then
            echo "错误: 删除用户需要指定用户名"
            show_help
            exit 1
        fi
        delete_user "$2"
        ;;
    -c|--change)
        if [ $# -lt 3 ]; then
            echo "错误: 修改密码需要指定用户名和新密码"
            show_help
            exit 1
        fi
        change_password "$2" "$3"
        ;;
    -l|--list)
        list_users
        ;;
    -h|--help)
        show_help
        ;;
    *)
        echo "错误: 未知选项 '$1'"
        show_help
        exit 1
        ;;
esac

exit 0
EOF

chmod 700 ${AUTH_DIR}/manage_users.sh
echo "✅ 用户管理脚本已创建: ${AUTH_DIR}/manage_users.sh"

# 创建认证验证脚本
echo "[6] 创建认证验证脚本..."
cat > ${AUTH_DIR}/auth-file.sh << 'EOF'
#!/bin/bash
# 文件认证脚本

USER_PASS_FILE="/etc/openvpn/auth/user_pass.txt"

# 获取用户名和密码
username="$1"
password="$2"

# 记录认证尝试
logger -t "openvpn-auth" "文件认证尝试: $username"

# 在文件中查找用户
user_line=$(grep "^$username," "$USER_PASS_FILE")

if [ -z "$user_line" ]; then
    logger -t "openvpn-auth" "文件认证失败: 用户 $username 不存在"
    exit 1
fi

# 提取密码
stored_password=$(echo "$user_line" | cut -d',' -f2)

# 验证密码
if [ "$password" = "$stored_password" ]; then
    logger -t "openvpn-auth" "文件认证成功: $username"
    exit 0
else
    logger -t "openvpn-auth" "文件认证失败: 密码错误 $username"
    exit 1
fi
EOF

chmod 700 ${AUTH_DIR}/auth-file.sh
echo "✅ 认证验证脚本已创建: ${AUTH_DIR}/auth-file.sh"

# 修改OpenVPN服务器配置
echo "[7] 更新OpenVPN服务器配置..."

# 检查服务器配置文件是否存在
if [ ! -f "${SERVER_CONF}" ]; then
    echo "⚠️ OpenVPN服务器配置文件不存在: ${SERVER_CONF}"
    echo "请先配置OpenVPN服务器,然后手动添加以下行到配置文件中:"
    echo ""
    echo "# 用户名/密码认证配置"
    echo "auth-user-pass-verify ${AUTH_DIR}/auth-file.sh via-env"
    echo "script-security 3"
    echo "username-as-common-name"
    echo "verify-client-cert require"
    echo "client-cert-not-required"
    exit 1
fi

# 添加认证配置到服务器配置文件
if ! grep -q "auth-user-pass-verify" "${SERVER_CONF}"; then
    cat >> ${SERVER_CONF} << EOF

# 用户名/密码认证配置
auth-user-pass-verify ${AUTH_DIR}/auth-file.sh via-env
script-security 3
username-as-common-name
verify-client-cert require
EOF
    echo "✅ OpenVPN服务器配置已更新"
else
    echo "⚠️ 认证配置已存在于服务器配置文件中"
fi

# 创建客户端配置示例
echo "[8] 创建客户端配置示例..."
cat > ${AUTH_DIR}/client-auth.ovpn << EOF
# OpenVPN客户端配置示例 - 用户名/密码认证

client
dev tun
proto udp
remote your_server_ip 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
verb 3

# 用户名/密码认证配置
auth-user-pass

# 证书和密钥
<ca>
# 在此处粘贴CA证书内容
</ca>

<cert>
# 在此处粘贴客户端证书内容
</cert>

<key>
# 在此处粘贴客户端密钥内容
</key>
EOF

echo "✅ 客户端配置示例已创建: ${AUTH_DIR}/client-auth.ovpn"

# 添加示例用户
echo "[9] 添加示例用户..."
${AUTH_DIR}/manage_users.sh --add openvpn_user password123
echo "✅ 示例用户已添加"

# 重启OpenVPN服务
echo "[10] 重启OpenVPN服务..."
if systemctl is-active --quiet openvpn@server; then
    systemctl restart openvpn@server
    echo "✅ OpenVPN服务已重启"
else
    echo "⚠️ OpenVPN服务未运行,请手动启动服务"
fi

# 显示完成信息
echo "\nOpenVPN PAM认证集成完成!"
echo "=============================="
echo "认证脚本: ${AUTH_DIR}/auth-file.sh"
echo "用户管理脚本: ${AUTH_DIR}/manage_users.sh"
echo "用户密码文件: ${USER_PASS_FILE}"
echo "客户端配置示例: ${AUTH_DIR}/client-auth.ovpn"
echo "\n使用以下命令管理用户:"
echo "  添加用户: ${AUTH_DIR}/manage_users.sh --add <用户名> <密码>"
echo "  删除用户: ${AUTH_DIR}/manage_users.sh --delete <用户名>"
echo "  修改密码: ${AUTH_DIR}/manage_users.sh --change <用户名> <新密码>"
echo "  列出用户: ${AUTH_DIR}/manage_users.sh --list"
echo "\n注意: 客户端需要配置auth-user-pass选项以启用用户名/密码认证"

用户名密码认证的优缺点

优点: - 用户熟悉的认证方式 - 可以与现有用户系统集成 - 可以与证书认证结合,提供双重验证 - 便于用户管理和密码更改

缺点: - 单独使用时安全性低于证书认证 - 需要额外的认证后端 - 密码可能被暴力破解

用户权限管理

访问控制列表

访问控制列表(ACL)允许您基于用户或证书属性控制VPN访问权限。

基于CCD的访问控制系统

”`bash #!/bin/bash

OpenVPN访问控制系统

文件名: openvpn_acl_manager.sh

set -e echo “OpenVPN访问控制管理工具” echo “==========================”

配置变量

OVPN_DIR=“/etc/openvpn” CCD_DIR=“${OVPN_DIR}/ccd” ACL_DIR=“${OVPN_DIR}/acl” SERVER_CONF=“${OVPN_DIR}/server.conf” ACL_CONF=“${ACL_DIR}/acl.conf” ROUTE_CONF=“${ACL_DIR}/routes.conf” GROUP_CONF=“${ACL_DIR}/groups.conf”

创建目录

mkdir -p ${CCD_DIR} mkdir -p ${ACL_DIR} chmod 700 ${ACL_DIR}

检查OpenVPN服务器配置

if [ ! -f “${SERVER_CONF}” ]; then echo “错误: OpenVPN服务器配置文件不存在: ${SERVER_CONF}” echo “请先配置OpenVPN服务器” exit 1 fi

创建ACL配置文件

echo “[1] 创建ACL配置文件…” cat > ${ACL_CONF} << EOF

OpenVPN访问控制列表配置

格式: <用户名/公共名称>,<允许访问的网络>,<拒绝访问的网络>

示例:

user1,10.0.0.0/8 192.168.1.0/24,10.1.1.0/24

user2,all,10.0.0.0/8

注意: ‘all’ 表示允许访问所有网络

EOF chmod 600 ${ACL_CONF} echo “✅ ACL配置文件已创建: ${ACL_CONF}”

创建路由配置文件

echo “[2] 创建路由配置文件…” cat > ${ROUTE_CONF} << EOF

OpenVPN路由配置

格式: <网络>,<网络掩码>,<描述>

示例:

10.0.0.0,255.0.0.0,内部网络

192.168.1.0,255.255.255.0,办公网络

EOF chmod 600 ${ROUTE_CONF} echo “✅ 路由配置文件已创建: ${ROUTE_CONF}”

创建组配置文件

echo “[3] 创建组配置文件…” cat > ${GROUP_CONF} << EOF

OpenVPN用户组配置

格式: <组名>,<用户列表>

示例:

admins,user1 user2 user3

developers,user4 user5

EOF chmod 600 ${GROUP_CONF} echo “✅ 组配置文件已创建: ${GROUP_CONF}”

更新OpenVPN服务器配置

echo “[4] 更新OpenVPN服务器配置…”

检查是否已配置CCD

if ! grep -q “client-config-dir” “${SERVER_CONF}”; then cat >> ${SERVER_CONF} << EOF

客户端配置目录

client-config-dir ${CCD_DIR}

启用用户名作为公共名称

username-as-common-name

启用脚本安全级别

script-security 2

客户端连接脚本

client-connect ${ACL_DIR}/client-connect.sh

客户端断开脚本

client-disconnect ${ACL_DIR}/client-disconnect.sh EOF echo “✅ OpenVPN服务器配置已更新” else echo “⚠️ CCD配置已存在于服务器配置文件中” fi

创建客户端连接脚本

echo “[5] 创建客户端连接脚本…” cat > ${ACL_DIR}/client-connect.sh << ‘EOF’ #!/bin/bash

OpenVPN客户端连接脚本

配置变量

OV