當需要通過雲助手在ECS執行個體上執行包含密碼、密鑰等敏感資訊的命令時,為防止這些資訊在API調用記錄、Action Trail日誌或管控日誌中以明文形式暴露,可以使用雲助手命令內容加密功能。此功能通過在執行個體內產生臨時金鑰組,結合本地的加密操作,實現對敏感性資料的端到端保護。
工作原理
雲助手命令內容加密功能基於非對稱式加密(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. 加密並進行 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 指令碼。該指令碼首先在執行個體內部解密密文,然後使用解密後的明文執行目標操作(例如修改密碼),並確保臨時密鑰在操作結束後被清理。
編寫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或Null 字元串),會影響後續業務處理,可能導致安全性漏洞(例如誤將使用者密碼重設為空白)。
功能限制
安全邊界:雲助手命令內容加密可防止敏感資訊在阿里雲管控鏈路中暴露,無法防禦已登入到ECS執行個體的使用者。擁有執行個體登入許可權的使用者,在臨時密鑰的有效期間內,可以通過重複執行解密命令來擷取明文。
Agent開源風險:雲助手Agent是開來源程式。如果在執行個體上啟動並執行是被惡意修改過的Agent版本,攻擊者可能會截獲私密金鑰或解密後的明文內容。請確保從阿里雲官方渠道擷取和使用雲助手Agent。
常見問題
如果執行解密的指令碼在中途失敗,密鑰會泄露嗎?
密鑰本身(私密金鑰)不會泄露,但它會殘留在執行個體記憶體中,直到逾時後(預設為 60 秒)被自動刪除。在此期間,能登入到執行個體的使用者可以嘗試使用該密鑰進行解密。為避免這種情況,我們強烈建議在您的指令碼中使用trap命令來確保無論指令碼成功還是失敗,清理密鑰的命令(aliyun-service data-encryption --remove-keypair)都會被執行。請參考我們操作步驟中提供的最佳實務指令碼。