apache / apache/cloudstack

Keystore Password Exposure in KVM Agent Security Setup

未關閉
#12,029 1 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視
component:logging
主要語言
Java
星號
3.1k
分支
1.4k
平均合併
7 天 14 小時
30 天內合併 PR
31

描述

## Summary

A critical security vulnerability has been identified in the Apache CloudStack KVM agent security setup process where **randomly generated keystore passwords are logged in plaintext**. During the `LibvirtServerDiscoverer.setupAgentSecurity()` method execution, sensitive keystore passwords are exposed through SSH command logging in `SSHCmdHelper.sshExecuteCmdOneShot()`, compromising the security of agent-to-management-server communications.

## Vulnerability Details

**Component:** `com.cloud.hypervisor.kvm.discoverer.LibvirtServerDiscoverer`
- `setupAgentSecurity()`
- `SSHCmdHelper.sshExecuteCmdWithResult()`
- `SSHCmdHelper.sshExecuteCmdOneShot()`

**Vulnerability Type:** Sensitive Information Disclosure / Password Exposure in Debug Logs (CWE-532, CWE-532)

## Technical Description

### Vulnerability Flow

#### 1. Password Generation and Password Exposure

The same password is used again in the certificate import command:

##### Expose source 1
```java
// void com.cloud.hypervisor.kvm.discoverer.LibvirtServerDiscoverer.setupAgentSecurity(Connection sshConnection, String agentIp, String agentHostname)
final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
String.format("sudo /usr/share/cloudstack-common/scripts/util/%s " +
"/etc/cloudstack/agent/agent.properties %s " + // ← keystorePassword exposed again
"/etc/cloudstack/agent/%s %s " +
"/etc/cloudstack/agent/%s \"%s\" " +
"/etc/cloudstack/agent/%s \"%s\" " +
"/etc/cloudstack/agent/%s \"%s\"",
KeyStoreUtils.KS_IMPORT_SCRIPT,
keystorePassword, // Password in command
KeyStoreUtils.KS_FILENAME,
KeyStoreUtils.SSH_MODE,
KeyStoreUtils.CERT_FILENAME,
certificateCommand.getEncodedCertificate(),
KeyStoreUtils.CACERT_FILENAME,
certificateCommand.getEncodedCaCertificates(),
KeyStoreUtils.PKEY_FILENAME,
certificateCommand.getEncodedPrivateKey()));
```
##### Expose source 2

```java
// Answer com.cloud.baremetal.networkservice.BaremetalPingPxeResource.execute(PrepareCreateTemplateCommand cmd)
protected Answer execute(PrepareCreateTemplateCommand cmd) {
...

String script =
String.format("python /usr/bin/prepare_tftp_bootfile.py backup %1$s %2$s %3$s %4$s %5$s %6$s %7$s %8$s %9$s %10$s %11$s", _tftpDir, cmd.getMac(),
_storageServer, _share, _dir, cmd.getTemplate(), _cifsUserName, _cifsPassword, cmd.getIp(), cmd.getNetMask(), cmd.getGateWay()); // Password in command
if (!SSHCmdHelper.sshExecuteCmd(sshConnection, script)) {
return new Answer(cmd, false, "prepare for creating template failed, command:" + script);
}
...
}
```

##### Expose source 3

```java
// PreparePxeServerAnswer com.cloud.baremetal.networkservice.BaremetalPingPxeResource.execute(PreparePxeServerCommand cmd)
protected PreparePxeServerAnswer execute(PreparePxeServerCommand cmd) {

String script =
String.format("python /usr/bin/prepare_tftp_bootfile.py restore %1$s %2$s %3$s %4$s %5$s %6$s %7$s %8$s %9$s %10$s %11$s", _tftpDir, cmd.getMac(),
_storageServer, _share, _dir, cmd.getTemplate(), _cifsUserName, _cifsPassword, cmd.getIp(), cmd.getNetMask(), cmd.getGateWay()); // Password in command
if (!SSHCmdHelper.sshExecuteCmd(sshConnection, script)) {
return new PreparePxeServerAnswer(cmd, "prepare PING at " + _ip + " failed, command:" + script);
}
```

#### 2. Debug Logging Exposure

Both commands are logged in `SSHCmdHelper.sshExecuteCmdOneShot()`:

```java
public static SSHCmdResult sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException {
LOGGER.debug("Executing cmd: " + cmd.split(KeyStoreUtils.KS_FILENAME)[0]); // ← Password logged here
Session sshSession = null;
try {
sshSession = sshConnection.openSession();
Thread.sleep(1000);

if (sshSession == null) {
throw new SshException("Cannot open ssh session");
}
// ... execution continues
}
...
if (!StringUtils.isAllEmpty(result.getStdOut(), result.getStdErr())) {
LOGGER.debug("SSH command: " + cmd.split(KeyStoreUtils.KS_FILENAME)[0] + "\nSSH command output:" + result.getStdOut().split("-----BEGIN")[0] + "\n" + result.getStdErr()); // ← Password logged here
}
}
```

### Incomplete Sanitization Attempt

The code attempts to sanitize by splitting on `KS_FILENAME` (likely `"agent.jks"`):

```java
cmd.split(KeyStoreUtils.KS_FILENAME)[0]
```

**However, this approach is fundamentally flawed:**

The password appears **before** `agent.jks` in the command string, so it's included in the first split segment and **fully exposed in logs**.

#### For example: Expose source 1 Command:
```bash
# Original command:
sudo .../keystore-import.sh /etc/.../agent.properties Xy9$mK2pL#4nQ7vR /etc/.../agent.jks ssh ...

# After split on "agent.jks" and taking [0]:
sudo .../keystore-import.sh /etc/.../agent.properties Xy9$mK2pL#4nQ7vR /etc/.../
# ❌ Password is BEFORE the split point, so it's INCLUDED in the logged output
```

Again, the password appears **before** the split point and is **fully exposed**.

## Root Cause Analysis

1. **Flawed Sanitization Logic**: The `split(KeyStoreUtils.KS_FILENAME)[0]` approach assumes the password appears *after* the keystore filename, but the actual command structure places it *before*

2. **Position-Dependent Vulnerability**: The sanitization logic is position-dependent but uses the wrong position marker

3. **No Pattern-Based Redaction**: No regex or pattern matching to identify and mask password parameters

4. **Debug Logging in Production**: Debug-level logging may be enabled in production environments, exposing sensitive data

貢獻指南

開啟貢獻指南

研究方向

從 LibvirtServerDiscoverer.setupAgentSecurity() 和 SSHCmdHelper.sshExecuteCmdWithResult()/sshExecuteCmdOneShot() 開始,接著追蹤傳遞給 SSH helper 的命令字串。檢視 bare-metal 命令建構範例,將其作為額外的暴露途徑。完成的標準是敏感密碼不再出現在偵錯命令或輸出記錄中,包括 keystore 設定流程。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
java
領域
infrastructure, security
Issue 類型
缺陷
難度
4/5
預估耗時
3-5 天
活躍度
停滯
描述清晰度
基本清楚
新手友好度
35/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。