Skip to content

RabbitMQ 双向认证 (mTLS)

概述

双向 TLS(Mutual TLS,简称 mTLS)是一种更安全的认证方式,不仅服务器需要向客户端出示证书,客户端也需要向服务器出示证书进行身份验证。这种方式提供了更强的安全性,广泛应用于零信任网络架构中。

核心知识点

mTLS 认证流程

┌─────────────────────────────────────────────────────────────┐
│                    mTLS 双向认证流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐                      ┌─────────────┐       │
│  │   Client    │                      │  RabbitMQ   │       │
│  │             │                      │   Server    │       │
│  └──────┬──────┘                      └──────┬──────┘       │
│         │                                    │              │
│         │  1. Client Hello                   │              │
│         │───────────────────────────────────▶│              │
│         │                                    │              │
│         │  2. Server Hello + Server Cert     │              │
│         │◀───────────────────────────────────│              │
│         │                                    │              │
│         │  3. Certificate Request            │              │
│         │◀───────────────────────────────────│              │
│         │                                    │              │
│         │  4. Client Certificate             │              │
│         │───────────────────────────────────▶│              │
│         │                                    │              │
│         │  5. Verify Client Cert             │              │
│         │         (验证客户端证书)            │              │
│         │                                    │              │
│         │  6. Key Exchange                   │              │
│         │◀──────────────────────────────────▶│              │
│         │                                    │              │
│         │  7. Encrypted Data Transfer        │              │
│         │◀──────────────────────────────────▶│              │
│         │                                    │              │
│  └───────────────────────────────────────────┴─────────────┘
│                                                             │
└─────────────────────────────────────────────────────────────┘

mTLS 与单向 TLS 对比

特性单向 TLS双向 TLS (mTLS)
服务器证书✅ 需要✅ 需要
客户端证书❌ 不需要✅ 需要
客户端验证用户名/密码证书 + 用户名/密码
安全级别极高
配置复杂度
适用场景一般应用高安全要求

mTLS 证书体系

                    ┌─────────────────┐
                    │    Root CA      │
                    │  (根证书颁发机构) │
                    └────────┬────────┘

              ┌──────────────┼──────────────┐
              │              │              │
              ▼              ▼              ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐
        │ Server   │  │ Client   │  │ Client   │
        │   Cert   │  │ Cert #1  │  │ Cert #2  │
        │(服务器证书)│  │(客户端1)  │  │(客户端2)  │
        └──────────┘  └──────────┘  └──────────┘
              │              │              │
              ▼              ▼              ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐
        │ Server   │  │ Client   │  │ Client   │
        │   Key    │  │  Key #1  │  │  Key #2  │
        │(服务器私钥)│  │(私钥1)    │  │(私钥2)    │
        └──────────┘  └──────────┘  └──────────┘

配置示例

服务端 mTLS 配置

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.verify     = verify_peer
ssl_options.fail_if_no_peer_cert = true

# 证书链深度
ssl_options.depth = 2

# 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

客户端证书认证配置

conf
# 启用证书认证插件
# rabbitmq-plugins enable rabbitmq_auth_mechanism_ssl

# 证书用户名提取
ssl_cert_login_from = common_name

# 或从证书主题中提取
# ssl_cert_login_from = subject

# 或从 SAN 中提取
# ssl_cert_login_from = san

# 证书 DN 格式
ssl_cert_login_san_type = dns

管理 UI mTLS 配置

conf
# 管理 UI mTLS 配置
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

# 强制客户端证书
management.ssl.verify     = verify_peer
management.ssl.fail_if_no_peer_cert = true

集群节点间 mTLS 配置

conf
# 集群间 TLS 通信
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config

# 集群 TLS 配置
cluster_formation.classic_config.nodes.1 = rabbit@node1.example.com
cluster_formation.classic_config.nodes.2 = rabbit@node2.example.com

# 集群间 TLS
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

# 集群间互验证
cluster_formation.node_cleanup.only_log_warning = true

PHP 代码示例

mTLS 客户端连接

php
<?php

use PhpAmqpLib\Connection\AMQPSSLConnection;

