安全问题
证书、JWT 与 KMS 相关的安全故障排查。
与安全相关的故障通常会阻止验证者启动或对synchronizer及其 API 进行身份验证。这些错误通常很神秘,但它们属于几个明确定义的类别。
证书问题
证书到期
验证者(用于其 Ledger API、管理 API 或入口)使用的 TLS 证书将在固定日期到期。当它们过期时,连接会立即失败。
症状:
javax.net.ssl.SSLHandshakeException: PKIX path validation failed:
java.security.cert.CertPathValidatorException: validity check failed
检查有效期:
# Check a PEM file
openssl x509 -in /path/to/cert.pem -noout -enddate
# Check a running server
openssl s_client -connect your-validator:443 2>/dev/null | openssl x509 -noout -dates
解决方案: 通过您的 CA 或证书管理器(Let’s Encrypt、AWS ACM 等)续订证书。替换证书文件后,重新启动使用它的服务。对于 Kubernetes 入口控制器,一旦 Secret 更新,通常会自动重新启动。
Kubernetes 中的证书续订
如果您使用cert-manager,请检查证书资源是否显示续订失败:
kubectl describe certificate validator-tls -n validator
查找类似 The certificate request has failed 或 Order is in state errored 的事件。常见原因:DNS 质询失败(检查 DNS 传播)、HTTP 质询失败(检查入口路由)或颁发者的凭据已过期。
不完整的CA链
如果客户端可以从某些位置连接到验证者,但不能从其他位置连接到验证者,则服务器可能会提供不完整的证书链。验证:
openssl s_client -connect your-validator:443 -servername your-validator 2>&1 | grep "verify return"
如果您看到verify return:0,则链条已损坏。按照从叶到根的顺序将中间 CA 证书连接到服务器证书文件中。
JWT 验证失败
JWT 令牌向验证者的 Ledger API 验证 API 客户端(您的应用程序、钱包 UI 或自动化脚本)。
时钟偏差
ACCESS_TOKEN_EXPIRED(2,0): Claims were valid until 2025-09-08T08:32:07Z,
current time is 2025-09-08T08:32:07.254413051Z
如果令牌生命周期很短,即使令牌发行者的时钟和验证者的时钟之间存在亚秒级的差异,也可能导致过期失败。修复:
-
将 OIDC 提供商中的访问令牌生命周期增加到至少 15 分钟。
-
确保 NTP 正在验证者主机上运行并同步:
timedatectl status # or chronyc tracking
错误的观众主张
PERMISSION_DENIED: JWT token has wrong audience. Expected: https://ledger_api.example.com
JWT 中的 aud 声明必须与验证者上配置的 LEDGER_API_AUTH_AUDIENCE 匹配。检查您的 OIDC 提供商的应用程序设置,并使受众与您的验证者配置保持一致。
过期的令牌
如果 API 调用间歇性失败并显示 UNAUTHENTICATED,则您的令牌刷新逻辑可能无法正常工作。验证:
- 令牌刷新间隔比令牌生命周期短。
- OIDC 提供商的令牌端点可从进行调用的应用程序访问。
- 刷新令牌本身尚未过期(刷新令牌通常具有较长但仍然有限的生命周期)。
JWKS 端点无法访问
如果验证者无法到达 JWKS(JSON Web 密钥集)端点来验证令牌签名:
Failed to fetch JWKS from https://your-tenant.auth0.com/.well-known/jwks.json
检查从验证者到 OIDC 提供商的网络连接。在 Kubernetes 中,DNS 解析问题或出口限制可能会阻止此操作。
密钥管理
KMS 连接
如果您的验证者使用云 KMS(AWS KMS、GCP Cloud KMS、Azure Key Vault)来签名密钥,连接故障会阻止事务签名。
症状:
com.amazonaws.SdkClientException: Unable to execute HTTP request: Connect to kms.us-east-1.amazonaws.com:443
检查:* 验证 KMS 端点是否可以从验证者 Pod 访问。
- 确认 IAM 角色或服务账户具有
kms:Sign、kms:GetPublicKey和kms:Decrypt权限。 - 如果在 EKS 中使用 IRSA(服务帐户的 IAM 角色),请验证服务帐户注释和 OIDC 提供商信任。
权限错误
AccessDeniedException: User: arn:aws:sts::123456789:assumed-role/validator-role/...
is not authorized to perform: kms:Sign on resource: arn:aws:kms:...
附加到验证者角色的 IAM 策略必须包含特定的 KMS 密钥 ARN。资源字段中的通配符 (*) 用于测试,但应将范围限定为生产中的确切键。
按键轮换
Canton 在节点初始化期间生成加密密钥。如果您需要轮换密钥(例如,在可疑的泄露之后),请使用 Canton 控制台:
@ participant1.keys.secret.list()
res1: Seq[com.digitalasset.canton.crypto.admin.grpc.PrivateKeyMetadata] = Vector(
PrivateKeyMetadata(
publicKeyWithName = SigningPublicKeyWithName(
publicKey = SigningPublicKey(
id = 12201ff69b1d...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = namespace
),
name = Some(KeyName(participant1-namespace))
),
wrapperKeyId = None,
kmsKeyId = None
),
PrivateKeyMetadata(
publicKeyWithName = SigningPublicKeyWithName(
publicKey = SigningPublicKey(
id = 12200cd80d7c...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = Set(signing, proof-of-ownership)
),
name = Some(KeyName(participant1-signing))
),
wrapperKeyId = None,
kmsKeyId = None
),
PrivateKeyMetadata(
publicKeyWithName = SigningPublicKeyWithName(
publicKey = SigningPublicKey(
id = 1220176a4b95...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = Set(sequencer-auth, proof-of-ownership)
),
name = Some(KeyName(participant1-sequencer-auth))
),
wrapperKeyId = None,
kmsKeyId = None
),
PrivateKeyMetadata(
publicKeyWithName = EncryptionPublicKeyWithName(
publicKey = EncryptionPublicKey(
id = 122029634afd...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-P256
),
name = Some(KeyName(participant1-encryption))
),
wrapperKeyId = None,
kmsKeyId = None
)
)
@ val newKey = participant1.keys.secret.generate_signing_key(name = "new-signing-key", usage = SigningKeyUsage.All)
newKey : SigningPublicKey = SigningPublicKey(
id = 122034956d8a...,
format = DER-encoded X.509 SubjectPublicKeyInfo,
keySpec = EC-Curve25519,
usage = Set(namespace, sequencer-auth, signing, proof-of-ownership)
)
密钥轮换需要仔细协调。始终首先在非生产环境中进行测试。
TLS 故障排除
可以使用此处描述的参数配置 TLS。
如果您在设置 SSL/TLS 时遇到问题,您可以启用 SSL 调试、增加 netty 日志记录或生成测试密钥和证书来验证您的配置。
启用 ssl 调试日志记录
您可以通过在启动 Canton 时将以下标志添加到命令行来启用 SSL 调试:
bin/canton -Djavax.net.debug=all
这会将详细的 SSL 相关信息打印到控制台,包括握手过程的详细信息。建议仅在故障排除时使用此标志,因为输出可能非常冗长,并且可能会影响应用程序的性能。
启用 netty 的调试日志记录
网络库netty提供的有关 TLS 问题的错误消息并不理想。如果您在设置 TLS 时遇到困难,请在 io.netty 记录器上启用 DEBUG 日志记录。这通常可以通过将以下行添加到 logback 日志配置中来完成:```xml theme={“theme”:{“light”:“github-light”,“dark”:“github-dark”}}
## 生成测试密钥和证书
如果您需要生成测试 TLS 证书来测试您的配置,可以使用以下 OpenSSL 脚本:
```none theme={"theme":{"light":"github-light","dark":"github-dark"}}
# include certs-common.sh from config/tls
. "$(dirname "${BASH_SOURCE[0]}")/certs-common.sh"
# create root certificate such that we can issue self-signed certs
create_key "root-ca"
create_certificate "root-ca" "/O=TESTING/OU=ROOT CA/emailAddress=canton@digitalasset.com"
print_certificate "root-ca"
# create public api certificate
create_key "public-api"
create_csr "public-api" "/O=TESTING/OU=DOMAIN/CN=localhost/emailAddress=canton@digitalasset.com" "DNS:localhost,IP:127.0.0.1"
sign_csr "public-api" "root-ca"
print_certificate "public-api"
# create participant ledger-api certificate
create_key "ledger-api"
create_csr "ledger-api" "/O=TESTING/OU=PARTICIPANT/CN=localhost/emailAddress=canton@digitalasset.com" "DNS:localhost,IP:127.0.0.1"
sign_csr "ledger-api" "root-ca"
# create participant admin-api certificate
create_key "admin-api"
create_csr "admin-api" "/O=TESTING/OU=PARTICIPANT ADMIN/CN=localhost/emailAddress=canton@digitalasset.com" "DNS:localhost,IP:127.0.0.1"
sign_csr "admin-api" "root-ca"
# create participant client key and certificate
create_key "admin-client"
create_csr "admin-client" "/O=TESTING/OU=PARTICIPANT ADMIN CLIENT/CN=localhost/emailAddress=canton@digitalasset.com"
sign_csr "admin-client" "root-ca"
print_certificate "admin-client"
如果您希望手动生成自己的密钥和证书集,则在此过程中使用的命令记录在此处:
DAYS=3650
function create_key {
local name=$1
openssl genrsa -out "${name}.key" 4096
# netty requires the keys in pkcs8 format, therefore convert them appropriately
openssl pkcs8 -topk8 -nocrypt -in "${name}.key" -out "${name}.pem"
}
# create self signed certificate
function create_certificate {
local name=$1
local subj=$2
openssl req -new -x509 -sha256 -key "${name}.key" \
-out "${name}.crt" -days ${DAYS} -subj "$subj"
}
# create certificate signing request with subject and SAN
# we need the SANs as our certificates also need to include localhost or the
# loopback IP for the console access to the admin-api and the ledger-api
function create_csr {
local name=$1
local subj=$2
local san=$3
(
echo "authorityKeyIdentifier=keyid,issuer"
echo "basicConstraints=CA:FALSE"
echo "keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment"
) > ${name}.ext
if [[ -n $san ]]; then
echo "subjectAltName=${san}" >> ${name}.ext
fi
# create certificate (but ensure that localhost is there as SAN as otherwise, admin local connections won't work)
openssl req -new -sha256 -key "${name}.key" -out "${name}.csr" -subj "$subj"
}
function sign_csr {
local name=$1
local sign=$2
openssl x509 -req -sha256 -in "${name}.csr" -extfile "${name}.ext" -CA "${sign}.crt" -CAkey "${sign}.key" -CAcreateserial \
-out "${name}.crt" -days ${DAYS}
rm "${name}.ext" "${name}.csr"
}
function print_certificate {
local name=$1
openssl x509 -in "${name}.crt" -text -noout
}
本文由 CC Privacy Club 根据 Canton Network 官方文档(CC-BY-4.0)整理翻译,仅供学习;实现细节以官方最新版本为准。