没有不值得去解决的问题,也没有不值得去学习的技术!

Tencent Cloud EdgeOne API Automation: Configure a Least-Privilege API Key From a CAM Sub-User

Figure 9: Illustrating the console semantics that the automation ultimately needs to reproduce

作者:

Recently, I have been continuing to refine the Production automation for the multilingual A Tour of Go project.

Previously, after every new release for the Chinese site, I had to log into the Tencent Cloud EdgeOne console and manually trigger a cache purge:

Plaintext
Content type: Hostname
Purge method: Direct delete

With only a single site, this was not much of a hassle.

However, as the project expanded to more languages, I progressively automated workflows like Cloudflare, Production deploy, machine acceptance, and browser acceptance. At this point, still having to manually open the EdgeOne console to purge the cache for the Chinese site became an obvious manual bottleneck in the entire release process.

So this time, I decided to bring EdgeOne cache purging into the Production automation as well.

Once I actually started, I realized it was not as simple as “applying for an API Key.” It also involved:

  • Whether I should use the primary account API Key;
  • How to create a dedicated CAM sub-user;
  • How to restrict permissions to the minimum required EdgeOne APIs;
  • Why I never saw the SecretKey when the first key was created;
  • Why the SecretKey cannot be queried afterward;
  • How to securely store the SecretId / SecretKey;
  • How to perform read-only API validation before actually executing a cache purge;
  • Why mock tests might all pass, yet the real API could still expose a misunderstanding of the fields.

This article documents the entire process.


1. Why not just use the primary account API Key

Initially, I did consider the simplest approach:

Directly create a set of API Keys for the Tencent Cloud primary account.

This would involve the fewest steps and eliminate the need to set up additional CAM users and permission policies.

But upon entering the Tencent Cloud API key management page, the console itself displayed a prominent security warning: primary account keys possess high-level operational permissions over cloud resources and are not recommended for direct programmatic access.

Figure 1: Tencent Cloud explicitly warns that the primary account API key has full control over all cloud resources and recommends using a sub-user key instead
Figure 1: Tencent Cloud explicitly warns that the primary account API key has full control over all cloud resources and recommends using a sub-user key instead

For my needs, the Production program actually only requires:

  1. Querying EdgeOne zones;
  2. Querying specific acceleration domains;
  3. Creating a cache purge task;
  4. Querying the status of the cache purge task.

There is clearly no need to grant the automation program permissions over the entire Tencent Cloud account.

Therefore, I finally decided:

To create a dedicated CAM sub-user specifically for go-tour-i18n Production.


2. Creating a dedicated CAM sub-user

After logging into Tencent Cloud CAM, I created a new sub-user.

The username I used was:

Plaintext
go-tour-i18n-production

Its purpose is also very specific:

Plaintext
go-tour-i18n EdgeOne Production automation

For the creation method, I selected:

Plaintext
Access resources and receive messages
Figure 2: Creating a dedicated CAM sub-user for Production automation instead of reusing the primary account
Figure 2: Creating a dedicated CAM sub-user for Production automation instead of reusing the primary account

This user only needs to make programmatic API calls, so I only enabled:

Plaintext
Programmatic access

It does not need the ability to log in to the daily Tencent Cloud console.

The benefit of this is that the permission boundary is very clear:

Plaintext
Tencent Cloud primary account

CAM sub-user
go-tour-i18n-production

Only used for go-tour-i18n Production automation

Even if I need to rotate the API Key in the future, I only have to deal with this dedicated user, leaving the primary account unaffected.


3. Granting only the 4 permissions actually needed for EdgeOne automation

Next was the most important step: customizing the CAM policy.

For the service, I selected:

Plaintext
Edge Security and Acceleration Platform EO (teo)

Ultimately, I only authorized 4 API Actions:

Plaintext
DescribeZones
DescribeAccelerationDomains
CreatePurgeTask
DescribePurgeTasks
Figure 3: The resource scope is set to "All resources", but only these 4 Actions are retained; the code then binds the official production hostname
Figure 3: The resource scope is set to “All resources”, but only these 4 Actions are retained; the code then binds the official production hostname

The responsibilities each of them bears are very clear.

1. DescribeZones

Query the EdgeOne Zone based on the official site name.

The automation program does not hardcode the ZoneId; instead, it first uses:

Plaintext
shuijingwanwq.com

to dynamically resolve the official Zone.