class MTLSRabbitMQClient
{
    private $config;

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    public function connect(): AMQPSSLConnection
    {
        $sslOptions = [
            'cafile' => $this->config['ca_cert'],
            'local_cert' => $this->config['client_cert_chain'] ?? $this->config['client_cert'],
            'local_pk' => $this->config['client_key'],
            'passphrase' => $this->config['passphrase'] ?? null,
            'verify_peer' => true,
            'verify_peer_name' => true,
            'allow_self_signed' => false,
            'ciphers' => 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES256-GCM-SHA384'
        ];

        return new AMQPSSLConnection(
            $this->config['host'],
            $this->config['port'] ?? 5671,
            $this->config['username'] ?? '',
            $this->config['password'] ?? '',
            $this->config['vhost'] ?? '/',
            $sslOptions,
            [
                'connection_timeout' => 10,
                'read_write_timeout' => 30
            ]
        );
    }

    public function testConnection(): array
    {
        try {
            $connection = $this->connect();
            $isConnected = $connection->isConnected();

            $channel = $connection->channel();
            $queueInfo = $channel->queue_declare('', false, false, true, false);
            $channel->close();
            $connection->close();

            return [
                'success' => true,
                'connected' => $isConnected,
                'message' => 'mTLS 双向认证连接成功'
            ];
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
}

// 使用示例
$client = new MTLSRabbitMQClient([
    'host' => 'rabbitmq.example.com',
    'port' => 5671,
    'vhost' => '/',
    'ca_cert' => '/path/to/ca_certificate.pem',
    'client_cert' => '/path/to/client_certificate.pem',
    'client_key' => '/path/to/client_key.pem',
    'passphrase' => 'optional_passphrase'
]);

$connection = $client->connect();
echo "mTLS 连接已建立\n";

mTLS 连接池管理

php
<?php

class MTLSConnectionPool
{
    private static $connections = [];
    private static $maxConnections = 10;
    private static $config;

    public static function init(array $config): void
    {
        self::$config = $config;
    }

    public static function getConnection(string $clientId): ?AMQPSSLConnection
    {
        $key = self::generateKey($clientId);

        if (isset(self::$connections[$key])) {
            $conn = self::$connections[$key];
            if ($conn->isConnected()) {
                return $conn;
            }
            unset(self::$connections[$key]);
        }

        if (count(self::$connections) >= self::$maxConnections) {
            self::cleanup();
        }

        $sslOptions = [
            'cafile' => self::$config['ca_cert'],
            'local_cert' => self::$config['client_certs'][$clientId]['cert'] ?? null,
            'local_pk' => self::$config['client_certs'][$clientId]['key'] ?? null,
            'verify_peer' => true,
            'verify_peer_name' => true
        ];

        try {
            $connection = new AMQPSSLConnection(
                self::$config['host'],
                self::$config['port'],
                self::$config['username'] ?? '',
                self::$config['password'] ?? '',
                self::$config['vhost'] ?? '/',
                $sslOptions
            );

            self::$connections[$key] = $connection;
            return $connection;
        } catch (Exception $e) {
            error_log("mTLS 连接失败: " . $e->getMessage());
            return null;
        }
    }

    private static function generateKey(string $clientId): string
    {
        return md5($clientId . ':' . self::$config['host']);
    }

    private static function cleanup(): void
    {
        foreach (self::$connections as $key => $conn) {
            if (!$conn->isConnected()) {
                unset(self::$connections[$key]);
            }
        }
    }

    public static function closeAll(): void
    {
        foreach (self::$connections as $conn) {
            try {
                if ($conn->isConnected()) {
                    $conn->close();
                }
            } catch (Exception $e) {
                error_log("关闭连接失败: " . $e->getMessage());
            }
        }
        self::$connections = [];
    }
}

证书用户映射服务

php
<?php

class CertificateUserMapper
{
    private $rabbitmqApiUrl;
    private $adminCredentials;

    public function __construct(string $host, int $port, string $adminUser, string $adminPassword)
    {
        $this->rabbitmqApiUrl = "http://{$host}:{$port}/api";
        $this->adminCredentials = [$adminUser, $adminPassword];
    }

    public function mapCertificateToUser(string $certCommonName, array $permissions = []): bool
    {
        $username = $this->normalizeUsername($certCommonName);

        $userData = [
            'password' => '',
            'tags' => ''
        ];

        $response = $this->httpRequest(
            "{$this->rabbitmqApiUrl}/users/{$username}",
            'PUT',
            $userData
        );

        if ($response['status'] !== 204) {
            return false;
        }

        if (!empty($permissions)) {
            return $this->setPermissions($username, $permissions);
        }

        return true;
    }

    public function setPermissions(string $username, array $permissions): bool
    {
        $vhost = urlencode($permissions['vhost'] ?? '/');
        $url = "{$this->rabbitmqApiUrl}/permissions/{$vhost}/{$username}";

        $response = $this->httpRequest($url, 'PUT', [
            'configure' => $permissions['configure'] ?? '.*',
            'write' => $permissions['write'] ?? '.*',
            'read' => $permissions['read'] ?? '.*'
        ]);

        return $response['status'] === 204;
    }

    public function revokeCertificateUser(string $certCommonName): bool
    {
        $username = $this->normalizeUsername($certCommonName);

        $response = $this->httpRequest(
            "{$this->rabbitmqApiUrl}/users/{$username}",
            'DELETE'
        );

        return $response['status'] === 204;
    }

    public function listCertificateUsers(): array
    {
        $response = $this->httpRequest("{$this->rabbitmqApiUrl}/users", 'GET');

        if ($response['status'] !== 200) {
            return [];
        }

        $users = json_decode($response['body'], true) ?: [];

        return array_filter($users, function ($user) {
            return empty($user['password_hash']) || $user['password_hash'] === '';
        });
    }

    private function normalizeUsername(string $commonName): string
    {
        return strtolower(preg_replace('/[^a-zA-Z0-9_-]/', '_', $commonName));
    }

    private function httpRequest(string $url, string $method, array $data = null): array
    {
        $ch = curl_init($url);

        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_USERPWD => implode(':', $this->adminCredentials),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json']
        ];

        if ($data !== null) {
            $options[CURLOPT_POSTFIELDS] = json_encode($data);
        }

        curl_setopt_array($ch, $options);

        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return ['status' => $status, 'body' => $body];
    }
}

// 使用示例
$mapper = new CertificateUserMapper('localhost', 15672, 'admin', 'password');

// 映射证书到用户
$mapper->mapCertificateToUser('client.example.com', [
    'vhost' => '/',
    'configure' => '^client_.*',
    'write' => '^client_.*',
    'read' => '^client_.*'
]);

mTLS 证书验证器

php
<?php

class MTLSValidator
{
    public function validateClientCertificate(string $certPath, string $keyPath, string $caPath): array
    {
        $results = [
            'certificate' => $this->validateCertificate($certPath),
            'key' => $this->validateKey($keyPath),
            'chain' => $this->validateChain($certPath, $caPath),
            'match' => $this->validateKeyCertMatch($certPath, $keyPath),
            'purpose' => $this->validateClientAuthPurpose($certPath)
        ];

        $results['valid'] = $results['certificate']['valid']
            && $results['key']['valid']
            && $results['chain']['valid']
            && $results['match']
            && $results['purpose'];

        return $results;
    }

