使用 JDK 工具和 JFR 事件监控 Java 应用程序安全
JFR 安全事件概述
监控 Java 应用程序的底层安全配置可以让你了解其在加密标准方面的整体强度。JDK 12 引入了四个 JDK Flight Recorder(JFR) 安全事件,默认情况下在 default.jfc
和 profile.jfc
JFR 配置文件中被禁用
jdk.SecurityPropertyModification
用于记录Security.setProperty(String key, String value)
方法调用jdk.TLSHandshake
用于跟踪 TLS 握手活动jdk.X509Validation
用于记录在成功的 X.509 验证中协商的 X.509 证书的详细信息jdk.X509Certificate
用于记录 X.509 证书的详细信息。
这些事件也被移植到 Oracle JDK 11.0.5 和 8u231 更新版本中。你可以通过修改 JFR 配置文件或通过标准 JFR 选项来启用这些事件。查看 JDK Flight Recorder 系列,了解如何配置它来捕获与 JVM 相关的事件。
另外两个 JFR 加密事件提供了关于初始 JDK 安全属性 (jdk.InitialSecurityProperty
) 和服务提供者方法调用次数 (jdk.SecurityProviderService
) 的见解。JDK 20 版本宣布了新的 jdk.InitialSecurityProperty
,它被移植到 Oracle JDK 17.0.7 和 11.0.20 更新版本中。jdk.SecurityProviderService
事件也从 JDK 20 版本开始可用,但也存在于 JDK 17.0.8、11.0.22 和 8u391 更新版本的代码库中。
本教程旨在向你展示如何使用这些 JFR 安全事件和其他 JDK 工具(keytool、JDK Flight Recorder、JDK Mission Control)来监控 Java 应用程序的安全。
观察 JDK 安全属性
jdk.InitialSecurityProperty
在 JDK 20 中被引入,用于记录通过 java.security.Security
类加载时的初始安全属性的详细信息。如果你设置了 java.security.debug=properties
系统属性,你也可以将初始安全属性打印到标准错误流
java -Djava.security.debug=properties
jdk.InitialSecurityProperty
事件在 default.jfc
和 profile.jfc
JFR 配置文件中默认启用。如果你启用了 jdk.SecurityPropertyModification
事件并保持 jdk.InitialSecurityProperty
启用,你可以使用 JFR 记录来监控所有安全属性的初始设置和任何后续更改。有几种方法可以全面了解 JDK 安全属性的更改,包括服务提供者调用
- 还在 JFR 配置中启用了
jdk.SecurityPropertyModification
和jdk.SecurityProviderService
$JAVA_HOME/bin/jfr configure jdk.SecurityPropertyModification#enabled=true jdk.SecurityProviderService#enabled=true
- 添加
-XX:StartFlightRecording
标志,使用默认设置,同时启用jdk.SecurityPropertyModification
和jdk.SecurityProviderService
java -XX:StartFlightRecording:settings=default,duration=60s,+jdk.SecurityPropertyModification#enabled=true,+jdk.SecurityProviderService#enabled=true
- 通过建立与正在运行的 JVM 的连接并配置事件,从 JDK Mission Control (JMC) 启动 JFR 记录。转到 JDK Mission Control (JMC) 菜单,选择
文件 > 连接... > [选择一个正在运行的 JVM] > 启动飞行记录
并配置每个 JDK 安全事件。
你可以从 JDK Mission Control (JMC) 或命令行启动记录,方法是
- 使用
-XX:StartFlightRecording
运行 java,或者 - 通过
jcmd
工具执行诊断命令jcmd llvmid JFR.start duration=60s filename=recording.jfr
获得 ".jfr" 记录文件后,可以使用 jfr JDK 工具打印事件
$JAVA_HOME/bin/jfr print --events "*Security*" /tmp/recording.jfr
jdk.InitialSecurityProperty {
startTime = 20:15:48.871 (2023-11-29)
key = "keystore.type"
value = "pkcs12"
eventThread = "main" (javaThreadId = 1)
}
....
jdk.SecurityPropertyModification {
startTime = 20:15:48.944 (2023-11-29)
key = "keystore.type"
value = "jks"
eventThread = "main" (javaThreadId = 1)
stackTrace = [
java.security.Security.setProperty(String, String) line: 762
CryptoExample.main() line: 26
]
}
jdk.SecurityProviderService {
startTime = 20:15:50.630 (2023-11-29)
type = "SecureRandom"
algorithm = "NativePRNG"
provider = "SUN"
eventThread = "Attach Listener" (javaThreadId = 37)
stackTrace = [
java.security.Provider.getService(String, String) line: 1298
java.security.SecureRandom.getDefaultPRNG(boolean, byte[]) line: 279
java.security.SecureRandom.<init>() line: 225
java.rmi.server.UID.<init>() line: 112
java.rmi.server.ObjID.<clinit>() line: 88
...
]
}
...
#output trimmed from a total of 98 security related events
通过分析此命令的输出,你可以观察到每个安全属性在其由 jdk.InitialSecurityProperty
捕获的初始值和 jdk.SecurityPropertyModification
事件的更改之间发生的更改。例如,jdk.InitialSecurityProperty
捕获了 keystore.type
,最初设置为 pkcs12
,后来 jdk.SecurityPropertyModification
将其值记录为 jks
。
你还可以通过加载记录文件并导航到 事件浏览器 > Java 开发工具包 > 安全
部分,在 JDK Mission Control 中检查和可视化捕获事件的演变
除了事件的表格显示外,JMC 还通过其视图提供性能分析见解
- 火焰视图呈现由 JFR 事件收集的聚合堆栈跟踪。
- 图形视图呈现具有累积计数的聚合堆栈跟踪。它以图形格式呈现堆栈跟踪,这有助于识别方法路径到其根。
- 热图视图提供堆栈跟踪中特定时间段内发生的事件的可视化表示。
- 依赖关系视图使用分层边捆绑呈现事件的聚合,并有助于可视化包之间的依赖关系。
如果你想知道你的 Java 应用程序正在使用哪个传输层安全 (TLS) 协议版本,这取决于你的 JDK 和应用程序的配置方式。在最新的 JDK 版本中,TLSv1.3
和 TLSv1.2
是默认选项。
确定应用程序使用的确切 TLS 协议版本最直接的方法是收集运行时数据。各种工具和记录器选项可用,下一节将讨论其中一些。
监控 TLS 协议
要捕获 TLS 协议信息,可以将网络协议分析器工具附加到正在运行的 JVM 通信的网络接口,并获取有关所有网络流量的信息。查找“服务器问候”记录和随附的版本值,以确定在特定套接字上使用的 TLS 版本。
但检查 TLS 协议版本更友好的 Java 开发人员方法是检查 JDK 调试日志。如果你将 javax.net.debug
系统属性启用为 ssl:handshake
(即 -Djavax.net.debug=ssl:handshake
),你将获得 TLS 版本协议值。以下是最近 JDK 21 版本中 ServerHello
捕获的示例
"ServerHello": {
"server version" : "TLSv1.2",
"random" : "D36A78A81EA96FA48CAA23D0397E2EDD1FBA783D2B105A8C00D58D7EE74E24A4",
"session id" : "A998EB34379D24829F6E8884D4D2BCC39BACEF6D77C4B9435D104779DC6003CD",
"cipher suite" : "TLS_AES_256_GCM_SHA384(0x1302)",
"compression methods" : "00",
"extensions" : [
"supported_versions (43)": {
"selected version": [TLSv1.3]
},
"key_share (51)": {
"server_share": {
"named group": x25519
"key_exchange": {
0000: 39 EC 40 25 89 1A 75 FF EF 53 0C 36 58 57 1F F8 9.@%..u..S.6XW..
0010: 23 F6 07 D6 9E A8 E4 43 F1 6C 20 F7 AE 5E B1 79 #......C.l ..^.y
}
},
}
]
}
上面的输出显示 TLSv1.3
用于此特定连接("selected version": [TLSv1.3]
)。
从长远来看,检查日志可能是一项繁琐的任务,因此通过 JDK Flight Recorder 捕获基本 TLS 信息的宝贵选择。jdk.TLSHandshake
事件捕获 JDK 执行的每个 TLS 握手的核心信息。要启用它,你可以执行以下操作
- 只需将 JFR 配置文件中的
jdk.TLSHandshake
选项切换为true
<event name="jdk.TLSHandshake">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
- 在终端窗口中运行
jfr configure
命令
$JAVA_HOME/bin/jfr configure jdk.TLSHandshake#enabled=true jdk.TLSHandshake#stackTrace=true
- 添加
-XX:StartFlightRecording
标志,使用默认设置,同时启用jdk.TLSHandshake
java -XX:StartFlightRecording:settings=default,duration=60s,+jdk.TLSHandshake#enabled=true,+jdk.TLSHandshake#stackTrace=true
- 通过建立与正在运行的 JVM 的连接并配置事件,从 JDK Mission Control (JMC) 启动 JFR 记录。转到 JDK Mission Control (JMC) 菜单,选择
文件 > 连接... > [选择一个正在运行的 JVM] > 启动飞行记录
并配置jdk.TLSHandshake
事件。
你可以从 JDK Mission Control (JMC) 或命令行启动记录,方法是
- 使用
-XX:StartFlightRecording
运行 java,或者 - 通过
jcmd
工具执行诊断命令jcmd llvmid JFR.start duration=60s filename=/tmp/TLS.jfr
获得记录后,可以使用 jfr 或在 JDK Mission Control 中分析 TLSHandshake
事件数据。例如,运行以下 jfr print
命令将显示 TLS 握手活动
$JAVA_HOME/bin/jfr print --events "TLS*" /tmp/TLS.jfr
jdk.TLSHandshake {
startTime = 15:28:42.949 (2023-11-30)
peerHost = "google.com"
peerPort = 443
protocolVersion = "TLSv1.3"
cipherSuite = "TLS_AES_128_GCM_SHA256"
certificateId = 587815551
eventThread = "main" (javaThreadId = 1)
stackTrace = [
sun.security.ssl.Finished.recordEvent(SSLSessionImpl) line: 1165
sun.security.ssl.Finished$T13FinishedProducer.onProduceFinished(ClientHandshakeContext, SSLHandshake$HandshakeMessage) line: 767
sun.security.ssl.Finished$T13FinishedProducer.produce(ConnectionContext, SSLHandshake$HandshakeMessage) line: 672
sun.security.ssl.SSLHandshake.produce(ConnectionContext, SSLHandshake$HandshakeMessage) line: 437
sun.security.ssl.Finished$T13FinishedConsumer.onConsumeFinished(ClientHandshakeContext, ByteBuffer) line: 1030
...
]
}
你可以在输出中观察以下事件字段
- 对等主机名
- 对等端口
- 协商的 TLS 协议版本
- 协商的 TLS 密码套件
- 对等客户端的证书 ID
虽然传输层安全 (TLS) 是一种旨在支持计算机网络上安全通信的加密协议,但数字证书可确保数据以私密方式传输,并且不会被修改、丢失或盗窃。下一节讨论如何记录和分析 X.509 证书详细信息。
分析 X.509 证书
X.509 证书广泛部署在 JDK 应用程序中,以支持安全系统中的身份验证和其他功能。X.509 证书具有一组根据 [RFC 1422] 定义的字段,包括
- 版本
- 序列号
- 签名(算法 ID 和参数)
- 颁发者名称
- 有效期
- 主体名称
- 主体公钥(以及关联的算法 ID)
这些字段的值会影响它们所用环境中的底层安全配置。例如,证书的有效期是一个重要的数据,因为过期证书会导致应用程序从特定日期开始出现停机。
从静态分析的角度来看,您可以使用keytool
查询证书。例如,您可以通过运行以下命令查看默认 JDK 信任库 (JDK 9 及更高版本中的$JDK_HOME/lib/security/cacerts
) 中每个证书的详细内容
$JAVA_HOME/bin/keytool -cacerts -list -v
上述场景很简单,但是如何检索实际用于 Java 应用程序的证书的详细信息呢?
通过配置调试系统属性-Djava.security.debug=certpath
和-Djavax.net.debug=all
,在 Java 应用程序的生命周期内打印详细的 X.509 证书信息。
java -Djava.security.debug=certpath -Djavax.net.debug=all
下面您可以看到证书路径验证尝试期间打印的 X.509 证书的示例输出
Trusted CA cert: [
[
Version: V3
Subject: CN=DigiCert Global Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 2048 bits
params: null
modulus: 28559384442792876273280274398620578979733786817784174960112400169719065906301471912340204391164075730987771255281479191858503912379974443363319206013285922932969143082114108995903507302607372164107846395526169928849546930352778612946811335349917424469188917500996253619438384218721744278787164274625243781917237444202229339672234113350935948264576180342492691117960376023738627349150441152487120197333042448834154779966801277094070528166918968412433078879939664053044797116916260095055641583506170045241549105022323819314163625798834513544420165235412105694681616578431019525684868803389424296613694298865514217451303
public exponent: 65537
Validity: [From: Fri Nov 10 00:00:00 UTC 2006,
To: Mon Nov 10 00:00:00 UTC 2031]
Issuer: CN=DigiCert Global Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US
SerialNumber: [ 083be056 904246b1 a1756ac9 5991c74a]
===
Certificate details are also printed during TLS handshake messages. e.g.:
"certificate" : {
"version" : "v3",
"serial number" : "083BE056904246B1A1756AC95991C74A",
"signature algorithm": "SHA1withRSA",
"issuer" : "CN=DigiCert Global Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US",
"not before" : "2006-11-10 24:00:00.000 UTC",
"not after" : "2031-11-10 24:00:00.000 UTC",
"subject" : "CN=DigiCert Global Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US",
"subject public key" : "RSA",
但是,详细的日志记录会降低系统速度,因为需要时间来收集更多信息或显示更多详细信息。您可以使用两个 JDK Flight Recorder 安全事件优雅地捕获有关 X.509 证书的相关数据
jdk.X509Validation
记录在成功的 X.509 验证中协商的 X.509 证书的详细信息。jdk.X509Certificate
捕获有关 JDK 安全库生成的每个 X.509 证书的信息。
您可以选择多种方法来启用这些事件
- 只需在您的 JFR 配置文件中将
jdk.X509Certificate
选项切换为true
<event name="jdk.X509Certificate">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
<event name="jdk.X509Validation">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
</event>
- 在终端窗口中运行
jfr configure
命令
$JAVA_HOME/bin/jfr configure jdk.X509Certificate#enabled=true jdk.X509Validation#enabled=true
- 添加
-XX:StartFlightRecording
标志,使用默认设置,同时启用jdk.X509Certificate
和jdk.X509Validation
java -XX:StartFlightRecording:settings=default,duration=60s,+jdk.X509Certificate#enabled=true,+jdk.X509Validation#enabled=true
- 通过建立与正在运行的 JVM 的连接并配置事件,从 JDK Mission Control (JMC) 启动 JFR 记录。转到 JDK Mission Control (JMC) 菜单,选择
文件 > 连接... > [选择一个正在运行的 JVM] > 启动飞行记录
,并配置jdk.X509Certificate
和jdk.X509Validation
事件。
你可以从 JDK Mission Control (JMC) 或命令行启动记录,方法是
- 使用
-XX:StartFlightRecording
运行 java,或者 - 通过
jcmd
工具执行诊断命令jcmd llvmid JFR.start duration=60s filename=/tmp/myTLSApp.jfr
例如,运行以下命令将显示您记录的有关 X.509 证书的详细信息
$JAVA_HOME/bin/jfr print --events jdk.X509Certificate /tmp/myTLSApp.jfr
jdk.X509Certificate {
startTime = 09:59:25.672 (2022-11-10)
algorithm = "SHA1withRSA"
serialNumber = "18dad19e267de8bb4a2158cdcc6b3b4a"
subject = "CN=VeriSign Class 3 Public Primary Certification Authority - G5, OU="(c) 2006 VeriSign, Inc. - For authorized use only", OU=VeriSign Trust Network, O="VeriSign, Inc.", C=US"
issuer = "CN=VeriSign Class 3 Public Primary Certification Authority - G5, OU="(c) 2006 VeriSign, Inc. - For authorized use only", OU=VeriSign Trust Network, O="VeriSign, Inc.", C=US"
keyType = "RSA"
keyLength = 2048
certificateId = 303010488
validFrom = 00:00:00.000 (2006-11-08)
validUntil = 23:59:59.000 (2036-07-16)
eventThread = "main" (javaThreadId = 1)
stackTrace = [
sun.security.jca.JCAUtil.tryCommitCertEvent(Certificate) line: 126
java.security.cert.CertificateFactory.generateCertificate(InputStream) line: 356
sun.security.pkcs12.PKCS12KeyStore.loadSafeContents(DerInputStream) line: 2428
sun.security.pkcs12.PKCS12KeyStore.engineLoad(InputStream, char[]) line: 2038
sun.security.util.KeyStoreDelegator.engineLoad(InputStream, char[]) line: 228
java.security.KeyStore.load(InputStream, char[]) line: 1500
java.security.KeyStore.getInstance(File, char[], KeyStore$LoadStoreParameter, boolean) line: 1828
java.security.KeyStore.getInstance(File, char[]) line: 1709
sun.security.tools.KeyStoreUtil.getCacertsKeyStore() line: 137
sun.security.tools.keytool.Main.buildTrustedCerts() line: 5072
sun.security.tools.keytool.Main.doCommands(PrintStream) line: 1122
sun.security.tools.keytool.Main.run(String[], PrintStream) line: 419
...
]
}
JDK Flight Recorder 提供丰富的结构化数据,例如堆栈跟踪和带时间戳的值,以及对事件流的 API 支持。在 JDK 16 之前,开发人员可以监控远程主机上的 Java 进程,并通过 JDK Mission Control 控制记录的内容。JDK Mission Control 使用FlightRecorderMXBean
从远程机器获取记录数据并配置事件。
从 JDK 16 开始,您可以使用MBeanServerConnection
以编程方式在网络上传输发生的记录事件。
String host = "com.example";
int port = 7091;
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
JMXServiceURL u = new JMXServiceURL(url);
JMXConnector c = JMXConnectorFactory.connect(u);
MBeanServerConnection connection = c.getMBeanServerConnection();
try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) {
stream.enabled("jdk.X509Certificate").withStackTrace();
stream.onEvent("jdk.X509Certificate", System.out::println),
stream.start();
}
因此,请利用 JDK 版本提供的 JDK 工具和 API,并在运行时分析安全设置和证书数据,以确保应用程序安全!
有用链接
更多学习
上次更新: 2023 年 12 月 1 日