2. DescribeAccelerationDomains

After obtaining the Zone, it confirms that the Production hostname to be operated actually belongs to this Zone and is currently in a normal online state.

For example, the Chinese site:

Plaintext
go-dev.shuijingwanwq.com

3. CreatePurgeTask

Actually creates the cache purge task.

My original manual operation was:

Plaintext
Hostname
+
Direct delete

Therefore, the API automation must also strictly maintain the same semantics:

Plaintext
Type = purge_host
Method = delete
Targets = [official Production hostname]

4. DescribePurgeTasks

CreatePurgeTask does not mean the purge work is finally complete.

The program still needs to query the task status based on the returned JobId until it confirms:

Plaintext
success

Only then can the CDN purge be considered passed.


4. Why the resource scope was still set to “All resources”

A CAM policy can be made even more granular, for example, by restricting it to a specific EdgeOne resource.

However, this is my own personal project with only a few sites at present, and the allowed API Actions have already been reduced to 4, so I chose:

Plaintext
All resources

without adding more complex resource-level QCS restrictions.

This does not mean the program can arbitrarily purge any domain.

go-tour-i18n itself has a second layer of restriction:

the hostname can only be read from the official production/identity.json.

That is, the script does not provide an interface like this:

Plaintext
--hostname arbitrary-example.com

Nor can it execute:

Plaintext
purge_all
*
wildcard
entire Zone

Therefore, the current security boundary is actually:

Plaintext
CAM:
Only 4 EdgeOne APIs allowed

            +

go-tour-i18n:
Only official hostnames in production/identity.json allowed

For this personal project at its current stage, I think this level of complexity is appropriate.


5. Associating the policy with the Production sub-user

After creating the policy, I associated it with:

Plaintext
go-tour-i18n-production
Figure 4: Associating the GoTourI18nEdgeOneMaintenance policy with go-tour-i18n-production
Figure 4: Associating the GoTourI18nEdgeOneMaintenance policy with go-tour-i18n-production

The policy name I used is:

Plaintext
GoTourI18nEdgeOneMaintenance

The policy was ultimately created and associated successfully.

Figure 5: Milestone result, policy created and associated successfully
Figure 5: Milestone result, policy created and associated successfully

At this point, the CAM permissions part was complete.

The permission structure can be simplified as:

Plaintext
go-tour-i18n-production
└── GoTourI18nEdgeOneMaintenance
    ├── DescribeZones
    ├── DescribeAccelerationDomains
    ├── CreatePurgeTask
    └── DescribePurgeTasks

6. First pitfall: when the first key was created, I never saw the SecretKey

Next, I entered the CAM sub-user’s:

Plaintext
API Keys

This was the strangest part of the operation.

When creating the go-tour-i18n-production sub-user, I had already checked:

Plaintext
Programmatic access

After the sub-user was created, entering the “API Keys” page showed that the system already had an enabled:

Plaintext
SecretId

This means the key had actually been created.

But the problem was:

In my memory, there was never a page in the preceding sub-user creation process that allowed me to view or save the SecretKey.

This is not the same as “I saw the SecretKey but forgot to save it.”

At least in my actual operation this time, the experience the process gave me was:

Plaintext
Create CAM sub-user
→ Enable programmatic access
→ Creation complete
→ SecretId already appears on the API Keys page

But in between, I did not notice any step that displayed the full SecretKey.

Later, when I tried to view the SecretKey, Tencent Cloud popped up a prompt:

The SecretKey is only provided when the key is created and cannot be queried again afterward.

Figure 6: Prompt indicating that the SecretKey cannot be queried again after creation
Figure 6: Prompt indicating that the SecretKey cannot be queried again after creation

This led to a somewhat awkward state:

Plaintext
SecretId already exists
SecretKey cannot be queried later
and I never saw the SecretKey during the creation process

From a user experience perspective, I am more inclined to believe this was a UI or flow issue in the console creation process at the time.

Of course, I cannot confirm whether it was a Tencent Cloud frontend bug, an omitted page redirect, or if a very inconspicuous display step was skipped.

But what is certain is:

After the first key was created, I did not have its SecretKey, and I could not query it again later.

Therefore, this key was effectively unusable for my Production automation.


7. Creating a new API Key again, and this time actually seeing the SecretKey

Since the old key’s SecretKey could no longer be retrieved, I did not keep struggling with it and directly clicked:

