金甲防护:解密阿里云云助手全链路加密黑科技

背景

在使用云助手的 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>窥探风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值