> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Tăng cường bảo mật AEM 6.5 với Azure Key Vault

Mọi dự án AEM khi tích hợp với dịch vụ thứ ba đều cần lưu trữ thông tin nhạy cảm. API key, token thanh toán, mật khẩu database. Những thứ này không bao giờ được phép nằm trong mã nguồn.

Trên AEM 6.5 on-premise, giải pháp truyền thống là mã hóa bằng Crypto Support (`crypto:aes{...}`) hoặc lưu trong OSGi config. Nhưng khi hệ thống mở rộng, cách làm này bộc lộ nhiều điểm nghẽn. Bài viết này hướng dẫn kết nối AEM 6.5 với Azure Key Vault để quản lý secret tập trung, áp dụng chuỗi xác thực linh hoạt cho cả môi trường local lẫn on-premise.

## Giới hạn của CryptoSupport và Cấu hình OSGi Cục bộ

Crypto Support của AEM hoạt động khá ổn cho các nhu cầu đơn giản. Nhưng nó có điểm yếu chí mạng: cặp khóa mã hóa (HMAC và Master key) nằm ngay trên ổ cứng của từng máy chủ AEM.

Trong một topology on-premise điển hình gồm một Author và nhiều Publisher, việc đồng bộ bộ khóa này giữa các instance đòi hỏi sự can thiệp thủ công. Và nếu một server gặp sự cố cần dựng lại từ đầu? Bạn lại phải sao chép key hoặc chạy lại quy trình mã hóa thủ công. Rất dễ sai sót.

Chưa kể, các hệ thống bên ngoài không thể giải mã được chuỗi `crypto:aes{...}` của AEM. Secret vì thế bị cô lập hoàn toàn.

Azure Key Vault giải quyết triệt để vấn đề này nhờ kiểm soát truy cập dựa trên vai trò (Azure RBAC). Toàn bộ thao tác truy xuất đều được ghi nhật ký và kiểm toán tập trung từ một nơi. Đây là lợi thế vượt trội so với việc phân tán biến môi trường hay khóa mã hóa cục bộ.

Tôi giữ kịch bản dưới đây ở mức tối giản (bảo mật dự án mà, bạn biết đấy). Mục tiêu là giúp bạn nắm vững kiến trúc tích hợp cơ bản.

## Kiến trúc Tổng quan: Local Dev vs On-Premise

Chiến lược ở đây là tách biệt phương thức xác thực theo môi trường nhưng dùng chung một giao diện code trong AEM.

```mermaid
flowchart TD
    subgraph Local["Môi trường Local Dev"]
        Dev["Lập trình viên"] -->|"1. az login"| AzCLI["Azure CLI"]
        LocalAEM["AEM 6.5 Quickstart / Docker"] -->|"2. AzureCliCredential"| AzCLI
        AzCLI -->|"3. Access Token"| KVLocal["Azure Key Vault"]
    end

    subgraph OnPrem["Môi trường Production / On-Premise (AEM 6.5)"]
        ServiceUser["Service User (Sling)"] -->|"Đọc PFX Cert"| KeyStore["AEM Granite Keystore"]
        KeyStore -->|"ClientCertificateCredential"| OSGiService["OSGi Secret Service"]
        OSGiService -->|"1. Request JWT Token"| EntraID["Microsoft Entra ID"]
        EntraID -->|"2. Bearer Access Token"| OSGiService
        OSGiService -->|"3. Get Secret (HTTPS)"| KVCloud["Azure Key Vault"]
        KVCloud -->|"4. Trả về Secret"| InMemCache["In-Memory Cache (Caffeine)"]
        InMemCache -->|"5. Phục vụ dữ liệu"| AEMComponents["AEM Components / Servlets"]
    end
```

Mô hình trên bảo đảm hai yếu tố: tiện lợi tối đa cho dev ở máy trạm và bảo mật tuyệt đối khi lên production. Không một chuỗi khóa bí mật nào bị phơi bày ra bên ngoài.

## Bước 1: Khởi tạo Key Vault và Secret

Khởi tạo một resource group và vault mới trên Azure. Chế độ phân quyền RBAC nên được bật ngay từ đầu:

```bash title="create-keyvault.sh"
# Tạo resource group
az group create \
    --location southeastasia \
    --name aem-security-rg

# Tạo Key Vault với RBAC authorization
az keyvault create \
    --resource-group aem-security-rg \
    --location southeastasia \
    --name aem-kv-app-001 \
    --enable-rbac-authorization true
```

Tài khoản cá nhân cần quyền quản trị để tạo secret và chứng chỉ ban đầu. Hai role sau đây đáp ứng đúng nhu cầu đó:

```bash title="assign-admin-roles.sh"
currentUserId=$(az ad signed-in-user show --query 'id' --out tsv)
vaultId=$(az keyvault list --query "[?name=='aem-kv-app-001'].id | [0]" --out tsv)

# Cấp quyền quản lý chứng chỉ
az role assignment create \
    --assignee "${currentUserId}" \
    --scope "${vaultId}" \
    --role "Key Vault Certificates Officer"

# Cấp quyền quản lý secret
az role assignment create \
    --assignee "${currentUserId}" \
    --scope "${vaultId}" \
    --role "Key Vault Secrets Officer"
```

Bây giờ, thêm một vài secret mẫu vào vault để chuẩn bị kiểm tra:

```bash title="set-secrets.sh"
az keyvault secret set \
    --vault-name aem-kv-app-001 \
    --name "payment-api-key" \
    --value "secret-token-value-live-2026"

az keyvault secret set \
    --vault-name aem-kv-app-001 \
    --name "crm-client-secret" \
    --value "super-confidential-secret-key"
```

## Bước 2: Khai báo Azure SDK trong AEM Bundle

Thêm thư viện Azure Identity và Azure Key Vault Secrets vào file `core/pom.xml`.

Lưu ý quan trọng cho môi trường OSGi: hãy loại trừ module `azure-core-http-netty`. Thư viện Netty thường xuyên gây xung đột classloading với HTTP service của Felix và Jetty bên trong AEM 6.5. Thay vào đó, hãy dùng transport OkHttp:

```xml title="core/pom.xml"
<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-security-keyvault-secrets</artifactId>
    <version>4.9.2</version>
    <exclusions>
        <exclusion>
            <groupId>com.azure</groupId>
            <artifactId>azure-core-http-netty</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-identity</artifactId>
    <version>1.15.3</version>
</dependency>
<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-core-http-okhttp</artifactId>
    <version>1.12.5</version>
</dependency>
```

Đừng quên cấu hình nhúng (embed) các package cần thiết vào OSGi bundle `core` của dự án.

## Bước 3: Xác thực nhanh ở Local bằng Azure CLI

Trên máy trạm hoặc trong Docker container, lập trình viên đã đăng nhập sẵn qua `az login`. Chúng ta có thể tận dụng ngay phiên làm việc này.

Lớp `AzureCliCredentialBuilder` cho phép `SecretClient` lấy Access Token trực tiếp từ CLI:

```java title="AzureCliSample.java"
final var credential = new AzureCliCredentialBuilder().build();

final var client = new SecretClientBuilder()
        .vaultUrl("https://aem-kv-app-001.vault.azure.net/")
        .credential(credential)
        .buildClient();

final var secret = client.getSecret("payment-api-key");
log.info("Secret value: {}", secret.getValue());
```

Cách này giúp lập trình viên kiểm thử code mà không phải lưu thông tin xác thực nào vào file `.env` hay cấu hình OSGi cục bộ. Và khi bạn gõ `az logout`, quyền truy cập lập tức bị hủy bỏ. Rất sạch sẽ.

## Bước 4: Tạo Service Principal cho Môi trường On-Premise

Máy chủ on-premise không có Azure CLI. Sử dụng `AzureCliCredential` trên production là điều bất khả thi.

Tài khoản dịch vụ (Service Principal) kết hợp với Client Certificate là lựa chọn tiêu chuẩn. Chúng ta tránh dùng Client Secret vì bản chất nó vẫn là một chuỗi mật khẩu tĩnh cần quản lý.

