IAM Roles Anywhere: dùng AWS làm hub danh tính cho workload on-prem truy cập AWS, Google Cloud và Azure
Cấp cho những máy không có danh tính cloud — VM on-prem, thiết bị edge, CI tự host — chứng chỉ X.509 ngắn hạn từ Vault PKI, biến chúng thành AWS role session bằng IAM Roles Anywhere, rồi federation tiếp sang Google Cloud và Azure mà không cần một static key nào.

Mọi bài trong loạt này cho tới giờ đều giả định workload đã có sẵn danh tính cloud: pod trên EKS, GKE hay AKS, một EC2 instance, một Cloud Run service. Rất nhiều workload thực tế không có. Đó là cluster VMware trong data centre, máy chạy batch đặt dưới gầm bàn ai đó, thiết bị edge trong nhà máy, hay CI runner tự host. Đây chính là những máy cuối cùng chứa cả AWS_ACCESS_KEY_ID, key.json của GCP lẫn client secret của Azure, chung một file .env.
IAM Roles Anywhere cho những máy đó một danh tính AWS, dựa trên thứ chúng có thể có: chứng chỉ X.509 do CA của chính bạn cấp. Kết hợp với những gì các bài trước đã trình bày, là AWS provider của Google Cloud và outbound identity federation của AWS sang Azure, thì AWS trở thành hub danh tính. Một chứng chỉ trên máy, một AWS role session, và từ đó có quyền truy cập ngắn hạn vào AWS, Google Cloud lẫn Azure.
Dựa trên các bài: AWS → Google Cloud, AWS → Azure, và ý tưởng control plane đa account.
IAM Roles Anywhere hoạt động thế nào
Có ba loại tài nguyên:
- Trust anchor: CA mà AWS tin. Có thể là bundle chứng chỉ CA của bạn (ở đây là intermediate CA của Vault PKI), hoặc AWS Private CA (xem phương án cert-manager cho Kubernetes).
- Profile: những role nào được assume qua nó, thời hạn session tối đa (15 phút tới 12 giờ), và session policy tuỳ chọn.
- Role: một IAM role bình thường, có trust policy chỉ định service principal
rolesanywhere.amazonaws.com.
Trên máy chạy aws_signing_helper, công cụ credential helper mã nguồn mở của AWS. Nó dùng private key của chứng chỉ để ký request CreateSession. Key có thể nằm trong file, TPM, HSM PKCS#11 hay keychain của hệ điều hành. AWS xác minh chuỗi chứng chỉ với trust anchor, kiểm tra CRL nếu đã import, rồi đánh giá trust policy của role. Nhiều trường trong chứng chỉ trở thành session principal tag để viết điều kiện:
| Lấy từ chứng chỉ | Trở thành |
|---|---|
Các RDN của Subject (CN, OU…) |
aws:PrincipalTag/x509Subject/CN… |
| Các RDN của Issuer | aws:PrincipalTag/x509Issuer/CN… |
| CN của Subject | source identity (CN=orders-reporter) |
| Serial number (hex) | session name của role |
Dòng cuối rất quan trọng cho audit: mọi sự kiện CloudTrail đều mang serial của chứng chỉ, nên luôn truy ngược được một API call về đúng chứng chỉ đã dùng.
Bước 1: một CA cho workload (Vault PKI)
Dùng một intermediate CA riêng cho workload, không bao giờ dùng root CA của công ty, và cấp chứng chỉ end-entity ngắn hạn. AWS yêu cầu chứng chỉ X.509v3, CA:false, key usage có Digital Signature, và thuật toán ký từ SHA-256 trở lên.
vault secrets enable -path=pki_workloads pki
vault secrets tune -max-lease-ttl=43800h pki_workloads
# intermediate CA, ký bởi root offline của bạn
vault write -field=csr pki_workloads/intermediate/generate/internal \
common_name="Workloads Intermediate CA" > workloads-int.csr
# … ký workloads-int.csr bằng root → workloads-int.pem
vault write pki_workloads/intermediate/set-signed certificate=@workloads-int.pem
# mỗi workload một role: CN cố định, chứng chỉ 24 giờ
vault write pki_workloads/roles/orders-reporter \
allowed_domains="orders-reporter" allow_bare_domains=true \
key_type=ec key_bits=256 ttl=24h max_ttl=24h \
key_usage="DigitalSignature" ext_key_usage="ClientAuth" server_flag=false
Trên mỗi máy, Vault Agent xác thực (ví dụ bằng AppRole hoặc TLS), xin chứng chỉ từ pki_workloads/issue/orders-reporter, ghi ra cert.pem và key.pem, và gia hạn trước khi hết hạn. Credential helper tự nhận ra file đã được cập nhật và dùng chứng chỉ mới ở lần làm mới tiếp theo, nên xoay vòng chứng chỉ không cần restart. Đây là cách đơn giản nhất, nhưng nó để private key nằm trên đĩa. Muốn giữ key hoàn toàn khỏi đĩa, xem biến thể chỉ trong bộ nhớ bên dưới.
Bước 2: trust anchor, profile và role (AWS identity account)
Đặt Roles Anywhere trong một identity account riêng (999999999999). Account này sẽ là hub.
resource "aws_rolesanywhere_trust_anchor" "vault_workloads" {
name = "vault-workloads"
enabled = true
source {
source_type = "CERTIFICATE_BUNDLE"
source_data {
x509_certificate_data = file("workloads-int.pem")
}
}
}
resource "aws_rolesanywhere_profile" "onprem" {
name = "onprem-workloads"
enabled = true
role_arns = [aws_iam_role.onprem_orders_reporter.arn]
duration_seconds = 3600
}
Trust policy của role phải ghim trust anchor (aws:SourceArn, chống tấn công confused deputy) và danh tính của workload trong chứng chỉ:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "rolesanywhere.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession", "sts:SetSourceIdentity"],
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:rolesanywhere:eu-west-1:999999999999:trust-anchor/<TA_ID>"
},
"StringEquals": {
"aws:PrincipalTag/x509Subject/CN": "orders-reporter"
}
}
}]
}
Thiếu điều kiện x509Subject/CN thì mọi chứng chỉ do CA đó cấp đều assume được role. Trust anchor chỉ chứng minh chứng chỉ đến từ CA của bạn, chứ không cho biết workload nào đang gọi.
Bước 3: credentials trên máy
Chạy helper như một service lâu dài ở chế độ serve. Nó mở một endpoint tương thích IMDSv2 tại 127.0.0.1:9911, và tự làm mới credentials 5 phút trước khi hết hạn:
# /etc/systemd/system/aws-roles-anywhere.service
[Service]
ExecStart=/usr/local/bin/aws_signing_helper serve \
--certificate /etc/vault-agent/orders-reporter/cert.pem \
--private-key /etc/vault-agent/orders-reporter/key.pem \
--trust-anchor-arn arn:aws:rolesanywhere:eu-west-1:999999999999:trust-anchor/<TA_ID> \
--profile-arn arn:aws:rolesanywhere:eu-west-1:999999999999:profile/<PROFILE_ID> \
--role-arn arn:aws:iam::999999999999:role/onprem-orders-reporter \
--hop-limit 1
Restart=always
# môi trường của ứng dụng — AWS SDK nào cũng không phải sửa code
AWS_EC2_METADATA_SERVICE_ENDPOINT=http://127.0.0.1:9911
AWS_REGION=eu-west-1
Mọi tiến trình trên máy truy cập được 127.0.0.1:9911 đều lấy được credentials này. Vì vậy hãy dành riêng máy hoặc VM cho một workload. Nếu không làm vậy được, dùng credential-process trong ~/.aws/config, vì cách này giữ credentials bên trong tiến trình gọi nó.
Bước 4: truy cập các AWS account khác
Từ role ở hub, dùng role chaining thông thường để vào các workload account. Role đích chỉ tin arn:aws:iam::999999999999:role/onprem-orders-reporter, không tin ai khác. Role chaining giới hạn session ở một giờ, và SDK tự làm mới mà ứng dụng không cần biết.
# ~/.aws/config
[profile prod]
role_arn = arn:aws:iam::444444444444:role/orders-reporter
credential_source = Ec2InstanceMetadata # = endpoint Roles Anywhere ở trên
Tốt hơn: service tự lấy chứng chỉ từ Vault, chỉ trong bộ nhớ
Bước 1 tới 3 để cert.pem và key.pem nằm trên đĩa, còn biến thể Kubernetes bên dưới để chúng trong Secret. Cả hai đều chạy được, nhưng private key nằm yên trên đĩa chính là thứ các công cụ quét bảo mật cloud như Wiz săn tìm: chúng đọc đĩa và snapshot của VM, đọc Kubernetes Secret, và một key xuất ra được, đổi được lấy AWS role, lần nào cũng thành một finding. Finding đó không oan. Một snapshot đĩa, một bản backup hay một volume bị lấy cắp đều mang theo một key còn dùng được.
Cách khắc phục là để chính service backend nói chuyện với Vault qua thư viện auth: nó đăng nhập bằng danh tính workload của mình, sinh cặp key trong bộ nhớ, chỉ gửi cho Vault một CSR, rồi ký request Roles Anywhere bằng key nằm trong bộ nhớ đó. Không có gì được ghi ra đĩa hay vào etcd, nên công cụ quét không còn gì để tìm, trên Kubernetes lẫn trên VM.
Vault: đăng nhập bằng Kubernetes, policy chỉ được ký, chứng chỉ ngắn hạn
# PKI role for this workload: fixed CN, client auth only, 1 h certificates, nothing stored
vault write pki_workloads/roles/orders-reporter \
allowed_domains="orders-reporter" allow_bare_domains=true \
allow_subdomains=false enforce_hostnames=false \
key_type=ec key_bits=256 ttl=1h max_ttl=1h \
key_usage="DigitalSignature" ext_key_usage="ClientAuth" \
server_flag=false client_flag=true \
no_store=true
# the workload may only ask Vault to *sign* (not issue) for its own role
vault policy write orders-reporter-pki - <<'EOF'
path "pki_workloads/sign/orders-reporter" {
capabilities = ["update"]
}
EOF
# Kubernetes auth: Vault runs outside the cluster, so give it the cluster CA
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://k8s-api.internal:6443" \
kubernetes_ca_cert=@k8s-ca.crt disable_local_ca_jwt=true
vault write auth/kubernetes/role/orders-reporter \
bound_service_account_names=orders-reporter \
bound_service_account_namespaces=orders \
audience=vault \
token_policies=orders-reporter-pki \
token_ttl=10m token_max_ttl=10m
Ba chi tiết quan trọng:
sign, không phảiissue.pki/issuesinh key ngay trong Vault và trả nó về trong response.pki/signnhận CSR, nên key không bao giờ rời process. Trong bài thử bên dưới, response của Vault hoàn toàn không cóprivate_key.server_flag=false. Role PKI của Vault mặc định thêm server auth, vàext_key_usagechỉ cộng thêm vào đó. Hãy tắt nó để chứng chỉ chỉ dùng cho client auth.no_store=true. Mỗi lần process khởi động và mỗi lần gia hạn đều tạo một chứng chỉ. Đừng bắt Vault lưu hết chúng.
Khi không đặt token_reviewer_jwt, Vault xác minh mỗi lần đăng nhập bằng cách gọi API TokenReview của Kubernetes với chính token của client, nên ServiceAccount của workload cần ClusterRole system:auth-delegator. Hoặc cấu hình một reviewer token chung cho cả cluster.
Pod: một token ngắn hạn cho Vault, không gì khác
spec:
serviceAccountName: orders-reporter
automountServiceAccountToken: false # no default API token in the pod
containers:
- name: app
image: registry.example.com/orders-reporter:1.5.0
env:
- { name: VAULT_ADDR, value: "https://vault.internal:8200" }
volumeMounts:
- { name: vault-token, mountPath: /var/run/secrets/vault, readOnly: true }
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
audience: vault # matches the Vault role's audience
expirationSeconds: 600
path: token
Kubelet tự xoay vòng token này. Nó chỉ dùng được để đăng nhập Vault, và hết hạn sau mười phút.
Service: đăng nhập Vault, key trong bộ nhớ, Roles Anywhere, AWS SDK
Credential helper của AWS cũng là một thư viện Go. Hàm GenerateCredentials của nó nhận bất kỳ Signer nào, nên service có thể đưa vào một key chỉ nằm trong bộ nhớ. Package dưới đây làm trọn luồng và gắn vào AWS SDK như một credentials provider:
// Package rawpki gets AWS credentials through IAM Roles Anywhere with a
// certificate that only ever exists in memory.
package rawpki
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"sync"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
ra "github.com/aws/rolesanywhere-credential-helper/aws_signing_helper"
vault "github.com/hashicorp/vault/api"
k8sauth "github.com/hashicorp/vault/api/auth/kubernetes"
)
type Config struct {
VaultRole string // Vault kubernetes auth role
TokenPath string // projected ServiceAccount token, audience "vault"
PKIPath string // "pki_workloads/sign/orders-reporter"
CommonName string // must match the PKI role and the AWS trust policy
TrustAnchorArn string
ProfileArn string
RoleArn string
}
// memSigner implements the credential helper's Signer over an in-memory key.
type memSigner struct {
key *ecdsa.PrivateKey
cert *x509.Certificate
}
func (s *memSigner) Public() crypto.PublicKey { return &s.key.PublicKey }
func (s *memSigner) Sign(r io.Reader, d []byte, o crypto.SignerOpts) ([]byte, error) {
return s.key.Sign(r, d, o)
}
func (s *memSigner) Certificate() (*x509.Certificate, error) { return s.cert, nil }
func (s *memSigner) CertificateChain() ([]*x509.Certificate, error) { return nil, nil }
func (s *memSigner) Close() {}
type Provider struct {
cfg Config
vault *vault.Client
mu sync.Mutex
signer *memSigner
}
func New(cfg Config) (*Provider, error) {
vc, err := vault.NewClient(vault.DefaultConfig()) // VAULT_ADDR, VAULT_CACERT from env
if err != nil {
return nil, err
}
return &Provider{cfg: cfg, vault: vc}, nil
}
// issue logs in to Vault and gets a certificate for a key generated here.
func (p *Provider) issue(ctx context.Context) (*memSigner, error) {
auth, err := k8sauth.NewKubernetesAuth(p.cfg.VaultRole, k8sauth.WithServiceAccountTokenPath(p.cfg.TokenPath))
if err != nil {
return nil, err
}
if _, err := p.vault.Auth().Login(ctx, auth); err != nil {
return nil, fmt.Errorf("vault login: %w", err)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
csr, err := x509.CreateCertificateRequest(rand.Reader,
&x509.CertificateRequest{Subject: pkix.Name{CommonName: p.cfg.CommonName}}, key)
if err != nil {
return nil, err
}
sec, err := p.vault.Logical().WriteWithContext(ctx, p.cfg.PKIPath, map[string]any{
"csr": string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})),
"common_name": p.cfg.CommonName,
})
if err != nil {
return nil, fmt.Errorf("vault pki sign: %w", err)
}
block, _ := pem.Decode([]byte(sec.Data["certificate"].(string)))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return &memSigner{key: key, cert: cert}, nil
}
// Retrieve implements aws.CredentialsProvider.
func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.signer == nil || time.Until(p.signer.cert.NotAfter) < 15*time.Minute {
s, err := p.issue(ctx)
if err != nil {
return aws.Credentials{}, err
}
p.signer = s
}
out, err := ra.GenerateCredentials(&ra.CredentialsOpts{
TrustAnchorArnStr: p.cfg.TrustAnchorArn,
ProfileArnStr: p.cfg.ProfileArn,
RoleArn: p.cfg.RoleArn,
SessionDuration: 3600,
}, p.signer, "AWS4-X509-ECDSA-SHA256")
if err != nil {
return aws.Credentials{}, err
}
exp, _ := time.Parse(time.RFC3339, out.Expiration)
return aws.Credentials{
AccessKeyID: out.AccessKeyId, SecretAccessKey: out.SecretAccessKey, SessionToken: out.SessionToken,
Source: "vault-pki+rolesanywhere", CanExpire: true, Expires: exp,
}, nil
}
Gắn vào SDK chỉ là một option trên config. Mọi AWS client tạo từ config đó đều dùng danh tính trong bộ nhớ, và credentials cache của SDK tự làm mới trước khi hết hạn:
p, _ := rawpki.New(rawpki.Config{
VaultRole: "orders-reporter",
TokenPath: "/var/run/secrets/vault/token",
PKIPath: "pki_workloads/sign/orders-reporter",
CommonName: "orders-reporter",
TrustAnchorArn: "arn:aws:rolesanywhere:eu-west-1:999999999999:trust-anchor/<TA_ID>",
ProfileArn: "arn:aws:rolesanywhere:eu-west-1:999999999999:profile/<PROFILE_ID>",
RoleArn: "arn:aws:iam::999999999999:role/onprem-orders-reporter",
})
cfg, _ := config.LoadDefaultConfig(ctx, config.WithRegion("eu-west-1"),
config.WithCredentialsProvider(aws.NewCredentialsCache(p)))
s3c := s3.NewFromConfig(cfg)
Đã kiểm chứng: package biên dịch được với
rolesanywhere-credential-helperv1.8.5,hashicorp/vault/apiv1.23.0,api/auth/kubernetesv0.12.0 và AWS SDK for Go v2. Trên Vault 2.2.0, role PKI và policy ở trên đã ký một CSR sinh trong bộ nhớ và trả về chứng chỉ chỉ có Digital Signature và client auth, hiệu lực một giờ, khớp với key trong bộ nhớ, không cóprivate_keytrong response, và từ chối CSR mang CN khác. Bước đăng nhập Kubernetes và lời gọiCreateSessioncần cluster và tài khoản AWS thật, nên không nằm trong bài thử này.
VM cũng được lợi y như vậy. Provider không quan tâm nó chạy ở đâu. Chỉ phần đăng nhập thay đổi: thay k8sauth bằng auth method của Vault phù hợp với máy, ví dụ auth method aws, gcp hay azure trên VM cloud, jwt với token lấy từ metadata service của nền tảng, hoặc AppRole với SecretID được response-wrap như phương án cuối. Key vẫn được sinh trong bộ nhớ, nên việc quét đĩa agentless cũng không tìm thấy gì trên VM.
Service không viết bằng Go. Phần ký request X.509 mà Roles Anywhere cần nằm trong credential helper viết bằng Go. Với service Java, Python hay Node.js, hãy chạy provider này như một sidecar nhỏ, phục vụ đúng endpoint tương thích IMDS trên 127.0.0.1 như aws_signing_helper serve. Ứng dụng giữ nguyên, và key vẫn không bao giờ chạm tới đĩa.
Cái giá phải trả:
- Vault nằm trên đường khởi động. Mỗi process đăng nhập và xin ký chứng chỉ khi khởi động, rồi khoảng 45 phút một lần sau đó. Hãy chạy Vault ở chế độ HA, và retry với backoff để một lần Vault gián đoạn ngắn chỉ làm chậm lần làm mới chứ không làm nó thất bại.
- Mỗi process một chứng chỉ, không phải mỗi workload. Với Vault PKI và
no_store=truethì điều này miễn phí, khác với giá theo từng chứng chỉ và trần 100 triệu của AWS Private CA. - Bộ nhớ không phải két sắt. Ai có quyền root trên máy vẫn đọc được bộ nhớ của process. Cách này loại bỏ key nằm yên trên đĩa, snapshot, backup và etcd, nhưng không chống được một máy đang chạy đã bị chiếm. Hãy giữ chứng chỉ và session ngắn, để key bị đánh cắp vô dụng trong vòng một giờ.
Phương án khác: AWS Private CA và cert-manager cho Kubernetes
Vault PKI rất hợp khi bạn đã có sẵn Vault. Nếu workload không có danh tính cloud của bạn là pod Kubernetes, ví dụ cluster on-prem, k3s ở edge hay cluster nằm ở cloud khác, thì có một biến thể không cần tự vận hành CA: AWS Private CA ký chứng chỉ, còn cert-manager đưa một chứng chỉ mới vào từng pod.
Trên production: đây là cách các pod trên cluster Huawei Cloud CCE và Yandex Managed Kubernetes truy cập AWS. Những cluster này không có danh tính AWS nào, và với cách làm này cũng không workload nào phải giữ AWS access key. Việc triển khai dựa theo hướng dẫn của AWS Connect your on-premises Kubernetes cluster to AWS APIs using IAM Roles Anywhere, với một thay đổi có chủ đích: mỗi workload một chứng chỉ thay vì mỗi pod một chứng chỉ như CSI driver, vì một quota của Private CA không thể tăng (bước 4).
| Vault PKI | AWS Private CA (chế độ short-lived) | |
|---|---|---|
| Ai vận hành CA | Bạn (HA, unseal, nâng cấp) | AWS. Key của CA nằm trong HSM do AWS quản lý |
| Trust anchor | CERTIFICATE_BUNDLE: upload chứng chỉ CA, upload lại khi xoay vòng |
AWS_ACM_PCA: trỏ tới ARN của CA |
| Thời hạn chứng chỉ | Tuỳ ý | Tối đa 7 ngày |
| Thu hồi | Phát hành CRL rồi ImportCrl |
Không cần với chứng chỉ ngắn hạn. Hết hạn chính là thu hồi |
| Chi phí | Hạ tầng của bạn | 50 USD mỗi CA mỗi tháng + 0,058 USD mỗi chứng chỉ (chế độ general-purpose: 400 USD + 0,75 USD) |
| Hợp nhất với | VM, bare metal, hệ thống hỗn hợp | Workload Kubernetes qua cert-manager |
1. Một issuing CA ở chế độ short-lived
AWS yêu cầu CA short-lived phải là CA cuối cùng trong chuỗi, nên hãy tạo nó dưới dạng subordinate của một root bạn đã có: một root trên AWS Private CA, hoặc root offline của riêng bạn.
aws acm-pca create-certificate-authority \
--certificate-authority-type SUBORDINATE \
--usage-mode SHORT_LIVED_CERTIFICATE \
--certificate-authority-configuration '{"KeyAlgorithm":"EC_prime256v1","SigningAlgorithm":"SHA256WITHECDSA","Subject":{"CommonName":"k8s-workloads-issuing"}}'
Kích hoạt một lần: lấy CSR của nó (get-certificate-authority-csr), ký bằng root (nếu root là AWS Private CA thì dùng template SubordinateCACertificate_PathLen0/V1), rồi import-certificate-authority-certificate. Chi phí đi theo số chứng chỉ bạn cấp, nên hãy cấp theo workload thay vì theo pod (bước 4): 500 workload gia hạn mỗi 16 giờ là khoảng 22.500 chứng chỉ mỗi tháng, tức chừng 1.300 USD với giá 0,058 USD.
2. Trust anchor trỏ tới CA
aws rolesanywhere create-trust-anchor --name k8s-workloads --enabled \
--source "sourceType=AWS_ACM_PCA,sourceData={acmPcaArn=arn:aws:acm-pca:eu-west-1:999999999999:certificate-authority/<CA_ID>}"
Không còn bundle chứng chỉ nào phải upload hay xoay vòng. Có hai chi tiết trong tài liệu cần lưu ý:
- Với trust anchor kiểu
AWS_ACM_PCA, API reference ghi rằng root certificate của CA đó được dùng để kiểm tra. Nếu cùng root đó còn ký cho CA khác, chứng chỉ của các CA kia cũng sẽ hợp lệ. Hãy ghim issuing CA trong trust policy của role bằngaws:PrincipalTag/x509Issuer/CN. - Với trust anchor dùng Private CA, sau khi tạo chỉ sửa được thời hạn hiệu lực. Khi chứng chỉ của CA được gia hạn, gọi
update-trust-anchorđể Roles Anywhere nhận thời hạn mới.
Profile và role giữ nguyên như ở bước 2. Trust policy của role giờ dựa vào những gì cert-manager ghi vào chứng chỉ:
"Condition": {
"ArnEquals": { "aws:SourceArn": "arn:aws:rolesanywhere:eu-west-1:999999999999:trust-anchor/<TA_ID>" },
"StringEquals": {
"aws:PrincipalTag/x509Issuer/CN": "k8s-workloads-issuing",
"aws:PrincipalTag/x509Subject/CN": "orders-reporter.orders"
}
}
3. cert-manager và issuer cho Private CA
Cài cert-manager, CSI driver của nó và plugin aws-privateca-issuer, rồi trỏ một cluster issuer tới CA:
helm repo add awspca https://cert-manager.github.io/aws-privateca-issuer
helm install aws-privateca-issuer awspca/aws-privateca-issuer -n cert-manager
apiVersion: awspca.cert-manager.io/v1beta1
kind: AWSPCAClusterIssuer
metadata:
name: k8s-workloads
spec:
arn: arn:aws:acm-pca:eu-west-1:999999999999:certificate-authority/<CA_ID>
region: eu-west-1
Issuer cần quyền acm-pca:IssueCertificate, acm-pca:GetCertificate và acm-pca:DescribeCertificateAuthority trên đúng CA đó, và đây là chỗ “con gà quả trứng”: trên một cluster không có danh tính cloud, credentials của chính issuer lấy từ đâu? Theo thứ tự ưu tiên:
- Cluster chạy trên GKE hoặc AKS. Federate ServiceAccount của issuer sang AWS theo kiểu không key (Google Cloud → AWS). Không có secret nào.
- Cho issuer một danh tính Roles Anywhere riêng. Plugin hỗ trợ IAM Roles Anywhere. Cấp tay một chứng chỉ ban đầu cho nó, sau đó để nó tự gia hạn từ cùng CA.
- Phương án cuối: một access key duy nhất chỉ được gọi ba action đó trên đúng CA đó. Cả cluster chỉ có một secret phạm vi rất hẹp, thay vì mỗi workload một secret.
4. Mỗi workload một chứng chỉ, không phải mỗi pod
Hướng dẫn của chính AWS cho mô hình này dùng CSI driver của cert-manager, cho mỗi pod một cặp key riêng sinh ngay trên node. Cách đó gọn, nhưng mỗi lần pod khởi động và mỗi lần gia hạn là một lần gọi IssueCertificate, mà AWS Private CA có một trần cứng cho việc này:
| Quota của AWS Private CA | Mặc định | Tăng được |
|---|---|---|
| Số chứng chỉ mỗi CA được cấp, trọn đời | 100.000.000 (1.000.000 nếu dùng complete CRL) | Không |
Tốc độ IssueCertificate |
25 mỗi giây cho mỗi account và Region | Có |
Với chứng chỉ theo từng pod, số lượng tăng theo số pod × số lần gia hạn + churn: autoscaling, rolling deploy, CronJob và crash loop đều sinh chứng chỉ mới. Một hệ thống churn 100.000 pod mỗi ngày sẽ chạm trần trong chưa tới ba năm, và trả 0,058 USD cho từng chứng chỉ trên đường đi. Khi một CA chạm trần, cách duy nhất là tạo CA mới và trust anchor mới. Trên các cluster Huawei và Yandex, đây chính là lý do bỏ CSI driver.
Cách khắc phục là cấp một chứng chỉ cho mỗi workload (mỗi ServiceAccount) bằng Certificate của cert-manager, lưu trong Secret, và để mọi replica cùng mount Secret đó. Số chứng chỉ giờ tăng theo số workload chứ không theo số pod:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: orders-reporter-aws
namespace: orders
spec:
secretName: orders-reporter-aws-identity
commonName: orders-reporter.orders # <serviceaccount>.<namespace>
duration: 24h
renewBefore: 8h
usages: ["client auth"]
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always
issuerRef:
name: k8s-workloads
kind: AWSPCAClusterIssuer
group: awspca.cert-manager.io
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-reporter
namespace: orders
spec:
replicas: 6 # cả sáu dùng chung một chứng chỉ
selector:
matchLabels: { app: orders-reporter }
template:
metadata:
labels: { app: orders-reporter }
spec:
serviceAccountName: orders-reporter
containers:
- name: app
image: registry.example.com/orders-reporter:1.4.2
env:
- { name: AWS_EC2_METADATA_SERVICE_ENDPOINT, value: "http://127.0.0.1:9911" }
- { name: AWS_REGION, value: eu-west-1 }
- name: aws-credentials # sidecar; tự build image nhỏ từ binary release
image: registry.example.com/aws-signing-helper:1.8.5
args:
- serve
- --certificate=/tls/tls.crt
- --private-key=/tls/tls.key
- --trust-anchor-arn=arn:aws:rolesanywhere:eu-west-1:999999999999:trust-anchor/<TA_ID>
- --profile-arn=arn:aws:rolesanywhere:eu-west-1:999999999999:profile/<PROFILE_ID>
- --role-arn=arn:aws:iam::999999999999:role/k8s-orders-reporter
volumeMounts:
- { name: aws-identity, mountPath: /tls, readOnly: true }
volumes:
- name: aws-identity
secret:
secretName: orders-reporter-aws-identity
Với Certificate, bất kỳ ai tạo được nó trong namespace của mình đều có thể xin bất kỳ common name nào, kể cả của team khác. Hãy chặn bằng cert-manager approver-policy, như hướng dẫn của AWS cũng làm: một policy chung cho cả cluster, chỉ duyệt chứng chỉ client auth có CN kết thúc bằng đúng namespace của người xin.
apiVersion: policy.cert-manager.io/v1alpha1
kind: CertificateRequestPolicy
metadata:
name: aws-workload-identity
spec:
allowed:
commonName:
value: "*"
required: true
validations:
- rule: "self.endsWith('.' + cr.namespace)"
message: "commonName must be <serviceaccount>.<namespace>"
usages: ["client auth"]
selector:
issuerRef:
name: k8s-workloads
kind: AWSPCAClusterIssuer
group: awspca.cert-manager.io
Gán verb use trên policy này cho ServiceAccount của controller cert-manager, vì chính controller tạo CertificateRequest cho các Certificate. Kết hợp với trust policy ghim x509Subject/CN, mỗi team chỉ có thể nhận đúng các AWS role dành cho namespace của mình.
Những gì phải đánh đổi, và cần để ý:
- Private key nằm trong Secret, tức là trong etcd. Bật encryption at rest và giữ quyền
get secretstrong namespace của workload chặt như mọi credential khác. - Các replica dùng chung danh tính. Cả sáu pod trình cùng một chứng chỉ, nên CloudTrail hiển thị cùng một session name (serial của chứng chỉ) cho tất cả. Quyền vốn đã cấp theo workload, nhưng phía AWS sẽ không còn phân biệt được từng pod.
- Xoay vòng vẫn không cần restart. Mount Secret dưới dạng volume, không dùng
subPath, để kubelet cập nhật file khi cert-manager gia hạn. Signing helper đọc chứng chỉ và key từ đĩa ở mỗi lần ký, nênservetự dùng cặp mới. Giữ nguyên thuật toán key qua các lần gia hạn, vì thuật toán ký chỉ được chọn một lần lúc khởi động. usages: ["client auth"]và không gì khác. Issuer chọn template của Private CA dựa trên đúng danh sách usage: chỉclient auththì dùngEndEntityClientAuthCertificate/V1, mọi tổ hợp khác rơi về template CSR pass-through. Hãy kiểm tra chứng chỉ được cấp có Digital Signature trong key usage (openssl x509 -noout -text), vì Roles Anywhere từ chối chứng chỉ thiếu nó.- Sidecar dùng chung localhost của pod, nên chỉ các container trong pod đó mới gọi được
127.0.0.1:9911. - Không có container image chính thức cho signing helper. Hãy build một image từ binary release đã công bố và ghim phiên bản.
CSI driver vẫn là lựa chọn tốt cho hệ thống nhỏ và ổn định, nơi việc key không bao giờ chạm tới etcd đáng giá hơn số lượng chứng chỉ. Thay volume secret: bằng volume csi.cert-manager.io với issuer-kind: AWSPCAClusterIssuer, common-name: "${SERVICE_ACCOUNT_NAME}.${POD_NAMESPACE}" và key-usages: "client auth", và theo dõi số chứng chỉ CA đã cấp.
AWS làm hub cho Google Cloud và Azure
Google Cloud: AWS provider trỏ vào endpoint cục bộ
Dựng pool y như bài AWS → Google Cloud: một AWS provider cho account 999999999999, và cấp quyền cho principalSet://…/attribute.aws_role/onprem-orders-reporter. Session name giờ là serial của chứng chỉ, nhưng tên role vẫn cố định, và tên role mới là thứ được cấp quyền.
File cấu hình credential --aws sinh ra mặc định đọc AWS credentials từ 169.254.169.254. Hãy trỏ nó sang endpoint của helper. Helper không phục vụ đường dẫn region, nên bỏ region_url đi và đặt AWS_REGION:
gcloud iam workload-identity-pools create-cred-config \
projects/123456789012/locations/global/workloadIdentityPools/aws-prod/providers/aws-hub \
--aws --enable-imdsv2 --output-file=gcp-wif.json
jq '.credential_source.url = "http://127.0.0.1:9911/latest/meta-data/iam/security-credentials"
| .credential_source.imdsv2_session_token_url = "http://127.0.0.1:9911/latest/api/token"
| del(.credential_source.region_url)' gcp-wif.json > gcp-wif-onprem.json
export GOOGLE_APPLICATION_CREDENTIALS=/etc/gcp/gcp-wif-onprem.json
export AWS_REGION=eu-west-1
Cách này chạy được với thư viện Python google-auth: nó đọc region từ AWS_REGION trước, và chấp nhận mọi URL metadata. Với ngôn ngữ khác, hoặc nếu không muốn sửa file, hãy dùng custom AWS credential supplier trong bài GCP, cài bằng AWS SDK. Supplier đó tự lấy credentials Roles Anywhere qua cùng biến AWS_EC2_METADATA_SERVICE_ENDPOINT.
Azure: outbound identity federation từ hub
Bật IAM outbound identity federation trong identity account, để có một issuer duy nhất cho mọi workload on-prem. Sau đó làm theo bài AWS → Azure:
- Cho role quyền
sts:GetWebIdentityToken, chỉ với audienceapi://AzureADTokenExchange, thuật toánRS256và thời hạn ngắn. - Trên managed identity của Azure, tạo federated credential với issuer của identity account và subject
arn:aws:iam::999999999999:role/onprem-orders-reporter.
import boto3
from azure.identity import ClientAssertionCredential
sts = boto3.client("sts", region_name="eu-west-1") # credentials lấy từ 127.0.0.1:9911
credential = ClientAssertionCredential(
tenant_id="<entra-tenant-id>",
client_id="<client-id của id-orders-reporter>",
func=lambda: sts.get_web_identity_token(
Audience=["api://AzureADTokenExchange"], SigningAlgorithm="RS256", DurationSeconds=300,
)["WebIdentityToken"],
)
Không muốn viết code? Cách dùng sidecar hoặc timer ghi file token cho AZURE_FEDERATED_TOKEN_FILE trong bài Azure vẫn dùng nguyên được ở đây.
Thu hồi quyền: các lớp “công tắc ngắt”
- Hết hạn. Với chứng chỉ 24 giờ, key bị đánh cắp sẽ tự vô dụng trong vòng một ngày.
- Một chứng chỉ. Thu hồi trong Vault, rồi import CRL bằng
aws rolesanywhere import-crl. AWS kiểm tra CRL đã import, nhưng không tự gọi CRL distribution point hay OCSP, nên hãy tự động hoá việc đẩy CRL lên. Với trust anchor dùng AWS Private CA, quy tắc vẫn vậy: Roles Anywhere chỉ kiểm tra CRL đã được import, nên CRL của Private CA chế độ general-purpose cũng phải đẩy lên bằngimport-crl. CA chế độ short-lived thì bỏ qua hoàn toàn bước này và dựa vào việc hết hạn. - Một workload. Gỡ role của nó khỏi profile, hoặc siết lại trust policy.
- Mọi thứ đến từ CA đó.
aws rolesanywhere disable-trust-anchor. - Một cloud. Xoá binding trên GCP hoặc FIC trên Azure.
Thu hồi chỉ chặn được session mới. Credentials đã cấp vẫn dùng được tới khi hết hạn, nên hãy giữ duration_seconds ngắn. Khi khẩn cấp, gắn thêm một deny policy lên role dựa trên aws:TokenIssueTime. Đó cũng chính là cách nút Revoke active sessions trong IAM console hoạt động.
Có nên dùng hub? Những đánh đổi
| Hub qua AWS | Mỗi cloud tự federation | |
|---|---|---|
| Số điểm tích hợp PKI | một (Roles Anywhere) | mỗi cloud một điểm |
| Thu hồi quyền | một nơi cho mọi cloud | làm riêng từng cloud |
| Audit | CloudTrail ghi mọi session kèm serial chứng chỉ | ba định dạng log khác nhau |
| Tính sẵn sàng | truy cập GCP/Azure phụ thuộc thêm AWS STS | độc lập |
| Độ trễ lần gọi đầu | thêm một bước | đi thẳng |
| Giới hạn session | role chaining tối đa 1 giờ | giới hạn gốc của từng cloud |
Có những lựa chọn khác đáng cân nhắc. Google Cloud cũng có thể federation trực tiếp bằng chứng chỉ X.509, và SPIFFE/SPIRE có thể đóng vai OIDC issuer mà cả ba cloud cùng tin. Hub qua AWS phát huy tác dụng khi AWS vốn là cloud chính và đã có identity account, khi bạn muốn một nơi duy nhất để thu hồi và audit, và khi các máy nằm ngoài mọi cloud.
Checklist
- Một intermediate CA riêng cho workload, chứng chỉ ngắn hạn (tính bằng giờ, không phải năm), và đặt key trong TPM hoặc HSM khi có thể.
- Trust policy ghim
aws:SourceArn(trust anchor) vàx509Subject/CNcủa chứng chỉ. - Mỗi workload một role và một Vault role, để một chứng chỉ chỉ có thể nhận đúng tên của chính nó.
- Không có private key nằm yên trên đĩa. Service backend tự đăng nhập Vault (Kubernetes auth, hoặc auth method của nền tảng với VM), sinh key trong bộ nhớ và dùng
pki/sign, nên công cụ quét đĩa và snapshot không có gì để báo. - Chỉ dùng
servetrên máy dành riêng cho một workload, kèm--hop-limit 1. Trường hợp khác dùngcredential-process. - Tự động import CRL, giữ thời hạn profile ngắn, và diễn tập việc tắt trust anchor.
- GCP cấp quyền theo
attribute.aws_role/<role>. Azure tạo FIC theo ARN của role. Không cloud nào cần biết về CA của bạn.
Kết luận
IAM Roles Anywhere lấp khoảng trống cuối cùng của workload identity: những máy không thuộc về cloud nào. Cấp cho chúng chứng chỉ ngắn hạn từ một CA dành riêng cho workload, và chúng có AWS role session với điều kiện dựa trên chính các trường trong chứng chỉ, được audit tới tận serial number. Federation tiếp từ một AWS identity account duy nhất, và cùng chứng chỉ đó mở được cả Google Cloud lẫn Azure, không còn access key, key.json hay client secret ở bất cứ đâu.
Nguồn
- Hướng dẫn IAM Roles Anywhere: https://docs.aws.amazon.com/rolesanywhere/latest/userguide/introduction.html
- Mô hình tin cậy của IAM Roles Anywhere: https://docs.aws.amazon.com/rolesanywhere/latest/userguide/trust-model.html
- Credential helper (aws_signing_helper): https://docs.aws.amazon.com/rolesanywhere/latest/userguide/credential-helper.html
- Mã nguồn rolesanywhere-credential-helper: https://github.com/aws/rolesanywhere-credential-helper
- Vault PKI secrets engine: https://developer.hashicorp.com/vault/docs/secrets/pki
- Workload Identity Federation với AWS (Google Cloud): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds
- AWS outbound identity federation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_outbound.html
- Entra workload identity federation: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation
- Kubernetes auth method của Vault: https://developer.hashicorp.com/vault/docs/auth/kubernetes
- API PKI của Vault, ký chứng chỉ: https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate
- Vault Go client, Kubernetes auth: https://pkg.go.dev/github.com/hashicorp/vault/api/auth/kubernetes
- Credential helper dùng như thư viện Go (
aws_signing_helper): https://pkg.go.dev/github.com/aws/rolesanywhere-credential-helper/aws_signing_helper - Các chế độ của AWS Private CA (general-purpose và short-lived): https://docs.aws.amazon.com/privateca/latest/userguide/short-lived-certificates.html
- Bảng giá AWS Private CA: https://aws.amazon.com/private-ca/pricing/
create-trust-anchor(nguồn AWS_ACM_PCA): https://docs.aws.amazon.com/cli/latest/reference/rolesanywhere/create-trust-anchor.html- aws-privateca-issuer cho cert-manager: https://github.com/cert-manager/aws-privateca-issuer
- CSI driver của cert-manager: https://cert-manager.io/docs/usage/csi-driver/



Bình luận