    private function validateCertificate(string $path): array
    {
        if (!file_exists($path)) {
            return ['valid' => false, 'error' => '证书文件不存在'];
        }

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

        if ($cert === false) {
            return ['valid' => false, 'error' => '无法解析证书'];
        }

        $info = openssl_x509_parse($cert);
        $now = time();

        if ($info['validTo_time_t'] < $now) {
            return ['valid' => false, 'error' => '证书已过期'];
        }

        if ($info['validFrom_time_t'] > $now) {
            return ['valid' => false, 'error' => '证书尚未生效'];
        }

        return [
            'valid' => true,
            'subject' => $info['subject']['CN'] ?? 'Unknown',
            'issuer' => $info['issuer']['CN'] ?? 'Unknown',
            'valid_to' => date('Y-m-d H:i:s', $info['validTo_time_t']),
            'days_remaining' => floor(($info['validTo_time_t'] - $now) / 86400)
        ];
    }

    private function validateKey(string $path): array
    {
        if (!file_exists($path)) {
            return ['valid' => false, 'error' => '私钥文件不存在'];
        }

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

        if ($key === false) {
            return ['valid' => false, 'error' => '无法解析私钥'];
        }

        $details = openssl_pkey_get_details($key);

        return [
            'valid' => true,
            'type' => $this->getKeyTypeName($details['type']),
            'bits' => $details['bits']
        ];
    }

