Skip to content

RabbitMQ 证书配置

概述

TLS 证书是建立安全通信的基础。本文档详细介绍 RabbitMQ 所需证书的生成、配置和管理方法,包括自签名证书和 CA 签名证书的完整流程。

核心知识点

证书类型

┌─────────────────────────────────────────────────────────────┐
│                      证书类型对比                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  自签名证书                    CA 签名证书                   │
│  ┌─────────────────┐          ┌─────────────────┐          │
│  │ • 适合测试环境   │          │ • 适合生产环境   │          │
│  │ • 快速生成      │          │ • 受信任的证书   │          │
│  │ • 无需 CA      │          │ • 需要 CA 签发   │          │
│  │ • 浏览器警告    │          │ • 无浏览器警告   │          │
│  │ • 零成本       │          │ • 可能有费用     │          │
│  └─────────────────┘          └─────────────────┘          │
│                                                             │
│  证书链结构:                                               │
│                                                             │
│       ┌─────────┐                                          │
│       │ Root CA │ ← 根证书颁发机构                          │
│       └────┬────┘                                          │
│            │                                               │
│       ┌────┴────┐                                          │
│       │Intermediate│ ← 中间证书颁发机构                      │
│       │    CA    │                                         │
│       └────┬────┘                                          │
│            │                                               │
│       ┌────┴────┐                                          │
│       │ Server  │ ← 服务器证书                              │
│       │  Cert   │                                          │
│       └─────────┘                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

证书文件格式

格式扩展名说明用途
PEM.pem, .crt, .cerBase64 编码最常用,RabbitMQ 默认
DER.der, .cer二进制格式Java 平台常用
PKCS#12.p12, .pfx包含私钥和证书Windows 平台
PKCS#7.p7b, .p7c证书链格式证书链导出

证书文件说明

RabbitMQ TLS 所需文件:

1. ca_certificate.pem  - CA 证书(用于验证客户端证书)
2. server_certificate.pem - 服务器证书
3. server_key.pem - 服务器私钥
4. client_certificate.pem - 客户端证书(可选,双向认证)
5. client_key.pem - 客户端私钥(可选,双向认证)

证书生成步骤

方式一:自签名证书(测试环境)

步骤 1:创建 CA 证书

bash
#!/bin/bash

# 创建工作目录
mkdir -p /tmp/rabbitmq-ssl
cd /tmp/rabbitmq-ssl

# 生成 CA 私钥
openssl genrsa -out ca.key 4096

# 生成 CA 证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca_certificate.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=IT/CN=MyCompany Root CA"

# 验证 CA 证书
openssl x509 -in ca_certificate.pem -text -noout

步骤 2:生成服务器证书

bash
#!/bin/bash

# 创建服务器私钥
openssl genrsa -out server_key.pem 4096

# 创建证书签名请求 (CSR)
openssl req -new -key server_key.pem -out server.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=IT/CN=rabbitmq.example.com"

# 创建扩展配置文件
cat > server_ext.cnf << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = rabbitmq.example.com
DNS.2 = localhost
DNS.3 = *.rabbitmq.example.com
IP.1 = 127.0.0.1
IP.2 = 192.168.1.100
EOF

# 使用 CA 签发服务器证书
openssl x509 -req -in server.csr -CA ca_certificate.pem -CAkey ca.key \
  -CAcreateserial -out server_certificate.pem -days 365 -sha256 \
  -extfile server_ext.cnf

# 验证服务器证书
openssl x509 -in server_certificate.pem -text -noout

步骤 3:生成客户端证书(双向认证)

bash
#!/bin/bash

# 创建客户端私钥
openssl genrsa -out client_key.pem 4096

# 创建客户端 CSR
openssl req -new -key client_key.pem -out client.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=IT/CN=client.example.com"

# 创建客户端扩展配置
cat > client_ext.cnf << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = clientAuth
EOF

# 使用 CA 签发客户端证书
openssl x509 -req -in client.csr -CA ca_certificate.pem -CAkey ca.key \
  -CAcreateserial -out client_certificate.pem -days 365 -sha256 \
  -extfile client_ext.cnf

# 验证客户端证书
openssl x509 -in client_certificate.pem -text -noout

步骤 4:验证证书链

bash
#!/bin/bash

# 验证证书链
openssl verify -CAfile ca_certificate.pem server_certificate.pem
openssl verify -CAfile ca_certificate.pem client_certificate.pem

