一時的な RSA キーペアを使用して、Cloud Assistant コマンド内のパスワードやキーなどの機密データを暗号化し、API 呼び出しやログでのプレーンテキストの露出を防ぎます。
仕組み
Cloud Assistant は、RSA-OAEP 非対称暗号化を使用してコマンドの内容を暗号化します。
-
一時キーペアの生成: Cloud Assistant は、ターゲット Elastic Compute Service (ECS) インスタンス上の Cloud Assistant エージェントのプロセスメモリ内に、一時的な RSA キーペア (公開鍵と秘密鍵) を生成します。
-
秘密鍵の保護: 秘密鍵はメモリ内にのみ存在し、転送、ディスクへの書き込み、ログへの記録は一切行われません。キーペアはデフォルトで 60 秒後に自動的に破棄されます。
-
ローカルでの暗号化: Cloud Assistant エージェントは公開鍵をクライアントまたはサーバーに返し、クライアントまたはサーバーはこれを使用してパスワードなどの機密情報を暗号文に暗号化します。
-
暗号文の転送: 復号命令と暗号文を含む新しいコマンドが、Cloud Assistant を通じてターゲットインスタンスに送信されます。
-
インスタンス内での復号と使用: Cloud Assistant エージェントは、メモリ内の秘密鍵を使用して暗号文を復号し、プレーンテキストをスクリプトロジックに渡します。
機密情報は、クライアントからインスタンスでの実行に至るまで、API レスポンスや ActionTrail ログなどの Alibaba Cloud 管理プラットフォーム上でプレーンテキストとして表示されることはありません。
注意事項
-
Cloud Assistant エージェントのバージョン: 2.x.3.398 以降。
-
権限要件: RAM ユーザーまたは RAM ロールは、ターゲットインスタンスに対する
ecs:RunCommand権限を持つ必要があります。
操作手順
ステップ 1: 一時キーペアの生成
暗号化のために、ターゲット ECS インスタンス上に一時キーペアを作成します。
-
RunCommandAPI を呼び出すか、コンソールでコマンドを実行して、次のコマンドをターゲットインスタンスに送信します。aliyun-service data-encryption -g --jsonパラメーター
-
-g:generateの略。キーペアを生成します。 -
--json: (オプション) プログラムで解析できるよう、結果を JSON フォーマットで出力します。 -
-i <key_pair_id>: (オプション) キーペア ID を指定します。省略した場合、システムが自動的に生成します。 -
-t <timeout_in_seconds>: (オプション) キーペアの有効期間を秒単位で指定します。デフォルト:60。
-
-
JSON レスポンスには、公開鍵とキー ID が含まれます。後続のステップのため、
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: コマンドの内容を暗号化するための公開鍵。
-
ステップ 2: ローカルでの機密データの暗号化
前のステップで取得した公開鍵を使用して、パスワードなどの機密データを暗号文に暗号化します。この例では、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. RSA-OAEP と SHA-256 を使用するように Cipher を設定します。
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("Encryption failed: " + e.getMessage());
return null;
}
}
public static void main(String[] args) {
// ステップ 1 で取得した公開鍵。
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("Encrypted ciphertext (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"Encryption failed: {e}")
return ""
def main():
# ステップ 1 で取得した公開鍵。
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("Encrypted ciphertext (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("failed to parse PEM block")
}
// 2. 公開鍵を解析します。
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse public key: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return "", fmt.Errorf("key is not an RSA public key")
}
// 3. RSA-OAEP (SHA-256) を使用して暗号化します。
// ラベルは通常空です。
secretBytes := []byte(secret)
encryptedBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, secretBytes, nil)
if err != nil {
return "", fmt.Errorf("encryption failed: %v", err)
}
// 4. Base64 エンコードします。
return base64.StdEncoding.EncodeToString(encryptedBytes), nil
}
func main() {
// ステップ 1 で取得した公開鍵。
publicKeyPEM := `-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----`
// 暗号化する機密情報。
secretToEncrypt := "<New_Password>"
encryptedText, err := encryptData(publicKeyPEM, secretToEncrypt)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("Encrypted ciphertext (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(`Encryption failed: ${e.message}`);
return null;
}
}
// ステップ 1 で取得した公開鍵。
const publicKey = `-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----`;
// 暗号化する機密情報。
const secretToEncrypt = "<New_Password>";
const encryptedText = encrypt(publicKey, secretToEncrypt);
if (encryptedText) {
console.log("Encrypted ciphertext (Base64):");
console.log(encryptedText);
}/*
文字列を ArrayBuffer に変換します (参考: 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($"Encryption failed: {e.Message}");
return null;
}
}
public static void Main()
{
// ステップ 1 で取得した公開鍵。
string publicKeyPem = @"-----BEGIN PUBLIC KEY-----
********
-----END PUBLIC KEY-----";
// 暗号化する機密情報。
string secretToEncrypt = "<New_Password>";
string encryptedText = Encrypt(publicKeyPem, secretToEncrypt);
if (!string.IsNullOrEmpty(encryptedText))
{
Console.WriteLine("Encrypted ciphertext (Base64):");
Console.WriteLine(encryptedText);
}
}
}ステップ 3: 復号と実行
インスタンス上で暗号文を復号し、パスワードの変更などのターゲット操作を実行し、完了後に一時キーをクリアするシェルスクリプトを作成します。
-
シェルスクリプトを作成します。以下のセキュリティ強化された例では、Linux インスタンスの
rootユーザーのパスワードをリセットします。シンプルな
echo "root:$decrypted_text" | chpasswdコマンドはセキュリティリスクをもたらします。復号が失敗した場合 (たとえば、キーの有効期限切れにより)、$decrypted_textは空になり、rootパスワードが空に設定されてしまいます。エラーチェックと自動クリーンアップを含む以下のスクリプトを使用してください。#!/bin/bash # ################################################################## # セキュアな実行スクリプト: 復号とパスワードリセット # ################################################################## # --- 設定セクション --- # ステップ 1 で取得したキーペア ID key_pair_id="t-hy03a65fmrd****" # ステップ 2 で取得した暗号化された暗号文 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 が自動的にクリーンアップ関数をトリガーします。 -
RunCommandAPI を使用して、スクリプトの内容をコマンドとしてターゲットインスタンスに送信します。
本番環境への適用
-
自動化のリスク: 一時キーペアのデフォルト TTL は 60 秒です。複雑な自動化ワークフローでは、ネットワークレイテンシー、API のキューイング、インスタンスの負荷により、キーの有効期限切れやプロセスの失敗が発生する可能性があります。キーを生成する際に
-tパラメーターを使用して有効期間を延長してください。 -
コードの堅牢性: 常に復号の戻り値を検証してください。失敗 (null または空の文字列) を無視すると、ビジネスロジックが破綻し、誤ってパスワードを空の値にリセットするなどのセキュリティの脆弱性が発生する可能性があります。
制限事項
-
セキュリティの境界: コマンド内容の暗号化は、Alibaba Cloud 管理プラットフォーム上での機密情報の露出を防ぎます。ECS インスタンスにすでにログインしているユーザーから保護するものではありません。インスタンスへのログイン権限を持つユーザーは、キーの有効期間内に復号コマンドを繰り返し実行してプレーンテキストを取得できます。
-
オープンソースエージェントのリスク: Cloud Assistant エージェントはオープンソースです。悪意を持って変更されたエージェントは、秘密鍵や復号されたプレーンテキストを傍受する可能性があります。常に Alibaba Cloud の公式チャネルから Cloud Assistant エージェントを入手し、使用してください。
よくある質問
復号スクリプトが途中で失敗した場合、キーは漏洩しますか?
秘密鍵は漏洩しませんが、有効期限 (デフォルト: 60 秒) まではメモリ内に残ります。この期間中、インスタンスへのログインアクセス権を持つユーザーは復号を試みることができます。 trap コマンドを使用して、スクリプトの結果に関係なく、キークリーンアップコマンド (aliyun-service data-encryption --remove-keypair) が実行されるようにしてください。 手順で紹介しているベストプラクティスのスクリプトを参照してください。