背景
在使用云助手的 RunCommand API,向指定的实例发送命令后,此操作会在以下系统/链路上产生记录:
- ecs:DescribeInvocation API: 能够查询到此次操作的完整信息
- ecs:DescribeInvocationResults API: 能够查询到此次操作的完整信息
- actionTrail:LookupEvents API: 检索详细事件
- 云服务的应用日志:阿里云网关日志、ECS管控链路日志、ECS实例内部日志。
通过以上API或日志,对于此次 RunCommand 记录,其他有权限的人员,可以查询到命令的详细内容。例如:
- 同主账号下的其他RAM用户 (通过使用 API)
- ECS实例的实际使用者 (通过查看ECS内的日志)
- 阿里云的相关工作人员 (通过查看阿里云的管控系统的日志)
某些情况下,命令内容中包含有较有机密的内容,例如:一个重置 Linux 的 root 用户登录密码的命令:
echo "root:TheNew_Password" | chpasswd
用户更希望,没有任何人其他人能看到这里的密码部分:“TheNew_Password”。这种情况下,可以使用云助手的命令加密功能。
使用方法与原理如下:
使用方法
第一步:发送命令:在ECS实例内的云助手Agent进程内,产生一个临时密钥对
命令内容如下:
aliyun-service data-encryption -g [-i key_paid_id] [-t key_pair_timeout] --json
## 参数说明:
-g 创建新的密钥
-i key_paid_id: 可选,为这个临时 KeyPair 指定一个 ID;如果不指定,则会自动生成并打印
-t key_pair_timeout: 默认60秒,该 KeyPair 的存在时间,过期后将自动删除。
--json: 可选,以 JSON 格式输出执行结果
该命令正常将立即执行成功,并且输出结果的示例如下:
{
"id": "t-hy03a65fmrd1zpc",
"createdTimestamp": 1675309078,
"expiredTimestamp": 1675309138,
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1sHHi9moKSyvtBura22F\nxZIgridKzouKTiiDShZssC4DlaMsbFgKPP6j0e/2UcGD34gjxcN8uNjOAgjXGLVY\n/NaaYfS4Es+El1TkJ47DQeDQIddks74ABxW8gF3xqtQC0Oz6k4IKOFvpy4qWPpSm\nRr/QKtfeZfNQlez3YiJHZd8aSxYOQorSQSKu7TzaosXXbFHOlahr2sRBaWZm7G6h\naTgHB/EDtzjam0dUfNoLP96fXHGQf05ZwgVJtzlCRVeyANPRCZbMt1OhZQivUUv3\n/TU8t3apkARtzWbK/YaIUlGnwFfADKDdAQTyKCPSeL9acxnC9+PYP259UCrMNLbF\nUQIDAQAB\n-----END PUBLIC KEY-----\n"
}
返回字段说明:
- id: 对应入参数的 -i key_paid_id
- createdTimestamp:密钥对创建时间
- expiredTimestamp:密钥对过期时间,对应入参 -t key_pair_timeout,默认1分钟
- publicKey: 密钥对的公钥;下一步将使用 publicKey 对 “TheNew_Password” 值进行加密
第二步:使用临时密钥对的公钥,对命令内容中的机密文本,做非对称加密
加密示例代码:
Python 示例
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
def encrypt_data(x509pem, secret):
public_key = RSA.import_key(x509pem)
cipher = PKCS1_OAEP.new(public_key, hashAlgo=SHA256) # 生成一个加密的类
encrypt_text = base64.b64encode(cipher.encrypt(secret.encode())) # 对数据进行加密,密文使用base64编码
return encrypt_text.decode()
def main():
msg = "TheNew_Password"
x509pem = '''-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT9Dd6VWNNmnRWIETkjo
78YWM1L/GjeEAQwLpScfX0rsFeAE3Y6Z/pulkCezOQb8ZFXjHw5eCmKgmEDjf5pb
HEaWRHnhBTOmJIKrqqlIjQzPH71SfXxSQw2OIwY9mgE+Bt0Z91s7ApqDJF1Isq5K
alnaoEKJSiMpeJh5uGjci1brxgYjt7lK13SQr3tYaBAI7QZja4TnXfLAjhEN3/y1
AOygsVQRLsPk4K1sPm7OoEA59WhxDPuLWu8CRRoxqtuVw0gMI33OLDQqsY+bkfa3
6+VXGu+k0dA2QEav9gylgJas8egRH4hjZjaOd7rAG3UrgAXuctZa+i4DbWI9+wQm
oQIDAQAB
-----END PUBLIC KEY-----'''
encrypted_txt = encrypt_data(msg, publickey)
print(encrypted_txt)
main()
NodeJS 加密示例代码:
import {constants, KeyObject} from "crypto";
export async function encrypt(publicKey: string, password: string): Promise<string> {
publicKey = publicKey.replace(/Public Key/g, "PUBLIC KEY")
const pk = crypto.createPublicKey({
key: publicKey,
format: "pem",
type: "spki",
})
const pwd = Buffer.alloc(password.length, password)
const bf: Buffer = crypto.publicEncrypt({
key: pk,
oaepHash: "sha256",
padding: constants.RSA_PKCS1_OAEP_PADDING
}, pwd)
return bf.toString("base64")
}
const pem = "-----BEGIN PUBLIC KEY-----" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT9Dd6VWNNmnRWIETkjo" +
"78YWM1L/GjeEAQwLpScfX0rsFeAE3Y6Z/pulkCezOQb8ZFXjHw5eCmKgmEDjf5pb" +
"HEaWRHnhBTOmJIKrqqlIjQzPH71SfXxSQw2OIwY9mgE+Bt0Z91s7ApqDJF1Isq5K" +
"alnaoEKJSiMpeJh5uGjci1brxgYjt7lK13SQr3tYaBAI7QZja4TnXfLAjhEN3/y1" +
"AOygsVQRLsPk4K1sPm7OoEA59WhxDPuLWu8CRRoxqtuVw0gMI33OLDQqsY+bkfa3" +
"6+VXGu+k0dA2QEav9gylgJas8egRH4hjZjaOd7rAG3UrgAXuctZa+i4DbWI9+wQm" +
"oQIDAQAB" +
"-----END PUBLIC KEY-----"
encrypt(pem, "TheNew_Password").then(result=>{
document.writeln("result = " + result)
}).catch(reason=>{
document.writeln("error = " + reason)
})
JavaScript 加密示例代码
/*
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-----" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT9Dd6VWNNmnRWIETkjo" +
"78YWM1L/GjeEAQwLpScfX0rsFeAE3Y6Z/pulkCezOQb8ZFXjHw5eCmKgmEDjf5pb" +
"HEaWRHnhBTOmJIKrqqlIjQzPH71SfXxSQw2OIwY9mgE+Bt0Z91s7ApqDJF1Isq5K" +
"alnaoEKJSiMpeJh5uGjci1brxgYjt7lK13SQr3tYaBAI7QZja4TnXfLAjhEN3/y1" +
"AOygsVQRLsPk4K1sPm7OoEA59WhxDPuLWu8CRRoxqtuVw0gMI33OLDQqsY+bkfa3" +
"6+VXGu+k0dA2QEav9gylgJas8egRH4hjZjaOd7rAG3UrgAXuctZa+i4DbWI9+wQm" +
"oQIDAQAB" +
"-----END PUBLIC KEY-----"
encrypt(pem, "new-secret-value").then(result=>{
document.writeln("result = " + result)
}).catch(reason=>{
document.writeln("error = " + reason)
})
第三步:改写命令内容:调用云助手服务进程,对加密后的内容进行再解密
#密钥对ID: "t-hy03a65fmrd1zpc"
key_pair_id={{KEY_PAIR_ID}}
#非对称加密后的密码文本值
encrypted_password={{ENCRYPTED_PASSWORD}}
#使用指定的密钥ID,解密被加密的文本,decrypted_text值将是"TheNew_Password"
decrypted_text=$(aliyun-service data-encryption -d -i $key_pair_id -T $encrypted_password)
#立即删除此密钥对
aliyun-service data-encryption --remove-keypair -i $key_pair_id
#使用解密后的密码值,进行重置密码操作
echo "root:$decrypted_text" | chpasswd
使用云助手 RunCommand ,向实例发送以上修改写后的命令,即可在实例内部完成解密与重置密码。
安全效果:
安全保证:
该方案由于密码使用了非对称密钥进行加密,而且私钥仅存在于ECS实例内部的云助手服务进程中,能够保证:
- 任何其他人,都无法通过调用云服务API接口的方式,获得到加密前的真实密码;
- 任何其他人,都无法通过查看管控系统日志的方式,获得到加密前的真实密码;
- 可登录到该ECS实例的人员,无法通过查看ECS实例内部日志的方式,获取到真实密码;
潜在风险:
使用此方案,无法保护密码的情况:
- 可登录到该ECS实例的人员,在临时密码的有效期内,通过重新执行解密命令,可以获取到真实密码。
aliyun-service data-encryption -d -i $key_pair_id -T $encrypted_password
- 提示:完成解密后,立即删除此密钥对,可以<u>部分降低</u>窥探风险。