    private function validateChain(string $certPath, string $caPath): array
    {
        $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 [
            'valid' => $result === true,
            'message' => $result === true ? '证书链验证通过' : '证书链验证失败'
        ];
    }

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

        $certResource = openssl_x509_read($cert);
        $keyResource = openssl_pkey_get_private($key);

        return openssl_x509_check_private_key($certResource, $keyResource);
    }

    private function validateClientAuthPurpose(string $certPath): bool
    {
        $cert = file_get_contents($certPath);
        $certResource = openssl_x509_read($cert);

        $result = openssl_x509_checkpurpose($certResource, X509_PURPOSE_SSL_CLIENT);

        return $result === true;
    }

    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';
    }
}

// 使用示例
$validator = new MTLSValidator();
$result = $validator->validateClientCertificate(
    '/path/to/client_certificate.pem',
    '/path/to/client_key.pem',
    '/path/to/ca_certificate.pem'
);

if ($result['valid']) {
    echo "客户端证书验证通过\n";
} else {
    echo "客户端证书验证失败\n";
    print_r($result);
}

实际应用场景

场景一:微服务 mTLS 认证

php
<?php

class MicroserviceMTLSAuth
{
    private $serviceId;
    private $certPath;
    private $keyPath;
    private $caPath;

    public function __construct(string $serviceId, array $certConfig)
    {
        $this->serviceId = $serviceId;
        $this->certPath = $certConfig['cert'];
        $this->keyPath = $certConfig['key'];
        $this->caPath = $certConfig['ca'];
    }

    public function createConnection(): AMQPSSLConnection
    {
        $sslOptions = [
            'cafile' => $this->caPath,
            'local_cert' => $this->certPath,
            'local_pk' => $this->keyPath,
            'verify_peer' => true,
            'verify_peer_name' => true,
            'allow_self_signed' => false
        ];

        return new AMQPSSLConnection(
            getenv('RABBITMQ_HOST'),
            (int)getenv('RABBITMQ_TLS_PORT'),
            '',
            '',
            getenv('RABBITMQ_VHOST') ?: '/',
            $sslOptions
        );
    }

    public function getServiceInfo(): array
    {
        $certContent = file_get_contents($this->certPath);
        $cert = openssl_x509_read($certContent);
        $info = openssl_x509_parse($cert);

        return [
            'service_id' => $this->serviceId,
            'certificate_cn' => $info['subject']['CN'] ?? null,
            'certificate_issuer' => $info['issuer']['CN'] ?? null,
            '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'])
        ];
    }
}

场景二:证书轮换管理

php
<?php

class MTLSRotationManager
{
    private $certDir;
    private $backupDir;
    private $rabbitmqApi;

    public function rotateClientCertificate(string $clientId, array $newCerts): array
    {
        $results = [
            'backup' => null,
            'deployed' => false,
            'user_updated' => false,
            'errors' => []
        ];

        try {
            $results['backup'] = $this->backupCertificate($clientId);

            $this->deployCertificate($clientId, $newCerts);
            $results['deployed'] = true;

            $this->updateUserMapping($clientId);
            $results['user_updated'] = true;
        } catch (Exception $e) {
            $results['errors'][] = $e->getMessage();

            if ($results['backup']) {
                $this->rollbackCertificate($clientId, $results['backup']);
            }
        }

        return $results;
    }

    private function backupCertificate(string $clientId): ?string
    {
        $certFile = $this->certDir . "/{$clientId}_certificate.pem";
        $keyFile = $this->certDir . "/{$clientId}_key.pem";

        if (!file_exists($certFile)) {
            return null;
        }

        $timestamp = date('Ymd_His');
        $backupPath = $this->backupDir . "/{$clientId}_{$timestamp}";

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

        if (file_exists($certFile)) {
            copy($certFile, $backupPath . '/certificate.pem');
        }
        if (file_exists($keyFile)) {
            copy($keyFile, $backupPath . '/key.pem');
        }

        return $backupPath;
    }