# 检查证书匹配
openssl x509 -noout -modulus -in server_certificate.pem | openssl md5
openssl rsa -noout -modulus -in server_key.pem | openssl md5

步骤 5:部署证书

bash
#!/bin/bash

# 创建 RabbitMQ SSL 目录
sudo mkdir -p /etc/rabbitmq/ssl

# 复制证书文件
sudo cp ca_certificate.pem /etc/rabbitmq/ssl/
sudo cp server_certificate.pem /etc/rabbitmq/ssl/
sudo cp server_key.pem /etc/rabbitmq/ssl/

# 设置权限
sudo chown rabbitmq:rabbitmq /etc/rabbitmq/ssl/*
sudo chmod 600 /etc/rabbitmq/ssl/server_key.pem
sudo chmod 644 /etc/rabbitmq/ssl/ca_certificate.pem
sudo chmod 644 /etc/rabbitmq/ssl/server_certificate.pem

方式二:Let's Encrypt 免费证书(生产环境)

bash
#!/bin/bash

# 安装 certbot
sudo apt-get update
sudo apt-get install -y certbot

# 申请证书(需要域名解析到服务器)
sudo certbot certonly --standalone -d rabbitmq.example.com

# 证书位置
# /etc/letsencrypt/live/rabbitmq.example.com/fullchain.pem  - 服务器证书链
# /etc/letsencrypt/live/rabbitmq.example.com/privkey.pem    - 私钥

# 创建 RabbitMQ 使用的证书目录
sudo mkdir -p /etc/rabbitmq/ssl

# 复制证书
sudo cp /etc/letsencrypt/live/rabbitmq.example.com/fullchain.pem /etc/rabbitmq/ssl/server_certificate.pem
sudo cp /etc/letsencrypt/live/rabbitmq.example.com/privkey.pem /etc/rabbitmq/ssl/server_key.pem

# 下载 Let's Encrypt CA 证书
sudo wget -O /etc/rabbitmq/ssl/ca_certificate.pem https://letsencrypt.org/certs/isrgrootx1.pem

# 设置权限
sudo chown -R rabbitmq:rabbitmq /etc/rabbitmq/ssl
sudo chmod 600 /etc/rabbitmq/ssl/server_key.pem

# 设置自动续期
sudo crontab -e
# 添加:0 0 1 * * certbot renew --quiet && systemctl restart rabbitmq-server

方式三:商业 CA 证书

bash
#!/bin/bash

# 1. 生成私钥
openssl genrsa -out server_key.pem 2048

# 2. 生成 CSR
openssl req -new -key server_key.pem -out server.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=IT/CN=rabbitmq.example.com"

# 3. 将 server.csr 提交给 CA 机构

# 4. 收到证书后保存为 server_certificate.pem

# 5. 下载 CA 中间证书并合并
cat server_certificate.pem intermediate.pem > server_certificate_chain.pem

# 6. 部署
sudo cp server_certificate_chain.pem /etc/rabbitmq/ssl/server_certificate.pem
sudo cp server_key.pem /etc/rabbitmq/ssl/

RabbitMQ 证书配置

基础配置

conf
# /etc/rabbitmq/rabbitmq.conf

# TLS 监听端口
listeners.ssl.default = 5671

# 证书文件路径
ssl_options.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem
ssl_options.certfile   = /etc/rabbitmq/ssl/server_certificate.pem
ssl_options.keyfile    = /etc/rabbitmq/ssl/server_key.pem

# 私钥密码(如果有)
# ssl_options.password = your_password

完整生产配置

conf
# /etc/rabbitmq/rabbitmq.conf

# TLS 监听器
listeners.ssl.default = 5671

# 证书配置
ssl_options.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem
ssl_options.certfile   = /etc/rabbitmq/ssl/server_certificate.pem
ssl_options.keyfile    = /etc/rabbitmq/ssl/server_key.pem

# TLS 版本
ssl_options.versions.1 = tlsv1.3
ssl_options.versions.2 = tlsv1.2

# 加密套件
ssl_options.ciphers.1 = TLS_AES_256_GCM_SHA384
ssl_options.ciphers.2 = TLS_AES_128_GCM_SHA256
ssl_options.ciphers.3 = ECDHE-RSA-AES256-GCM-SHA384

# 客户端验证
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = false
ssl_options.depth = 2

# 会话配置
ssl_options.session_timeout = 3600
ssl_options.honor_cipher_order = true

# 管理 UI TLS
management.ssl.port = 15671
management.ssl.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem
management.ssl.certfile   = /etc/rabbitmq/ssl/server_certificate.pem
management.ssl.keyfile    = /etc/rabbitmq/ssl/server_key.pem

PHP 代码示例

证书生成工具类

php
<?php

class CertificateGenerator
{
    private $outputDir;
    private $caConfig = [
        'country' => 'CN',
        'state' => 'Beijing',
        'city' => 'Beijing',
        'organization' => 'MyCompany',
        'organizationalUnit' => 'IT'
    ];

    public function __construct(string $outputDir)
    {
        $this->outputDir = $outputDir;
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0700, true);
        }
    }

    public function generateCA(int $keyBits = 4096, int $days = 3650): array
    {
        $dn = [
            'countryName' => $this->caConfig['country'],
            'stateOrProvinceName' => $this->caConfig['state'],
            'localityName' => $this->caConfig['city'],
            'organizationName' => $this->caConfig['organization'],
            'organizationalUnitName' => $this->caConfig['organizationalUnit'],
            'commonName' => $this->caConfig['organization'] . ' Root CA'
        ];

        $privateKey = openssl_pkey_new([
            'private_key_bits' => $keyBits,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ]);

        $csr = openssl_csr_new($dn, $privateKey);
        $certificate = openssl_csr_sign($csr, null, $privateKey, $days);

        openssl_x509_export($certificate, $certOut);
        openssl_pkey_export($privateKey, $keyOut);

        $caCertPath = $this->outputDir . '/ca_certificate.pem';
        $caKeyPath = $this->outputDir . '/ca_key.pem';

        file_put_contents($caCertPath, $certOut);
        file_put_contents($caKeyPath, $keyOut);
        chmod($caKeyPath, 0600);

        return [
            'certificate' => $caCertPath,
            'private_key' => $caKeyPath
        ];
    }

    public function generateServerCertificate(
        string $caCertPath,
        string $caKeyPath,
        string $commonName,
        array $altNames = [],
        int $keyBits = 4096,
        int $days = 365
    ): array {
        $dn = [
            'countryName' => $this->caConfig['country'],
            'stateOrProvinceName' => $this->caConfig['state'],
            'localityName' => $this->caConfig['city'],
            'organizationName' => $this->caConfig['organization'],
            'organizationalUnitName' => $this->caConfig['organizationalUnit'],
            'commonName' => $commonName
        ];

        $privateKey = openssl_pkey_new([
            'private_key_bits' => $keyBits,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ]);

        $csr = openssl_csr_new($dn, $privateKey);

        $caCert = openssl_x509_read(file_get_contents($caCertPath));
        $caKey = openssl_pkey_get_private(file_get_contents($caKeyPath));

        $san = $this->buildSubjectAltNames($altNames);
        $certificate = openssl_csr_sign($csr, $caCert, $caKey, $days, [
            'digest_alg' => 'sha256',
            'x509_extensions' => $this->getServerExtensions($san)
        ]);

        openssl_x509_export($certificate, $certOut);
        openssl_pkey_export($privateKey, $keyOut);

        $serverCertPath = $this->outputDir . '/server_certificate.pem';
        $serverKeyPath = $this->outputDir . '/server_key.pem';

        file_put_contents($serverCertPath, $certOut);
        file_put_contents($serverKeyPath, $keyOut);
        chmod($serverKeyPath, 0600);

        return [
            'certificate' => $serverCertPath,
            'private_key' => $serverKeyPath
        ];
    }

    public function generateClientCertificate(
        string $caCertPath,
        string $caKeyPath,
        string $commonName,
        int $keyBits = 4096,
        int $days = 365
    ): array {
        $dn = [
            'countryName' => $this->caConfig['country'],
            'stateOrProvinceName' => $this->caConfig['state'],
            'localityName' => $this->caConfig['city'],
            'organizationName' => $this->caConfig['organization'],
            'organizationalUnitName' => $this->caConfig['organizationalUnit'],
            'commonName' => $commonName
        ];

        $privateKey = openssl_pkey_new([
            'private_key_bits' => $keyBits,
            'private_key_type' => OPENSSL_KEYTYPE_RSA
        ]);

        $csr = openssl_csr_new($dn, $privateKey);

        $caCert = openssl_x509_read(file_get_contents($caCertPath));
        $caKey = openssl_pkey_get_private(file_get_contents($caKeyPath));

        $certificate = openssl_csr_sign($csr, $caCert, $caKey, $days, [
            'digest_alg' => 'sha256',
            'x509_extensions' => $this->getClientExtensions()
        ]);

        openssl_x509_export($certificate, $certOut);
        openssl_pkey_export($privateKey, $keyOut);

        $clientCertPath = $this->outputDir . '/client_certificate.pem';
        $clientKeyPath = $this->outputDir . '/client_key.pem';

        file_put_contents($clientCertPath, $certOut);
        file_put_contents($clientKeyPath, $keyOut);
        chmod($clientKeyPath, 0600);

        return [
            'certificate' => $clientCertPath,
            'private_key' => $clientKeyPath
        ];
    }

    private function buildSubjectAltNames(array $altNames): string
    {
        $san = [];
        $dnsIndex = 1;
        $ipIndex = 1;

        foreach ($altNames as $type => $values) {
            foreach ((array)$values as $value) {
                if ($type === 'dns') {
                    $san[] = "DNS.{$dnsIndex} = {$value}";
                    $dnsIndex++;
                } elseif ($type === 'ip') {
                    $san[] = "IP.{$ipIndex} = {$value}";
                    $ipIndex++;
                }
            }
        }

        return implode("\n", $san);
    }

    private function getServerExtensions(string $san = ''): string
    {
        $ext = <<<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
{$san}
EOF;
        return $ext;
    }

    private function getClientExtensions(): string
    {
        return <<<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF;
    }
}

// 使用示例
$generator = new CertificateGenerator('/tmp/rabbitmq-ssl');

// 生成 CA
$ca = $generator->generateCA();
echo "CA 证书: {$ca['certificate']}\n";

// 生成服务器证书
$server = $generator->generateServerCertificate(
    $ca['certificate'],
    $ca['private_key'],
    'rabbitmq.example.com',
    [
        'dns' => ['rabbitmq.example.com', 'localhost', '*.rabbitmq.example.com'],
        'ip' => ['127.0.0.1', '192.168.1.100']
    ]
);
echo "服务器证书: {$server['certificate']}\n";

// 生成客户端证书
$client = $generator->generateClientCertificate(
    $ca['certificate'],
    $ca['private_key'],
    'client.example.com'
);
echo "客户端证书: {$client['certificate']}\n";

证书信息查看工具

php
<?php

class CertificateReader
{
    public function readCertificate(string $path): array
    {
        if (!file_exists($path)) {
            throw new RuntimeException("证书文件不存在: {$path}");
        }

        $content = file_get_contents($path);
        $cert = openssl_x509_read($content);

        if ($cert === false) {
            throw new RuntimeException("无法解析证书: {$path}");
        }

        $info = openssl_x509_parse($cert);

        return [
            'subject' => $this->formatDN($info['subject']),
            'issuer' => $this->formatDN($info['issuer']),
            'serial_number' => $info['serialNumber'],
            'version' => $info['version'],
            'valid_from' => date('Y-m-d H:i:s', $info['validFrom_time_t']),
            'valid_to' => date('Y-m-d H:i:s', $info['validTo_time_t']),
            'is_valid' => $info['validTo_time_t'] > time() && $info['validFrom_time_t'] <= time(),
            'days_remaining' => max(0, floor(($info['validTo_time_t'] - time()) / 86400)),
            'signature_algorithm' => $info['signatureTypeSN'] ?? 'unknown',
            'purposes' => $this->getPurposes($cert),
            'extensions' => $info['extensions'] ?? []
        ];
    }

    public function readPrivateKey(string $path, string $passphrase = null): array
    {
        if (!file_exists($path)) {
            throw new RuntimeException("私钥文件不存在: {$path}");
        }

        $content = file_get_contents($path);
        $key = openssl_pkey_get_private($content, $passphrase);

        if ($key === false) {
            throw new RuntimeException("无法解析私钥: {$path}");
        }

        $details = openssl_pkey_get_details($key);

        return [
            'type' => $this->getKeyTypeName($details['type']),
            'bits' => $details['bits'],
            'is_encrypted' => strpos($content, 'ENCRYPTED') !== false
        ];
    }

    public function verifyCertificateChain(string $certPath, string $caPath): bool
    {
        $cert = file_get_contents($certPath);
        $ca = file_get_contents($caPath);

        $certResource = openssl_x509_read($cert);
        $result = openssl_x509_checkpurpose($certResource, X509_PURPOSE_ANY, [$ca]);

        return $result === true;
    }

    public function checkCertificateMatch(string $certPath, string $keyPath): bool
    {
        $cert = file_get_contents($certPath);
        $key = file_get_contents($keyPath);

        $certModulus = openssl_x509_parse($cert);
        $keyResource = openssl_pkey_get_private($key);

        return openssl_x509_check_private_key(openssl_x509_read($cert), $keyResource);
    }

    private function formatDN(array $dn): string
    {
        $parts = [];
        foreach ($dn as $key => $value) {
            $parts[] = "{$key}={$value}";
        }
        return implode(', ', $parts);
    }

    private function getPurposes($cert): array
    {
        $purposes = [];
        $purposeConstants = [
            X509_PURPOSE_SSL_CLIENT => 'ssl_client',
            X509_PURPOSE_SSL_SERVER => 'ssl_server',
            X509_PURPOSE_SMIME_SIGN => 'smime_sign',
            X509_PURPOSE_SMIME_ENCRYPT => 'smime_encrypt',
            X509_PURPOSE_CRL_SIGN => 'crl_sign',
            X509_PURPOSE_ANY => 'any'
        ];

        foreach ($purposeConstants as $constant => $name) {
            $result = openssl_x509_checkpurpose($cert, $constant);
            $purposes[$name] = $result === true;
        }

        return $purposes;
    }

    private function getKeyTypeName(int $type): string
    {
        $types = [
            OPENSSL_KEYTYPE_RSA => 'RSA',
            OPENSSL_KEYTYPE_DSA => 'DSA',
            OPENSSL_KEYTYPE_DH => 'DH',
            OPENSSL_KEYTYPE_EC => 'EC'
        ];

        return $types[$type] ?? 'Unknown';
    }
}

// 使用示例
$reader = new CertificateReader();

$certInfo = $reader->readCertificate('/etc/rabbitmq/ssl/server_certificate.pem');
print_r($certInfo);

$keyInfo = $reader->readPrivateKey('/etc/rabbitmq/ssl/server_key.pem');
print_r($keyInfo);

$chainValid = $reader->verifyCertificateChain(
    '/etc/rabbitmq/ssl/server_certificate.pem',
    '/etc/rabbitmq/ssl/ca_certificate.pem'
);
echo "证书链验证: " . ($chainValid ? '通过' : '失败') . "\n";

实际应用场景

场景一:自动化证书部署

php
<?php

class CertificateDeployer
{
    private $rabbitmqSslDir = '/etc/rabbitmq/ssl';
    private $backupDir = '/backup/rabbitmq/ssl';

    public function deploy(array $certificates): array
    {
        $results = [
            'backup' => null,
            'deployed' => [],
            'errors' => []
        ];

        $results['backup'] = $this->backupExisting();

        foreach ($certificates as $name => $content) {
            try {
                $path = $this->rabbitmqSslDir . '/' . $name;
                file_put_contents($path, $content);

                if (strpos($name, 'key') !== false) {
                    chmod($path, 0600);
                } else {
                    chmod($path, 0644);
                }

                chown($path, 'rabbitmq');
                chgrp($path, 'rabbitmq');

                $results['deployed'][] = $name;
            } catch (Exception $e) {
                $results['errors'][] = [
                    'file' => $name,
                    'error' => $e->getMessage()
                ];
            }
        }

        return $results;
    }

    private function backupExisting(): ?string
    {
        if (!is_dir($this->rabbitmqSslDir)) {
            return null;
        }

        $timestamp = date('Ymd_His');
        $backupPath = $this->backupDir . '/' . $timestamp;

        if (!is_dir($backupPath)) {
            mkdir($backupPath, 0700, true);
        }

        $files = glob($this->rabbitmqSslDir . '/*.pem');
        foreach ($files as $file) {
            copy($file, $backupPath . '/' . basename($file));
        }

        return $backupPath;
    }

    public function rollback(string $backupPath): bool
    {
        if (!is_dir($backupPath)) {
            return false;
        }

        $files = glob($backupPath . '/*.pem');
        foreach ($files as $file) {
            copy($file, $this->rabbitmqSslDir . '/' . basename($file));
        }

        return true;
    }
}

常见问题与解决方案

问题 1:证书权限错误

错误信息

Error: unable to read server key file

解决方案

bash
# 检查文件权限
ls -la /etc/rabbitmq/ssl/

# 修复权限
chmod 600 /etc/rabbitmq/ssl/server_key.pem
chmod 644 /etc/rabbitmq/ssl/server_certificate.pem
chmod 644 /etc/rabbitmq/ssl/ca_certificate.pem
chown rabbitmq:rabbitmq /etc/rabbitmq/ssl/*

问题 2:证书链不完整

解决方案

bash
# 合并证书链
cat server_certificate.pem intermediate.pem > server_certificate_chain.pem

# 验证证书链
openssl verify -CAfile ca_certificate.pem -untrusted intermediate.pem server_certificate_chain.pem

问题 3:私钥与证书不匹配

解决方案

bash
# 检查私钥和证书的模数是否匹配
openssl x509 -noout -modulus -in server_certificate.pem | openssl md5
openssl rsa -noout -modulus -in server_key.pem | openssl md5

# 两个 MD5 值应该相同

最佳实践建议

1. 证书管理清单

php
<?php

class CertificateChecklist
{
    public function validate(array $paths): array
    {
        $checks = [
            'files_exist' => $this->checkFilesExist($paths),
            'permissions' => $this->checkPermissions($paths),
            'validity' => $this->checkValidity($paths),
            'chain' => $this->checkChain($paths),
            'match' => $this->checkKeyCertMatch($paths)
        ];

        $allPassed = !in_array(false, array_column($checks, 'passed'), true);

        return [
            'all_passed' => $allPassed,
            'checks' => $checks
        ];
    }

    private function checkFilesExist(array $paths): array
    {
        $missing = [];
        foreach ($paths as $name => $path) {
            if (!file_exists($path)) {
                $missing[] = $name;
            }
        }

        return [
            'passed' => empty($missing),
            'missing' => $missing
        ];
    }

    private function checkPermissions(array $paths): array
    {
        $issues = [];
        foreach ($paths as $name => $path) {
            if (!file_exists($path)) continue;

            $perms = fileperms($path);
            if (strpos($name, 'key') !== false && ($perms & 077) !== 0) {
                $issues[] = "{$name}: 私钥权限过于宽松";
            }
        }

        return [
            'passed' => empty($issues),
            'issues' => $issues
        ];
    }

    private function checkValidity(array $paths): array
    {
        $reader = new CertificateReader();
        $issues = [];

        foreach (['ca_cert', 'server_cert'] as $name) {
            if (!isset($paths[$name]) || !file_exists($paths[$name])) continue;

            $info = $reader->readCertificate($paths[$name]);
            if (!$info['is_valid']) {
                $issues[] = "{$name}: 证书无效或已过期";
            } elseif ($info['days_remaining'] < 30) {
                $issues[] = "{$name}: 证书将在 {$info['days_remaining']} 天后过期";
            }
        }

        return [
            'passed' => empty($issues),
            'issues' => $issues
        ];
    }

    private function checkChain(array $paths): array
    {
        if (!isset($paths['ca_cert']) || !isset($paths['server_cert'])) {
            return ['passed' => true, 'message' => '跳过证书链检查'];
        }

        $reader = new CertificateReader();
        $valid = $reader->verifyCertificateChain($paths['server_cert'], $paths['ca_cert']);

        return [
            'passed' => $valid,
            'message' => $valid ? '证书链验证通过' : '证书链验证失败'
        ];
    }

    private function checkKeyCertMatch(array $paths): array
    {
        if (!isset($paths['server_key']) || !isset($paths['server_cert'])) {
            return ['passed' => true, 'message' => '跳过密钥匹配检查'];
        }

        $reader = new CertificateReader();
        $match = $reader->checkCertificateMatch($paths['server_cert'], $paths['server_key']);

        return [
            'passed' => $match,
            'message' => $match ? '私钥与证书匹配' : '私钥与证书不匹配'
        ];
    }
}

安全注意事项

重要警告

  1. 私钥保护:私钥文件必须设置 600 权限,仅允许所有者读取
  2. 证书备份:定期备份证书和私钥到安全位置
  3. 有效期监控:设置告警监控证书过期时间
  4. 安全传输:证书文件传输必须使用加密通道
  5. 访问控制:限制证书目录的访问权限

相关链接