有什麼好處?
傳統的redis 密碼是一個長期有效的明文密碼,寫在 wp-config.php 裡,會跟著備份、複本一起擴散。
IAM auth token 不是每個請求都重新產生:php-fpm 會把它快取在 APCu 裡,最多 600 秒;token 是用 EC2 instance role 的臨時憑證簽出來的,主機上不存放任何長期密鑰。
IAM 驗證防不了的事:
- 主機被入侵:只要能在這台 EC2 上execute,就能向 IMDS 取得 role 憑證、自行簽 token,效果和竊取
wp-config.php裡的密碼差不多。差別在 role 憑證只有數小時有效,能存取的範圍也受 policy 限制。 - 額外負擔:多了 AWS SDK 和 IMDS 兩個依賴;每個請求多一次
AUTH。 - Audit:
elasticache:Connect(IAMAUTH)不會出現在 CloudTrail。 - IAM 驗證可以audit「誰被授權、何時改了權限」,但無法從 CloudTrail 查到「誰在何時連線」。連線層面只能看 CloudWatch ,而且指標只有次數,沒有user 身分。
結論:對單台主機的 WordPress 而言,IAM 驗證的實際好處是主機上不再存放長期密碼、不需手動輪換、撤銷只需改 IAM;對主機被入侵的防護,和密碼驗證差異有限。
1. 摘要
WordPress(Redis Object Cache plugin +phpredis)原本以 RBAC 密碼連線 ElastiCache Serverless。本次改為IAM auth連線。
做法:由一支 PHP script 在本機用 AWS SDK for PHP 對 ElastiCache 做 SigV4 presign,產生短效 IAM token,當作密碼交給plugin 。token 有效 900 秒,快取最多 600 秒,每個 PHP 請求在快取過期時自動重新產生。php-fpm 用 APCu 快取;APCu 異常時才退回EBS 上的檔案快取,並留下檔案作為警示;CLI 不快取。
2. 環境與資源
| OS/PHP | Amazon Linux 2023,PHP 8.5.12(NTS,aarch64) |
| Redis client | phpredis 6.3.0 |
| WordPress plugin | Redis Object Cache 3.0.0(drop-in v3.0.0) |
| Cache | ElastiCache Serverless ,Valkey 9 |
| IAM ElastiCache user | iam-wordpress-user(on ~* +@all,Authentication Type iam) |
| SDK | /home/www/lib/aws.phar(AWS SDK for PHP v3) |
| token 產生器 | /home/www/lib/elasticache-iam-token.php |
| token 快取(php-fpm) | APCu(php8.5-pecl-apcu 5.1.28,apc.enabled=On,apc.shm_size=32M) |
| APCu 異常時的備援快取 | /home/www/lib/cache/(目錄 0700,檔案 0600)。正常情況下是空的,出現檔案即代表 php-fpm 的 APCu 不可用或寫入失敗 |
| CLI 的 token | 不快取、不寫檔案,每次執行直接產生(APCu 在 CLI 預設關閉) |
3. 方案選擇與依據
3.1 為什麼「token 當密碼」在 phpredis+plugin 可行
- ElastiCache IAM 驗證的運作方式是:把短效 token 當成
AUTH/HELLO的密碼送出。 - plugin 的
WP_REDIS_PASSWORD接受[username, password]。 - phpredis 6.3.0 的
redis.pconnect.echo_check_liveness預設為1。重用持久連線時,redis_sock_check_liveness()會先用目前的密碼重送AUTH,再送ECHO。因此每個請求都會用當下的 token 重新驗證。
3.2 ElastiCache IAM 驗證的限制(AWS 文件)
- 需要 Valkey 7.2 以上或 Redis OSS 7.0 以上(本案為 Valkey 9)。
- 必須啟用 in-transit encryption(TLS)。Serverless 預設啟用。
- IAM user 的 username 和 user-id 必須相同。
- token 有效 15 分鐘;用過期 token 重新驗證會被拒絕。
- IAM 驗證的連線 12 小時後會自動斷開;送出帶新 token 的
AUTH/HELLO可再延長 12 小時。 MULTI/EXEC或 Lua 腳本內不支援重新驗證。- cache 名稱建立時會轉成小寫,產生 token 時必須用小寫。
- Serverless 的 token 要多帶
ResourceType=ServerlessCache。
4. 前置條件檢查
4.1 確認 EC2 使用的 IAM 身分
aws sts get-caller-identity
{
"UserId": "<ROLE_UNIQUE_ID>:<INSTANCE_ID>",
"Account": "<ACCOUNT_ID>",
"Arn": "arn:aws:sts::<ACCOUNT_ID>:assumed-role/EC2ConnectElastiCacheRole/<INSTANCE_ID>"
}
一個 instance profile 只能掛一個 role,所以 elasticache:Connect 權限要加在 EC2ConnectElastiCacheRole 上。
4.2 確認 cache 和 user 狀態
aws elasticache describe-serverless-caches --region ap-east-2 \
--serverless-cache-name valkey-serverless \
--query 'ServerlessCaches[0].[Status,UserGroupId,Endpoint.Address]'
[
"available",
"default-valkey-group",
"valkey-serverless.serverless.ape2.cache.amazonaws.com"
]
aws elasticache describe-users --region ap-east-2 \
--query 'Users[].[UserId,UserName,Engine,AccessString,Authentication.Type]'
確認結果:
- cache 名稱為
valkey-serverless,Valkey 9。 - 帳號內已有其他 IAM 模式的user :
iam-valkey-user、default.iam-user、wordpress-limited。
5. 實施步驟
步驟 1:建立 IAM 模式的 ElastiCache user
aws elasticache create-user --region ap-east-2 \
--user-id iam-wordpress-user --user-name iam-wordpress-user \
--engine valkey --authentication-mode Type=iam \
--access-string "on ~* +@all"
步驟 2:把user 加入 serverless cache 的 user group
aws elasticache modify-user-group --region ap-east-2 \
--user-group-id <valkey-serverless 的 UserGroupId> \
--user-ids-to-add iam-wordpress-user
確認:
aws elasticache describe-user-groups --region ap-east-2 \
--user-group-id <UserGroupId> \
--query 'UserGroups[0].[Status,Engine,UserIds,PendingChanges]'
預期:Status 為 active,UserIds 包含 iam-wordpress-user,PendingChanges 為空。
步驟 3:給 instance role 加上 elasticache:Connect 權限
"Resource": "*",等於允許這台 EC2 以 IAM 連線帳號內所有 cache 和所有 IAM user 。改成最小權限,目前實際套用的 inline policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "elasticache:Connect",
"Resource": [
"arn:aws:elasticache:ap-east-2:<ACCOUNT_ID>:user:iam-wordpress-user",
"arn:aws:elasticache:ap-east-2:<ACCOUNT_ID>:serverlesscache:valkey-serverless"
]
}
]
}
IAM 是在每次 AUTH 時才評估權限,所以 policy 改完後,下一次 AUTH 就會套用新設定。驗證新 policy 時,要用新產生的 token 測一次正向案例,再用一個不在 policy 裡的 IAM user 測一次反向案例:
# 正向:iam-wordpress-user 應回 PONG
REDISCLI_AUTH="$(php -r 'require "/home/www/lib/elasticache-iam-token.php"; echo ec_iam_token("valkey-serverless","iam-wordpress-user","ap-east-2");')" \
valkey-cli -c --tls -h valkey-serverless.serverless.ape2.cache.amazonaws.com -p 6379 --user iam-wordpress-user PING
PONG
# 反向:不在 policy 裡的 iam-valkey-user 應被拒絕(前提是它也在這個 cache 的 user group 裡,否則被拒的原因是不在 group)
REDISCLI_AUTH="$(php -r 'require "/home/www/lib/elasticache-iam-token.php"; echo ec_iam_token("valkey-serverless","iam-valkey-user","ap-east-2");')" \
valkey-cli -c --tls -h valkey-serverless.serverless.ape2.cache.amazonaws.com -p 6379 --user iam-valkey-user PING
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
(error) NOAUTH Authentication required.
確認權限(需要有 IAM 權限的 profile):
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::<ACCOUNT_ID>:role/EC2ConnectElastiCacheRole \
--action-names elasticache:Connect \
--resource-arns arn:aws:elasticache:ap-east-2:<ACCOUNT_ID>:serverlesscache:valkey-serverless \
arn:aws:elasticache:ap-east-2:<ACCOUNT_ID>:user:iam-wordpress-user \
--query 'EvaluationResults[].[EvalResourceName,EvalDecision]'
[
[
"arn:aws:elasticache:${Region}:${Account}:${ResourceType}:${ResourceName}",
"allowed"
]
]
步驟 4:安裝 AWS SDK for PHP,請放在 webroot 以外。
sudo mkdir -p /home/www/lib && cd /home/www/lib
sudo curl -LO https://docs.aws.amazon.com/aws-sdk-php/v3/download/aws.phar
sudo chown -R www:www /home/www/lib
步驟 5:建立 token generator
/home/www/lib/elasticache-iam-token.php:
<?php
// ElastiCache IAM 驗證 token generator(SigV4 presign)
// - token 有效 900 秒(AWS 規定 15 分鐘)
// - php-fpm:正常情況快取在 APCu,最多 600 秒,且不超過 instance role 臨時憑證剩餘時間(保留 60 秒)
// - APCu 不可用或寫入失敗時:寫 error_log,並改用 /home/www/lib/cache/ 檔案快取
// => 這個目錄出現檔案,就代表 php-fpm 的 APCu 出過問題
// - CLI(APCu 預設關閉):不快取、不寫檔案,避免每次跑 wp-cli 都誤報
// - 只有需要產生 token 時才載入 AWS SDK
function ec_iam_token(string $cacheName, string $userId, string $region, bool $serverless = true): string
{
$key = 'ec_iam_token:' . $cacheName . ':' . $userId;
$maxTtl = 600;
$isCli = (PHP_SAPI === 'cli');
$useApcu = !$isCli && function_exists('apcu_enabled') && apcu_enabled();
$file = '/home/www/lib/cache/' . md5($key);
// 1) 讀快取:先 APCu;APCu 沒有時再看備援檔案(正常情況下檔案不存在)
if ($useApcu) {
$t = apcu_fetch($key, $ok);
if ($ok && $t !== '') {
return $t;
}
}
if (!$isCli && is_file($file) && filemtime($file) > time() - $maxTtl) {
$t = file_get_contents($file);
if ($t !== false && $t !== '') {
return $t;
}
}
// 2) 產生新 token
try {
require_once '/home/www/lib/aws.phar';
$query = 'Action=connect&User=' . rawurlencode($userId);
if ($serverless) {
$query .= '&ResourceType=ServerlessCache'; // serverless 必須帶這個參數
}
$req = new \GuzzleHttp\Psr7\Request('GET', 'http://' . strtolower($cacheName) . '/?' . $query);
$creds = \Aws\Credentials\CredentialProvider::defaultProvider()()->wait(); // EC2 instance role(IMDS)
$signed = (new \Aws\Signature\SignatureV4('elasticache', $region))->presign($req, $creds, '+900 seconds');
$token = preg_replace('#^http://#', '', (string) $signed->getUri());
} catch (\Throwable $e) {
error_log('ec_iam_token failed: ' . $e->getMessage());
return ''; // 失敗時回傳空字串,讓plugin 走 graceful 降級,不讓整站 fatal
}
if ($isCli) {
return $token;
}
// 3) 快取時間不超過臨時憑證剩餘時間,避免 token 提早失效卻還留在快取
$ttl = $maxTtl;
$exp = $creds->getExpiration();
if ($exp) {
$ttl = max(30, min($maxTtl, $exp - time() - 60));
}
if ($useApcu && apcu_store($key, $token, $ttl)) {
return $token;
}
// 4) APCu 不可用或寫入失敗:記錄原因,改用檔案快取(檔案出現即為異常訊號)
error_log(sprintf(
'ec_iam_token: APCu %s, fallback to file cache %s',
$useApcu ? 'store failed' : 'unavailable',
$file
));
@mkdir(dirname($file), 0700, true);
$tmp = $file . '.' . getmypid();
if (file_put_contents($tmp, $token, LOCK_EX) !== false) {
chmod($tmp, 0600);
rename($tmp, $file);
touch($file, time() - ($maxTtl - $ttl)); // 讓檔案快取在 $ttl 秒後過期
}
return $token;
}
php-fpm 正常時只用 APCu;APCu 不可用或寫入失敗時,會寫 error_log 並改用 /home/www/lib/cache/ 檔案快取,所以這個目錄出現檔案,就是 APCu 出問題的訊號。CLI 不快取、不寫檔案。
chown www:www /home/www/lib/elasticache-iam-token.php
php -l /home/www/lib/elasticache-iam-token.php
sudo systemctl reload php-fpm # 清空 APCu
token 格式:對 http://<cache名稱>/?Action=connect&User=<userId>&ResourceType=ServerlessCache 做 SigV4 presign(服務名 elasticache,有效 900 秒),再去掉開頭的 http://。
步驟 6:用 CLI 單獨驗證 token
TOKEN=$(php -r 'require "/home/www/lib/elasticache-iam-token.php"; echo ec_iam_token("valkey-serverless","iam-wordpress-user","ap-east-2");')
echo "${TOKEN:0:80}..."
REDISCLI_AUTH="$TOKEN" valkey-cli -c --tls \
-h valkey-serverless.serverless.ape2.cache.amazonaws.com -p 6379 \
--user iam-wordpress-user PING
valkey-serverless/?Action=connect&User=iam-wordpress-user&ResourceType=Serverles...
PONG
檢查 token 結構時,請mask簽章和 session token:
echo "$TOKEN" | tr '&' '\n' | sed -E 's/(X-Amz-Security-Token=|X-Amz-Signature=).*/\1***/'
output
valkey-serverless/?Action=connect
User=iam-wordpress-user
ResourceType=ServerlessCache
X-Amz-Security-Token=***
X-Amz-Algorithm=AWS4-HMAC-SHA256
X-Amz-Credential=ASIAZQ3DUCTPN6PU5GGG%2F20261023%2Fap-east-2%2Felasticache%2Faws4_request
X-Amz-Date=20261023T063005Z
X-Amz-SignedHeaders=host
X-Amz-Expires=900
X-Amz-Signature=***
步驟 7:修改 wp-config.php
/**wp redis object cache*/
define( 'WP_REDIS_CLIENT', 'phpredis' );
define( 'WP_REDIS_PERSISTENT', true );
define( 'WP_REDIS_IGBINARY', true );
define('WP_REDIS_CLUSTER', [
'tls://valkey-serverless.serverless.ape2.cache.amazonaws.com:6379',
]);
require_once '/home/www/lib/elasticache-iam-token.php';
define('WP_REDIS_PASSWORD', ['iam-wordpress-user', ec_iam_token('valkey-serverless', 'iam-wordpress-user', 'ap-east-2')]);
define('WP_REDIS_SSL_CONTEXT', [
'verify_peer' => true,
'verify_peer_name' => true,
]);
步驟 8:啟用 APCu,檔案快取改為異常備援和警示
背景:token generator 在沒有 APCu 時,會把 token 寫到 /home/www/lib/cache/
OPcache 只快取 PHP 腳本編譯後的 bytecode,沒有讓程式存取任意資料的 API,不能用來存 token。要在 php-fpm 的 worker 之間共用資料,要用 APCu。
sudo dnf install -y php8.5-pecl-apcu
sudo systemctl restart php-fpm
php-fpm -m | grep -i apcu
php-fpm -i | grep -E '^apc\.(enabled|shm_size)'
實際輸出:
Installed:
php8.5-pecl-apcu-5.1.28-1.amzn2023.0.1.aarch64
apcu
apc.enabled => On => On
apc.shm_size => 32M => 32M
APCu 的特性:
- 快取存在 php-fpm master 的共享記憶體,同一個 master 底下的 worker 共用。
- 重啟或 reload php-fpm 會清空,下一個請求會自動重新產生 token。
- token 只有幾 KB,32M 綽綽有餘。
實際記憶體用量:
# pmap -x $(pgrep -o php-fpm) | awk '$2==32768'
0000ffff732d1000 32768 136 136 rw-s- apc.9yud3J (deleted)
# php-fpm -i | grep -E '^opcache\.memory_consumption'
opcache.memory_consumption => 128 => 128
- APCu 的共享記憶體區段大小 32768 KB,但 RSS 只有 136 KB。
apc.shm_size只是預留的虛擬位址空間,實際佔用的 RAM 取決於存了多少資料,所以維持 32M,不調整。 - 同一個 php-fpm master 裡,128M 的
zero (deleted)區段是 OPcache(opcache.memory_consumption=128),RSS 約 2.7 MB。
6. token 更新機制
沒有背景程式或排程在更新 token,更新是在請求中「需要時才做」:
- 每個 PHP 請求載入
wp-config.php時,都會呼叫一次ec_iam_token()。 - APCu 快取未過期:直接回傳快取的 token,不呼叫 AWS。
- APCu 快取過期(最多 600 秒,或憑證快到期時更短),或在 CLI 執行:載入 SDK,向 IMDS 取得 instance role 憑證,在本機簽出新 token;php-fpm 會寫回 APCu。
- phpredis 取得這個 token:
- 新連線:用它送
AUTH。 - 重用持久連線:liveness 檢查用它重送
AUTH〔已驗證,原始碼〕。
- 新連線:用它送
token 有效 900 秒,快取最多 600 秒,所以每個請求拿到的 token 至少還有約 5 分鐘有效。每個請求都重送 AUTH,推斷也會一直延長 IAM 連線 12 小時的斷線期限。
7. 驗證結果
wp redis status :
Status: Connected
Client: PhpRedis (v6.3.0)
Drop-in: Valid
Ping: 1
Errors: []
PHP Version: 8.5.12
Plugin Version: 3.0.0
Redis Version: 7.2
Global Prefix: "wp_"
Blog Prefix: "wp_"
Timeout: 1
Read Timeout: 1
Persistent: 1
WP_REDIS_CLIENT: "phpredis"
WP_REDIS_SSL_CONTEXT: {"verify_peer": true}
WP_REDIS_PERSISTENT: true
WP_REDIS_CLUSTER: ["tls://valkey-serverless.serverless.ape2.cache.amazonaws.com:6379"]
WP_REDIS_IGBINARY: true
WP_REDIS_PLUGIN_PATH: "/home/www/bbken.org/wp-content/plugins/redis-cache"
WP_REDIS_PASSWORD: ["iam-wordpress-user","••••••••"]
判讀:
- IAM 驗證成功,使用持久連線。
Redis Version: 7.2: Valkey 為了相容性,把INFO的redis_version固定在 7.2;