    private function deployCertificate(string $clientId, array $newCerts): void
    {
        $certPath = $this->certDir . "/{$clientId}_certificate.pem";
        $keyPath = $this->certDir . "/{$clientId}_key.pem";

        file_put_contents($certPath, $newCerts['certificate']);
        file_put_contents($keyPath, $newCerts['key']);

        chmod($certPath, 0644);
        chmod($keyPath, 0600);
    }

    private function updateUserMapping(string $clientId): void
    {
        // 更新 RabbitMQ 用户映射
    }

    private function rollbackCertificate(string $clientId, string $backupPath): void
    {
        $certFile = $this->certDir . "/{$clientId}_certificate.pem";
        $keyFile = $this->certDir . "/{$clientId}_key.pem";

        if (file_exists($backupPath . '/certificate.pem')) {
            copy($backupPath . '/certificate.pem', $certFile);
        }
        if (file_exists($backupPath . '/key.pem')) {
            copy($backupPath . '/key.pem', $keyFile);
        }
    }
}

常见问题与解决方案

问题 1:客户端证书验证失败

错误信息

SSL: certificate verify failed

解决方案

bash
# 检查客户端证书链
openssl verify -CAfile /etc/rabbitmq/ssl/ca_certificate.pem /path/to/client_certificate.pem

# 检查客户端证书用途
openssl x509 -in /path/to/client_certificate.pem -noout -purpose | grep "SSL client"

# 检查证书扩展
openssl x509 -in /path/to/client_certificate.pem -noout -text | grep -A1 "Extended Key Usage"

问题 2:用户名提取失败

解决方案

conf
# 确保配置正确的用户名提取方式
ssl_cert_login_from = common_name

# 检查证书 CN
openssl x509 -in /path/to/client_certificate.pem -noout -subject

问题 3:证书权限不足

解决方案

bash
# 确保用户已创建并设置权限
rabbitmqctl add_user client_cn ""
rabbitmqctl set_permissions -p / client_cn ".*" ".*" ".*"

# 或使用 API 创建
curl -u admin:password -X PUT http://localhost:15672/api/users/client_cn \
  -H "Content-Type: application/json" \
  -d '{"password":"","tags":""}'

最佳实践建议

1. 证书生命周期管理

php
<?php

class MTLSLifecycleManager
{
    private $certDir;
    private $warningDays = 30;

    public function checkAllCertificates(): array
    {
        $certificates = glob($this->certDir . '/*_certificate.pem');
        $results = [];

        foreach ($certificates as $certPath) {
            $clientId = basename($certPath, '_certificate.pem');
            $results[$clientId] = $this->checkCertificate($certPath);
        }

        return $results;
    }

    private function checkCertificate(string $path): array
    {
        $content = file_get_contents($path);
        $cert = openssl_x509_read($content);
        $info = openssl_x509_parse($cert);

        $now = time();
        $daysRemaining = floor(($info['validTo_time_t'] - $now) / 86400);

        return [
            'valid' => $info['validTo_time_t'] > $now,
            'days_remaining' => $daysRemaining,
            'needs_renewal' => $daysRemaining <= $this->warningDays,
            'expires_at' => date('Y-m-d H:i:s', $info['validTo_time_t'])
        ];
    }
}

2. 证书撤销列表 (CRL)

conf
# 启用 CRL 检查
ssl_options.crl_check = true
ssl_options.crl_file = /etc/rabbitmq/ssl/crl.pem

安全注意事项

重要警告

  1. 强制客户端证书:生产环境必须设置 fail_if_no_peer_cert = true
  2. 证书有效期:定期检查并续期客户端证书
  3. 私钥保护:客户端私钥必须安全存储,设置 600 权限
  4. 证书撤销:建立证书撤销机制,及时吊销泄露的证书
  5. 审计日志:记录所有证书认证事件

相关链接