Plaintext
New Key

This time, the behavior was very clear.

Tencent Cloud popped up the “Create SecretKey” window and simultaneously displayed:

Plaintext
SecretId
SecretKey
Figure 7: Recreating the API key, the page only displays SecretId / SecretKey during creation
Figure 7: Recreating the API key, the page only displays SecretId / SecretKey during creation

And the page prompted again:

The newly created key only provides the SecretKey during creation and cannot be queried again later. Please save the SecretKey properly.

This time, I immediately saved the full SecretId / SecretKey.

So, based on my actual experience, the more accurate lesson is not:

“The first time, I forgot to save the SecretKey.”

Rather, it should be:

If the page clearly displays the SecretKey after creating an API Key, be sure to save it immediately; if, like my first time, you only see the SecretId after creation and never go through a SecretKey display step, do not expect to be able to view it again later. It is more efficient to just create a new key.

In particular, do not send:

Plaintext
SecretId
SecretKey

to chat tools, Git repositories, or regular documents.

The truly sensitive part is the SecretKey, which should only go into a controlled Production secret file.


8. How to store EdgeOne API credentials on the server

On the Production server, I ultimately used:

Plaintext
/etc/go-tour/edgeone.env

The content structure is only two lines:

Bash
TENCENTCLOUD_SECRET_ID=<SecretId>
TENCENTCLOUD_SECRET_KEY=<SecretKey>

The file permissions are set to:

Plaintext
root:root
0600

The actual check result:

Plaintext
root:root 600 /etc/go-tour/edgeone.env

I also ran a structural check that does not output the real values, ultimately yielding:

Plaintext
EDGEONE SECRET CONTRACT: PASS

One important point here is:

Plaintext
Do not cat /etc/go-tour/edgeone.env

At least there is no need to print the real SecretKey to the terminal output just to “confirm the configuration”.

The production program only needs to confirm:

  • The file is a regular file;
  • owner is root;
  • group is root;
  • mode is 0600;
  • Only the two specified variables exist;
  • Both values are non-empty.

9. Second pitfall: do not paste interactive read and subsequent commands all at once

While configuring the server secret, I also hit an interesting little pitfall.

To prevent the SecretKey from entering the shell history, I used:

Bash
read -r -s -p 'TENCENTCLOUD_SECRET_KEY: ' ...

The approach itself was fine.

The problem was that I pasted the multi-line command containing read into the terminal all at once.

As a result, the shell’s subsequent:

Plaintext
printf
umask
install
rm
stat

commands were treated as standard input by read.

The generated:

Plaintext
/etc/go-tour/edgeone.env

actually had 19 lines.

The automation preflight failed quickly:

Plaintext
[production-cdn] FAILED: secret file contains an invalid assignment

Continuing to inspect it showed:

Plaintext
lines = 19
contains_cr = True

The file even contained:

Plaintext
umask 077
tmp=...
install ...
rm ...

These were shell commands that should have been executed.

Finally, I separated the operations:

Plaintext
First execute read
→ Enter SecretId separately

Then execute read -s
→ Enter SecretKey separately

Finally execute the non-interactive file writing command

Only then was the problem solved.

This pitfall is actually unrelated to Tencent Cloud, but whenever using interactive:

Plaintext
read
password prompt
secret prompt

it is worth noting:

Do not paste commands that require manual input and subsequent commands all at once.


10. Perform a read-only API preflight first, instead of purging the cache directly

After the credentials were configured, I did not immediately execute:

Plaintext
CreatePurgeTask

Instead, I first added a read-only validation for Production:

Bash
scripts/verify-edgeone-authority.sh zh-CN

It only allows calling:

Plaintext
DescribeZones
DescribeAccelerationDomains

It absolutely will not call:

Plaintext
CreatePurgeTask

This way, even if the API client is written incorrectly, it will not affect the real CDN cache.

As it turned out, this step was very necessary.


11. Third pitfall: mock tests all green, but the real API still fails

When running the real preflight for the first time, I got:

Plaintext
[production-cdn] FAILED:
EdgeOne zone identity/name/status is not exact and active

The secret file was correct, and the API permissions did not report AccessDenied.

Continuing to check the code, I found that my custom EdgeOne client incorrectly assumed the Zone response structure was:

Plaintext
Name
Id
Status

The code was similar to:

Python
zone.get("Name")
zone.get("Id")
zone.get("Status") == "active"

But the real DescribeZones returned by Tencent Cloud is:

Plaintext
ZoneName
ZoneId
Type
Status
CnameStatus
ActiveStatus
LockStatus
Paused

This is no longer a simple field spelling issue.

What is more troublesome is that:

Plaintext
Status=active

cannot serve as a unified availability check for all site types.

My EdgeOne site uses:

Plaintext
Type=partial

which means CNAME access.

In this case:

Plaintext
Status=pending

only means the NS has not been switched, and does not mean the CNAME access site is unavailable.

Therefore, I finally adjusted the Zone validation to be type-aware.

The common requirements are:

Plaintext
ZoneName exact match
ZoneId non-empty
ActiveStatus=active
Paused=false
LockStatus=enable

If:

Plaintext
Type=partial

then it requires:

Plaintext
CnameStatus=finished
Status can be pending / active

If:

Plaintext
Type=full

then it requires:

Plaintext
Status=active

Unknown Type:

Plaintext
fail closed

12. Why this error was not exposed until the real API

What is more worth reflecting on is:

Before running the real API, all automated tests actually PASSED.

The reason is simple.

The mock fixtures at the time were also built based on the incorrect understanding:

JSON
{
  "Name": "shuijingwanwq.com",
  "Id": "zone-test",
  "Status": "active"
}

This created a very typical problem:

Plaintext
Wrong specification

Wrong implementation

Wrong mock

Implementation matches mock exactly

Tests all green

The tests could only prove:

that the code conformed to its own defined assumptions.

But they could not prove:

that these assumptions matched the real third-party API.

This time, it reaffirmed an important principle for me:

For third-party APIs, the request fields, response fields, enums, status semantics, and pagination contracts should be used to build test fixtures directly from the real official schema, and must not be inferred from interface naming conventions.

Especially for Production mutation-related APIs, a read-only real API preflight should be arranged before the actual mutation:

Plaintext
read-only real API preflight

13. After the fix, the real EdgeOne authority validation passes

After fixing the Zone contract, I ran again:

Bash
scripts/verify-edgeone-authority.sh zh-CN

Finally getting:

Plaintext
EDGEONE AUTHORITY PREFLIGHT: PASS
zone_name: shuijingwanwq.com
hostname: go-dev.shuijingwanwq.com

This means the entire real chain below has been validated:

Plaintext
root-only Secret

TC3-HMAC-SHA256

DescribeZones

Exact resolution of official Zone

DescribeAccelerationDomains

Exact validation of Production hostname

PASS

No cache purge was executed during the entire validation process.

This is exactly the effect I wanted:

Prove that the permissions, identity resolution, and API client are all correct first, before allowing the actual Production mutation.


14. The final automated cache purge remains consistent with manual console operations

My original manual operation in the EdgeOne console was:

Plaintext
Content type: Hostname
Purge method: Direct delete
Figure 9: Illustrating the console semantics that the automation ultimately needs to reproduce
Figure 9: Illustrating the console semantics that the automation ultimately needs to reproduce

So the API request semantics that the automation ultimately generates are also fixed as:

JSON
{
  "Type": "purge_host",
  "Method": "delete",
  "Targets": [
    "<正式 Production hostname>"
  ]
}

This does not expose:

Plaintext
purge_all
wildcard
arbitrary hostname
multiple Targets
zone-wide purge

The target hostname can only come from:

Plaintext
production/identity.json

For example:

Plaintext
go-dev.shuijingwanwq.com

15. CreatePurgeTask does not immediately mean success

There is another easily overlooked issue in cache purge automation:

Plaintext
CreatePurgeTask

A successful call only means the task has been received by EdgeOne.

The formal Production workflow will continue based on the:

Plaintext
JobId

to call:

Plaintext
DescribePurgeTasks

The status processing rules are similar to:

Plaintext
processing
→ continue waiting

success
→ PASS

failed
timeout
canceled
unknown
→ FAIL

That is:

A successful API request response does not equal the completion of the Production cache purge.


16. When the network result is uncertain, do not blindly retry the cache purge

There is an even more troublesome scenario:

Plaintext
CreatePurgeTask

After the request is sent, a local:

  • timeout;
  • connection interruption;
  • incomplete response body.

At this point, it is impossible to determine:

whether EdgeOne actually received and created the task?

The most dangerous approach is:

Plaintext
Timeout
→ immediately CreatePurgeTask again

Because this may create duplicate mutations.

The current approach is to first use:

Plaintext
DescribePurgeTasks

to query recent tasks, and based on:

Plaintext
ZoneId
Type=purge_host
Target=official hostname
CreateTime

attempt reconciliation.

Only when it can be clearly proven that the first task was not created is a limited retry allowed.

If it remains impossible to confirm:

Plaintext
fail closed

rather than pretending it succeeded.


17. After the new API Key is successfully validated, delete the first unusable key

After the newly created API Key completed the real read-only preflight, I returned to Tencent Cloud CAM.

I finally deleted the first key that was no longer usable.

Here I emphasize again:

The first key was not a case of me seeing the SecretKey and forgetting to save it, but rather that during the sub-user creation, I never found the SecretKey display page at all.

Therefore, that key only had a visible SecretId left and was of no practical use to my automation.

After deletion, go-tour-i18n-production ultimately retains only one officially active API Key.

Figure 8: Finally retaining only one enabled new API Key
Figure 8: Finally retaining only one enabled new API Key

This leaves the credential state quite clean:

Plaintext
1 Production CAM sub-user
1 least-privilege policy
1 valid API Key
1 root-only secret file

18. Final structure

After organizing, this EdgeOne Production authority can be summarized as:

Plaintext
Tencent Cloud primary account

CAM sub-user
go-tour-i18n-production

Custom policy
GoTourI18nEdgeOneMaintenance

4 API Actions

API Key

/etc/go-tour/edgeone.env
root:root 0600

read-only Production preflight

exact hostname purge

The program itself continues to restrict:

Plaintext
Zone
from official production authority

hostname
from production/identity.json

Type
fixed to purge_host

Method
fixed to delete

Targets
fixed to a single official hostname

19. The biggest takeaway this time is not “getting an API Key”

Initially, I just wanted to solve a very specific problem:

I did not want to manually enter the Tencent Cloud EdgeOne console and click cache purge every time the Chinese site went live.

But after actually finishing it, I feel the more valuable part was clarifying the entire permission and mutation boundary.

A few practical lessons are especially worth keeping.

Do not use the primary account API Key just to save a few steps

A dedicated CAM sub-user and least-privilege policy take a few more minutes to configure, but are clearly safer for long-term maintenance.

If the page displays the SecretKey, save it immediately

The SecretKey cannot be queried again later.

But my first key this time was more special:

After the sub-user was created, a SecretId already existed, but I never found the SecretKey display page at all.

So if you encounter a similar situation, do not assume you can still query the SecretKey later.

The most hassle-free approach is usually:

Plaintext
Create a new API Key
→ Confirm the page clearly displays SecretId + SecretKey
→ Save it immediately
→ Validate the new Key
→ Delete the old Key

As for why the SecretKey did not appear the first time, I am now more inclined to believe it was an issue with the Tencent Cloud console flow or UI at the time, but I do not have enough evidence to confirm the specific cause.

Secrets should not go into Git, logs, or regular terminal output

On the Production server, only a root-only 0600 file is kept.

Third-party API mocks must come from the real schema

Otherwise, it is easy to end up with:

Plaintext
Wrong implementation
Wrong test data
Resulting in all tests passing

It is best to have a real read-only preflight before a Production mutation

This time, without the real validation from:

Plaintext
DescribeZones
+
DescribeAccelerationDomains

the Zone schema error might not have been exposed until the actual cache purge.

Unknown network results and explicit failures are not the same thing

An explicit failure can be safely retried.

When the result is unknown, you should reconcile first, rather than directly repeating the mutation.


Conclusion

After completing the EdgeOne API authority, the CDN cache purge for the Chinese site could finally be incorporated into the go-tour-i18n Production automation.

This step itself is just a small part of optimizing the entire process.

What actually prompted me to start doing this was an official upstream sync for A Tour of Go: when the same change needed to be published to 10 language sites, I found that manual operations that were still acceptable for a single site turned into a massive amount of repetitive work when scaled across multiple locales.

Later, I continued to turn:

Plaintext
publish
CDN purge
shared-assets purge
Production maintenance
machine acceptance
browser acceptance

into a batched process step by step.

Now, the daily multilingual Production release can be completed serially via a single top-level batch command.

I plan to organize this part of the process into a separate next article.

系列导航

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan