全部產品
Search
文件中心

Elastic Compute Service:雲助手命令內容加密

更新時間:Apr 29, 2026

當需要通過雲助手在ECS執行個體上執行包含密碼、密鑰等敏感資訊的命令時,為防止這些資訊在API調用記錄、Action Trail日誌或管控日誌中以明文形式暴露,可以使用雲助手命令內容加密功能。此功能通過在執行個體內產生臨時金鑰組,結合本地的加密操作,實現對敏感性資料的端到端保護。

工作原理

雲助手命令內容加密功能基於非對稱式加密(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. 成功執行後,將獲得包含公開金鑰和密鑰ID的JSON響應。記錄命令的輸出結果,需要在後續步驟中使用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使用者登入密碼為例,請確保本地環境已安裝相應的依賴庫(如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. 加密並進行 Base 64 編碼
            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)
        # 加密資料並進行 Base 64 編碼
        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. Base 64 編碼
	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或Null 字元串),會影響後續業務處理,可能導致安全性漏洞(例如誤將使用者密碼重設為空白)。

功能限制

  • 安全邊界:雲助手命令內容加密可防止敏感資訊在阿里雲管控鏈路中暴露,無法防禦已登入到ECS執行個體的使用者。擁有執行個體登入許可權的使用者,在臨時密鑰的有效期間內,可以通過重複執行解密命令來擷取明文。

  • Agent開源風險:雲助手Agent是開來源程式。如果在執行個體上啟動並執行是被惡意修改過的Agent版本,攻擊者可能會截獲私密金鑰或解密後的明文內容。請確保從阿里雲官方渠道擷取和使用雲助手Agent。

常見問題

如果執行解密的指令碼在中途失敗,密鑰會泄露嗎?

密鑰本身(私密金鑰)不會泄露,但它會殘留在執行個體記憶體中,直到逾時後(預設為 60 秒)被自動刪除。在此期間,能登入到執行個體的使用者可以嘗試使用該密鑰進行解密。為避免這種情況,我們強烈建議在您的指令碼中使用trap命令來確保無論指令碼成功還是失敗,清理密鑰的命令(aliyun-service data-encryption --remove-keypair)都會被執行。請參考我們操作步驟中提供的最佳實務指令碼。