云助手命令内容加密

更新时间:
复制为 MD 格式

当需要通过云助手在ECS实例上执行包含密码、密钥等敏感信息的命令时,为防止这些信息在API调用记录、操作审计日志或管控日志中以明文形式暴露,可以使用云助手命令内容加密功能。此功能通过在实例内生成临时密钥对,结合本地的加密操作,实现对敏感数据的端到端保护。

工作原理

云助手命令内容加密功能基于非对称加密(RSA-OAEP)实现。其核心工作流如下:

  1. 生成临时密钥对:首先通过云助手命令,在目标ECS实例的云助手Agent进程内存中,生成一个临时的 RSA 密钥对(公钥和私钥)。

  2. 保护私钥:私钥全程仅存于实例的内存中,不会在网络中传输,也不会落盘或记录在任何日志里。该密钥对默认在60秒后会自动销毁。

  3. 本地加密:云助手Agent将公钥返回给客户端或服务器。在客户端或服务器上,使用此公钥加密需要传递的敏感信息(如密码),生成密文。

  4. 传递密文:将包含解密指令和密文的新命令,通过云助手发送到目标实例。

  5. 实例内解密与使用:实例上的云助手 Agent 接收到命令后,使用内存中的私钥解密密文,得到原始的敏感信息,并在后续的脚本逻辑中使用。

此机制确保了从客户端到实例内部执行的整个链路中,敏感信息都不会在阿里云的管控平台(如API返回、ActionTrail日志)上以明文形式出现。

适用范围

  • 云助手Agent版本:不低于2.x.3.398版本。

  • 权限要求:执行该操作的RAM用户或RAM角色必须具备对目标实例的ecs:RunCommand权限。

操作步骤

步骤一:生成临时密钥对

在目标 ECS 实例上安全地创建一个有时效性的密钥对,用于后续的加密操作。

  1. 通过调用RunCommandAPI或在控制台执行命令,向目标实例发送以下指令以生成密钥对。

    aliyun-service data-encryption -g --json

    参数说明

    • -ggenerate 的缩写,表示生成密钥对。

    • --json:(可选)以 JSON格式输出结果,便于程序解析。

    • -i <key_pair_id>:(可选)为临时密钥对指定一个 ID。若不指定,系统将自动生成。

    • -t <timeout_in_seconds>:(可选)指定密钥对的有效期,单位为秒,默认为60

  2. 成功执行后,将获得包含公钥和密钥IDJSON响应。记录命令的输出结果,需要在后续步骤中使用idpublicKey字段的值。

    输出示例

    {
      "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用户登录密码为例,请确保本地环境已安装相应的依赖库(如Pythonpycryptodome)。

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 脚本。该脚本首先在实例内部解密密文,然后使用解密后的明文执行目标操作(例如修改密码),并确保临时密钥在操作结束后被清理。

  1. 编写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函数
  2. 将脚本内容作为命令,通过RunCommandAPI发送到目标实例执行。

生产环境使用建议

  • 自动化风险:临时密钥对过期时间默认为60秒,在复杂的自动化流程中,网络延迟、API 排队、实例负载等因素都可能导致密钥过期,从而使流程失败。如果自动化脚本需要更长的准备时间,可以在生成密钥时使用-t参数适当延长有效期。

  • 代码健壮性:请校验解密操作的返回值 忽略解密失败的情况(如返回null或空字符串),会影响后续业务处理,可能导致安全漏洞(例如误将用户密码重置为空)。

功能限制

  • 安全边界:云助手命令内容加密可防止敏感信息在阿里云管控链路中暴露,无法防御已登录到ECS实例的用户。拥有实例登录权限的用户,在临时密钥的有效期内,可以通过重复执行解密命令来获取明文。

  • Agent开源风险:云助手Agent是开源程序。如果在实例上运行的是被恶意修改过的Agent版本,攻击者可能会截获私钥或解密后的明文内容。请确保从阿里云官方渠道获取和使用云助手Agent。

常见问题

如果执行解密的脚本在中途失败,密钥会泄露吗?

密钥本身(私钥)不会泄露,但它会残留在实例内存中,直到超时后(默认为 60 秒)被自动删除。在此期间,能登录到实例的用户可以尝试使用该密钥进行解密。为避免这种情况,我们强烈建议在您的脚本中使用trap命令来确保无论脚本成功还是失败,清理密钥的命令(aliyun-service data-encryption --remove-keypair)都会被执行。请参考我们操作步骤中提供的最佳实践脚本。