当需要通过云助手在ECS实例上执行包含密码、密钥等敏感信息的命令时,为防止这些信息在API调用记录、操作审计日志或管控日志中以明文形式暴露,可以使用云助手命令内容加密功能。此功能通过在实例内生成临时密钥对,结合本地的加密操作,实现对敏感数据的端到端保护。
工作原理
云助手命令内容加密功能基于非对称加密(RSA-OAEP)实现。其核心工作流如下:
生成临时密钥对:首先通过云助手命令,在目标ECS实例的云助手Agent进程内存中,生成一个临时的 RSA 密钥对(公钥和私钥)。
保护私钥:私钥全程仅存于实例的内存中,不会在网络中传输,也不会落盘或记录在任何日志里。该密钥对默认在60秒后会自动销毁。
本地加密:云助手Agent将公钥返回给客户端或服务器。在客户端或服务器上,使用此公钥加密需要传递的敏感信息(如密码),生成密文。
传递密文:将包含解密指令和密文的新命令,通过云助手发送到目标实例。
实例内解密与使用:实例上的云助手 Agent 接收到命令后,使用内存中的私钥解密密文,得到原始的敏感信息,并在后续的脚本逻辑中使用。
此机制确保了从客户端到实例内部执行的整个链路中,敏感信息都不会在阿里云的管控平台(如API返回、ActionTrail日志)上以明文形式出现。
适用范围
云助手Agent版本:不低于2.x.3.398版本。
权限要求:执行该操作的RAM用户或RAM角色必须具备对目标实例的
ecs:RunCommand权限。
操作步骤
步骤一:生成临时密钥对
在目标 ECS 实例上安全地创建一个有时效性的密钥对,用于后续的加密操作。
通过调用
RunCommandAPI或在控制台执行命令,向目标实例发送以下指令以生成密钥对。aliyun-service data-encryption -g --json参数说明
-g:generate的缩写,表示生成密钥对。--json:(可选)以 JSON格式输出结果,便于程序解析。-i <key_pair_id>:(可选)为临时密钥对指定一个 ID。若不指定,系统将自动生成。-t <timeout_in_seconds>:(可选)指定密钥对的有效期,单位为秒,默认为60。
成功执行后,将获得包含公钥和密钥ID的JSON响应。记录命令的输出结果,需要在后续步骤中使用
id和publicKey字段的值。输出示例
{ "id": "t-hy03a65fmrd****", "createdTimestamp": 1675309078, "expiredTimestamp": 1675309138, "publicKey": "-----BEGIN PUBLIC KEY-----\n****\n****\n-----END PUBLIC KEY-----\n" }参数说明
id:密钥ID。createdTimestamp:密钥对的创建时间。expiredTimestamp:密钥对的过期时间。publicKey:密钥对的公钥,用于对命令内容进行加密。
步骤二:本地加密敏感数据
使用上一步获取的公钥,在本地环境安全地将敏感数据(如密码)转换为加密后的密文,以重置Linux 的root用户登录密码为例,请确保本地环境已安装相应的依赖库(如Python的pycryptodome)。
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
public class RsaOaepSha256Encryptor {
public static String encrypt(String publicKeyPem, String secret) {
try {
// 1. 处理 PEM 格式公钥,去除头尾和换行
String publicKeyContent = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", ""); // 去除换行和空格
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyContent);
// 2. 生成公钥对象
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 3. 配置 Cipher 使用 RSA-OAEP 和 SHA-256
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
// 明确指定主哈希和 MGF1 哈希均为 SHA-256
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
"SHA-256",
"MGF1",
MGF1ParameterSpec.SHA256,
PSource.PSpecified.DEFAULT
);
cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
// 4. 加密并进行 Base64 编码
byte[] encryptedBytes = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
System.err.println("加密失败: " + e.getMessage());
return null;
}
}
public static void main(String[] args) {
// 步骤一中获取的公钥
String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
"****\n" +
"****\n" +
"-----END PUBLIC KEY-----";
// 需要加密的敏感信息
String secretToEncrypt = "<New_Password>";
String encryptedText = encrypt(publicKeyPem, secretToEncrypt);
if (encryptedText != null) {
System.out.println("加密后的密文 (Base64):");
System.out.println(encryptedText);
}
}
}import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
def encrypt_data(public_key_pem: str, secret: str) -> str:
"""使用 RSA-OAEP 和 SHA-256 加密数据"""
try:
# 导入公钥
public_key = RSA.import_key(public_key_pem)
# 创建加密器
cipher = PKCS1_OAEP.new(public_key, hashAlgo=SHA256)
# 加密数据并进行 Base64 编码
encrypted_bytes = cipher.encrypt(secret.encode('utf-8'))
return base64.b64encode(encrypted_bytes).decode('utf-8')
except Exception as e:
print(f"加密失败: {e}")
return ""
def main():
# 步骤一中获取的公钥
public_key_pem = """-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----"""
# 需要加密的敏感信息
secret_to_encrypt = "<New_Password>"
encrypted_text = encrypt_data(public_key_pem, secret_to_encrypt)
if encrypted_text:
print("加密后的密文 (Base64):")
print(encrypted_text)
if __name__ == "__main__":
main()package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"os"
)
func encryptData(publicKeyPEM string, secret string) (string, error) {
// 1. 解析 PEM 块
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return "", fmt.Errorf("无法解析 PEM 块")
}
// 2. 解析公钥
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("解析公钥失败: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return "", fmt.Errorf("密钥不是 RSA 公钥")
}
// 3. 使用 RSA-OAEP (SHA-256) 加密
// label 通常为空
secretBytes := []byte(secret)
encryptedBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, secretBytes, nil)
if err != nil {
return "", fmt.Errorf("加密失败: %v", err)
}
// 4. Base64 编码
return base64.StdEncoding.EncodeToString(encryptedBytes), nil
}
func main() {
// 步骤一中获取的公钥
publicKeyPEM := `-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----`
// 需要加密的敏感信息
secretToEncrypt := "<New_Password>"
encryptedText, err := encryptData(publicKeyPEM, secretToEncrypt)
if err != nil {
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
os.Exit(1)
}
fmt.Println("加密后的密文 (Base64):")
fmt.Println(encryptedText)
}const crypto = require('crypto');
function encrypt(publicKey, secret) {
try {
const secretBuffer = Buffer.from(secret, 'utf8');
const encryptedBuffer = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
secretBuffer
);
return encryptedBuffer.toString('base64');
} catch (e) {
console.error(`加密失败: ${e.message}`);
return null;
}
}
// 步骤一中获取的公钥
const publicKey = `-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----`;
// 需要加密的敏感信息
const secretToEncrypt = "<New_Password>";
const encryptedText = encrypt(publicKey, secretToEncrypt);
if (encryptedText) {
console.log("加密后的密文 (Base64):");
console.log(encryptedText);
}/*
Convert a string into an ArrayBuffer
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
*/
function base64ToArrayBuffer(base64Text: string): ArrayBuffer {
var binary = window.atob(base64Text);
var len = binary.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer)
const binary = String.fromCharCode(...uint8Array)
return window.btoa(binary)
}
async function encrypt(publicKey: string, password: string): Promise<string> {
const content = publicKey.replace(/-+(BEGIN|END) PUBLIC KEY-+/ig, '');
const pemText = content.replace(/[\r\n]+/g, '');
const binaryDer = base64ToArrayBuffer(pemText);
const crypto = window.crypto
|| (window as any).webkitCrypto
|| (window as any).mozCrypto
|| (window as any).oCrypto
|| (window as any).msCrypto;
const cryptoKey = await crypto!.subtle.importKey(
'spki',
binaryDer,
{name: 'RSA-OAEP', hash: 'SHA-256'},
true,
['encrypt']
)
const uint8Pwd: Uint8Array = Buffer.from(password);
const encrypted: ArrayBuffer = await crypto!.subtle.encrypt(
{name: 'RSA-OAEP'},
cryptoKey,
uint8Pwd
);
return arrayBufferToBase64(encrypted);
}
const pem = "-----BEGIN PUBLIC KEY-----" +
"********" +
"-----END PUBLIC KEY-----"
encrypt(pem, "new-secret-value").then(result=>{
document.writeln("result = " + result)
}).catch(reason=>{
document.writeln("error = " + reason)
})using System;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public static string Encrypt(string publicKeyPem, string secret)
{
try
{
using (var rsa = RSA.Create())
{
// 1. 导入 PEM 格式公钥 (适用于 .NET Core 3.0+, .NET 5+)
rsa.ImportFromPem(publicKeyPem);
// 2. 将数据转换为字节数组
byte[] dataToEncrypt = Encoding.UTF8.GetBytes(secret);
// 3. 使用 RSA-OAEP (SHA-256) 加密
byte[] encryptedData = rsa.Encrypt(dataToEncrypt, RSAEncryptionPadding.OaepSHA256);
// 4. 返回 Base64 字符串
return Convert.ToBase64String(encryptedData);
}
}
catch (Exception e)
{
Console.Error.WriteLine($"加密失败: {e.Message}");
return null;
}
}
public static void Main()
{
// 步骤一中获取的公钥
string publicKeyPem = @"-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----";
// 需要加密的敏感信息
string secretToEncrypt = "<New_Password>";
string encryptedText = Encrypt(publicKeyPem, secretToEncrypt);
if (!string.IsNullOrEmpty(encryptedText))
{
Console.WriteLine("加密后的密文 (Base64):");
Console.WriteLine(encryptedText);
}
}
}步骤三:执行解密与后续操作
构造并执行一个安全的 Shell 脚本。该脚本首先在实例内部解密密文,然后使用解密后的明文执行目标操作(例如修改密码),并确保临时密钥在操作结束后被清理。
编写Shell脚本。以下是一个经过安全加固的示例,用于重置
root用户的密码。简单的
echo "root:$decrypted_text" | chpasswd命令存在安全风险。如果解密失败(例如密钥过期)$decrypted_text将为空,这将导致root密码被设置为空。请务必使用下面包含错误检查和自动清理机制的脚本。#!/bin/bash # ################################################################## # 安全执行脚本:解密并重置密码 # ################################################################## # --- 配置区 --- # 从步骤一获取的密钥对 ID key_pair_id="t-hy03a65fmrd****" # 从步骤二生成的加密后密文 encrypted_password="YOUR_ENCRYPTED_BASE64_STRING_HERE" # --- 核心逻辑区 --- # 定义清理函数,用于在脚本退出时自动删除密钥对 cleanup() { echo "正在执行清理:删除临时密钥对 $key_pair_id ..." aliyun-service data-encryption --remove-keypair -i "$key_pair_id" > /dev/null 2>&1 } # 注册清理函数,确保脚本在任何情况下(正常退出、出错、被中断)都会执行清理 trap cleanup EXIT # 执行解密操作 decrypted_text=$(aliyun-service data-encryption -d -i "$key_pair_id" -T "$encrypted_password") # 关键安全检查:确保解密成功且内容不为空 if [ -z "$decrypted_text" ]; then echo "错误:解密失败。原因可能是密钥已过期、密钥ID错误或密文无效。操作已中止。" >&2 exit 1 fi # 使用解密后的明文执行密码重置操作 echo "root:$decrypted_text" | chpasswd if [ $? -eq 0 ]; then echo "密码已成功重置。" else echo "错误:密码重置命令执行失败。" >&2 exit 1 fi # 脚本将在此处正常退出,并自动触发trap定义的cleanup函数将脚本内容作为命令,通过
RunCommandAPI发送到目标实例执行。
生产环境使用建议
自动化风险:临时密钥对过期时间默认为60秒,在复杂的自动化流程中,网络延迟、API 排队、实例负载等因素都可能导致密钥过期,从而使流程失败。如果自动化脚本需要更长的准备时间,可以在生成密钥时使用
-t参数适当延长有效期。代码健壮性:请校验解密操作的返回值。 忽略解密失败的情况(如返回null或空字符串),会影响后续业务处理,可能导致安全漏洞(例如误将用户密码重置为空)。
功能限制
安全边界:云助手命令内容加密可防止敏感信息在阿里云管控链路中暴露,无法防御已登录到ECS实例的用户。拥有实例登录权限的用户,在临时密钥的有效期内,可以通过重复执行解密命令来获取明文。
Agent开源风险:云助手Agent是开源程序。如果在实例上运行的是被恶意修改过的Agent版本,攻击者可能会截获私钥或解密后的明文内容。请确保从阿里云官方渠道获取和使用云助手Agent。
常见问题
如果执行解密的脚本在中途失败,密钥会泄露吗?
密钥本身(私钥)不会泄露,但它会残留在实例内存中,直到超时后(默认为 60 秒)被自动删除。在此期间,能登录到实例的用户可以尝试使用该密钥进行解密。为避免这种情况,我们强烈建议在您的脚本中使用trap命令来确保无论脚本成功还是失败,清理密钥的命令(aliyun-service data-encryption --remove-keypair)都会被执行。请参考我们操作步骤中提供的最佳实践脚本。