Lệnh sau khởi tạo một Service Principal trong Microsoft Entra ID và tự sinh chứng chỉ lưu thẳng vào Key Vault:

```bash title="create-sp-cert.sh"
# Tạo app registration, service principal và certificate trong Key Vault
appId=$(az ad sp create-for-rbac \
    --name aem-kv-integration-sp \
    --create-cert \
    --cert aem-app-cert \
    --keyvault aem-kv-app-001 \
    --query 'appId' --out tsv)

# Gắn tag ứng dụng tích hợp Entra ID
az ad sp update --id "${appId}" --set 'tags=["WindowsAzureActiveDirectoryIntegratedApp"]'

# Cấp quyền đọc duy nhất secret cần dùng theo nguyên tắc đặc quyền tối thiểu
vaultId=$(az keyvault list --query "[?name=='aem-kv-app-001'].id | [0]" --out tsv)

az role assignment create \
    --assignee "${appId}" \
    --scope "${vaultId}/secrets/payment-api-key" \
    --role "Key Vault Secrets User"
```

Lúc này, Service Principal chỉ được phép đọc đúng một secret `payment-api-key`. Các secret khác hoàn toàn ngoài tầm với.

## Bước 5: Cài đặt Certificate vào AEM Keystore

Tải chứng chỉ từ Key Vault về máy và đóng gói sang định dạng PKCS12 có mật khẩu bảo vệ:

```bash title="export-cert.sh"
# Tải secret chứa private key, decode base64 và đóng gói thành tệp .pfx
az keyvault secret show \
    --vault-name aem-kv-app-001 \
    --name aem-app-cert \
    --query 'value' --out tsv \
| base64 --decode \
| openssl pkcs12 -nodes -passin pass:"" \
| openssl pkcs12 -export \
    -out keystore-for-aem.pfx \
    -name aemKeyVaultCert \
    -passout pass:"SecretPassword2026!"
```

Trên AEM 6.5, mở giao diện quản trị Keystore của system user tương ứng (ví dụ: `authentication-service` tại đường dẫn `/libs/granite/security/content/v2/usereditor.html` hoặc qua User Management trong Tools > Security). Tải tệp `.pfx` vừa xuất lên và nhập mật khẩu bảo vệ:

![Cài đặt Certificate vào AEM Keystore](/static/image/aem-keystore-install-cert.5ababe7d99.gif)
Private key lúc này được AEM mã hóa và lưu trữ an toàn trong kho chứng chỉ của hệ thống.

## Bước 6: Ghép chuỗi xác thực với ChainedTokenCredential

Giờ là lúc gắn kết tất cả lại với nhau trong Java code.

Chúng ta cần đọc certificate từ AEM Keystore, nạp vào bộ nhớ và cung cấp cho `ClientCertificateCredentialBuilder`. Đồng thời, sử dụng `ChainedTokenCredential` để kết nối cả hai phương thức xác thực:

```java title="AzureKeyVaultSecretServiceImpl.java"
package com.mysite.core.services.impl;

import com.azure.core.credential.TokenCredential;
import com.azure.core.exception.HttpResponseException;
import com.azure.identity.AzureCliCredentialBuilder;
import com.azure.identity.ChainedTokenCredentialBuilder;
import com.azure.identity.ClientCertificateCredentialBuilder;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.adobe.granite.keystore.KeyStoreService;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.util.UUID;

@Component(service = AzureKeyVaultSecretServiceImpl.class, immediate = true)
@Designate(ocd = AzureKeyVaultSecretServiceImpl.Config.class)
public class AzureKeyVaultSecretServiceImpl {

    private static final Logger log = LoggerFactory.getLogger(AzureKeyVaultSecretServiceImpl.class);

    @ObjectClassDefinition(name = "Azure Key Vault Integration Configuration")
    public @interface Config {
        @AttributeDefinition(description = "Application Client ID")
        String client_id();

        @AttributeDefinition(description = "Microsoft Entra Directory Tenant ID")
        String tenant_id();

        @AttributeDefinition(description = "Alias của certificate trong AEM Keystore")
        String keystore_entry_alias() default "aemKeyVaultCert";

        @AttributeDefinition(description = "Tài khoản sở hữu Keystore (ví dụ: authentication-service)")
        String keystore_user_id() default "authentication-service";

        @AttributeDefinition(description = "Endpoint URL của Azure Key Vault")
        String vault_url();
    }

    private Config config;

    @Reference
    private KeyStoreService keyStoreService;

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Activate
    protected void activate(final Config config) {
        this.config = config;
    }

    public String getSecretValue(final String secretName) {
        final TokenCredential credential = getTokenCredential();
        final SecretClient client = new SecretClientBuilder()
                .vaultUrl(config.vault_url())
                .credential(credential)
                .buildClient();

        try {
            return client.getSecret(secretName).getValue();
        } catch (HttpResponseException e) {
            log.error("Không thể lấy secret '{}' từ Key Vault", secretName, e);
            return null;
        }
    }

    private TokenCredential getTokenCredential() {
        final var builder = new ChainedTokenCredentialBuilder();

        // 1. Thử xác thực Azure CLI cho môi trường local dev
        builder.addLast(new AzureCliCredentialBuilder().build());

        // 2. Fallback sang Client Certificate lấy từ AEM Keystore cho production
        final var pfxPasswd = RandomStringUtils.randomAscii(8);
        try (var resolver = resourceResolverFactory.getServiceResourceResolver(null);
             var pfxStream = getPfxCertificateStream(resolver, pfxPasswd)) {

            if (pfxStream != null) {
                builder.addLast(new ClientCertificateCredentialBuilder()
                        .tenantId(config.tenant_id())
                        .clientId(config.client_id())
                        .pfxCertificate(pfxStream)
                        .clientCertificatePassword(pfxPasswd)
                        .build());
            }
        } catch (LoginException | IOException e) {
            log.warn("Không thể tải certificate từ AEM Keystore", e);
        }

        return builder.build();
    }

    private ByteArrayInputStream getPfxCertificateStream(final ResourceResolver resolver, final String pfxPasswd) {
        try {
            final var entry = keyStoreService.getKeyStoreEntry(
                    resolver, config.keystore_user_id(), config.keystore_entry_alias());

            if (entry == null) {
                log.warn("Không tìm thấy certificate alias '{}' trong Keystore", config.keystore_entry_alias());
                return null;
            }

            final var pfx = KeyStore.getInstance("PKCS12");
            pfx.load(null, null);
            pfx.setEntry(UUID.randomUUID().toString(), entry, new KeyStore.PasswordProtection(pfxPasswd.toCharArray()));

            final var out = new ByteArrayOutputStream();
            pfx.store(out, pfxPasswd.toCharArray());
            return new ByteArrayInputStream(out.toByteArray());

        } catch (Exception e) {
            log.warn("Lỗi trích xuất PFX certificate từ Keystore", e);
            return null;
        }
    }
}
```

Mỗi khi `SecretClient` cần lấy token, nó sẽ hỏi chuỗi `ChainedTokenCredential`. Trình xác thực duyệt lần lượt qua các phương án: nếu có session Azure CLI thì dùng ngay, nếu không thì lấy chứng chỉ từ Keystore ra để ký token với Entra ID.

Cùng một đoạn mã nguồn, hoạt động trơn tru trên cả hai môi trường.

## Giải pháp Này Có Thực Tế Trên AEM 6.5 Không?

Hoàn toàn khả thi. Tôi đã trực tiếp áp dụng mô hình này cho các dự án chạy AEM 6.5 trong môi trường enterprise khắt khe.

Nhưng trong thế giới AEM thực chiến, code đẹp thôi chưa đủ. Bạn cần xử lý bốn vấn đề cốt lõi sau để giải pháp vận hành ổn định:

### 1. Phân quyền Service User cho AEM Keystore

Đoạn mã phía trên gọi `resourceResolverFactory.getServiceResourceResolver(null)`. Thao tác này sẽ quăng `LoginException` ngay lập tức nếu bạn quên cấu hình OSGi Service User Mapping.

Trên AEM 6.5, bạn cần định nghĩa System User và cấp quyền đọc đường dẫn chứa Keystore (thường là `/home/users/system/...`). Sau đó ánh xạ trong cấu hình `org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended`:

```text
com.myproject.core:azure-vault-service=[azure-vault-service-user]
```

### 2. Đóng gói Azure SDK vào OSGi Bundle

Bộ thư viện Azure Java SDK không sinh ra sẵn ở dạng OSGi bundle hoàn chỉnh. Nếu chỉ khai báo `dependency` thông thường trong Maven, bundle AEM của bạn sẽ gặp lỗi `ClassNotFoundException` hoặc `Unresolved constraint`.

Giải pháp là cấu hình `bnd-maven-plugin` hoặc `maven-bundle-plugin` để nhúng (embed) các package cần thiết vào bundle `core`:

```xml
<Embed-Dependency>
    azure-security-keyvault-secrets,
    azure-identity,
    azure-core,
    azure-core-http-okhttp
</Embed-Dependency>
```

Đặc biệt, loại trừ `azure-core-http-netty` để tránh xung đột thư viện native epoll với HTTP Jetty bên trong AEM 6.5.

### 3. Vượt qua Corporate Proxy và Tường lửa On-Premise

Máy chủ AEM on-premise của doanh nghiệp thường đặt sâu trong mạng nội bộ (DMZ / Intranet) và không có đường ra Internet trực tiếp.

Để AEM 6.5 kết nối được tới `https://<vault-name>.vault.azure.net` và `login.microsoftonline.com`, bạn có hai hướng xử lý hạ tầng:

Thứ nhất, làm việc với đội Network/SecOps để whitelist outbound cổng 443 tới các domain của Azure, hoặc định tuyến qua kết nối chuyên biệt như Azure ExpressRoute.

Thứ hai, nếu bắt buộc phải đi qua Forward Proxy (như Squid, BlueCoat), hãy thêm cấu hình proxy trực tiếp vào JVM startup parameters của AEM:

```bash
CQ_JVM_OPTS="-Dhttps.proxyHost=proxy.corp.internal -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.internal"
```

Transport OkHttp của Azure SDK sẽ tự động nhận diện và sử dụng proxy từ các tham số JVM này. Không cần viết thêm code tùy biến phức tạp.

### 4. Bắt buộc phải có Tầng Cache

Mỗi lượt gọi `client.getSecret()` là một network round-trip đến Azure data center, gây thêm 100ms - 300ms độ trễ cho request của AEM. Chưa kể Azure Key Vault áp dụng chính sách giới hạn tần suất gọi (rate-limiting).

Đừng gọi Key Vault trực tiếp trong từng Sling Model hay Servlet. Hãy bọc `SecretClient` qua một tầng in-memory cache (như Caffeine hoặc Guava Cache) với TTL khoảng 15 đến 30 phút.

Nhanh hơn. An toàn hơn. Tránh được mã lỗi HTTP 429 không đáng có.

## Lợi ích của Kiến trúc Này

Lập trình viên không bao giờ phải lưu secret nhạy cảm trên máy tính cá nhân.

Đội ngũ vận hành cập nhật mật khẩu hay xoay vòng API key tại một nơi duy nhất trên Azure. Tất cả các instance Author và Publish tự động nhận giá trị mới mà không đòi hỏi phải redeploy, restart server hay đồng bộ lại master key cục bộ.

Hơn nữa, dịch vụ Key Vault đem lại các tính năng nâng cao mà cấu hình cục bộ không thể so sánh: giám sát truy vết (audit logs), cảnh báo bất thường qua Azure Monitor và tính năng chống xóa nhầm (Soft Delete & Purge Protection).

Bảo mật ứng dụng không bắt đầu từ công cụ phức tạp. Nó bắt đầu từ thói quen không lưu secret phân tán. Một khi mô hình định danh và phân quyền được thiết lập chuẩn mực, việc kết nối AEM 6.5 với Azure Key Vault sẽ mang lại sự an tâm cho toàn bộ hệ thống.

## Tham khảo

- Tham khảo và biên soạn ngắn gọn từ [bài viết gốc của Juan Ayala (The AEM Maven)](https://www.theaemmaven.com/post/enhance-security-in-aem-with-azure-key-vault-integration).
