Cloud WAF/Docs 中文 EN
Cloud WAF is an enterprise Web protection management platform. Go + Gin REST API backend, Vue 3 SPA frontend, with the zcloud CLI for automation.

This document belongs to Cloud WAF — Enterprise Web Protection Management Platform
CLI tool: zcloud · 5 modules: guard / sys / analytics / cli_release / auth
Full API index: /api/openapi.json · Sitemap: /sitemap.xml · AI Quick Read: /llms.txt


Authentication

Overview

Cloud WAF supports two authentication methods. Every protected endpoint requires Authorization, and each request must pick exactly one channel:

Scenario Header Description
Human login / Web Console / interactive CLI login Authorization: Bearer <token> The token is issued by POST /api/auth/login and is valid for several hours (subject to your security policy)
Scripts / CI / third-party integrations Authorization: ApiKey zck_<prefix>.<secret> API Key is a machine credential; plaintext is returned only once at issuance and is suitable for long-running automation

Recommendation: use Bearer sessions for human operations, and use API Keys for machine-to-machine integrations. The legacy “username/password → short-lived token → 401 auto-relogin” pattern still works, but should be treated as a compatibility fallback.

API Key Machine Credential

An API Key is a machine credential issued for scripts, CI, and third-party systems. It represents a specific user under a specific OEM and is constrained by two boundaries:

  1. the issuing user's current RBAC permissions;
  2. the API Key's own scopes list.

The effective permission set is: effective_perms = user.RBAC ∩ key.scope. scope can only narrow permissions, not expand them. If scopes is empty, the key inherits the issuing user's current RBAC fully.

Issue an API Key

curl -sS -X POST https://waf.example.com/api/sys/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "prod-integration",
        "scopes": ["guard.domain.list", "guard.domain.view"],
        "expires_in_days": 90
      }'

The data.api_key field is the full plaintext key:

{
  "code": 0,
  "message": "ok",
  "data": {
    "key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
    "api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
    "prefix": "zck_abc12345",
    "last4": "5t0",
    "expires_at": 1732982400000
  }
}

⚠ The plaintext api_key is returned only once. Later list responses show only prefix and last4; the full secret cannot be recovered. If lost, revoke it and issue a new one.

Call APIs with an API Key

curl -sS https://waf.example.com/api/guard/domains \
  -H "Authorization: ApiKey zck_abc12345.A1b2C3d4..." \
  -H 'Accept-Language: en-US'

API Key vs Bearer token

Dimension Bearer token API Key
Primary use Short-lived session after human login Long-running calls from machines, scripts, CI, third-party systems
How to obtain POST /api/auth/login POST /api/sys/api-keys
Header Authorization: Bearer <token> Authorization: ApiKey zck_<prefix>.<secret>
Lifetime Usually several hours, governed by auto-logout policy Default 90 days, max 365 days, revocable
Permission boundary Current user RBAC Current user RBAC ∩ Key scopes
Plaintext storage Client stores the token Returned only once; server never stores plaintext

Scope matching rules (important)

API Key scopes are deliberately separated from user-role RBAC matching semantics. Read this before issuing keys:

Security guidance

Login Flow (text diagram)

┌─────────────┐                                  ┌────────────────┐
│ Client / CLI│ ──── POST /api/auth/login ───▶ │  Cloud WAF API │
└─────────────┘    {username, password}          └────────────────┘
       │                                                  │
       │ ◀────── 200 OK {data: {token, user}} ───────────│
       │                                                  │
       │ ──── GET /api/sys/users (Bearer token) ────────▶│
       │ ◀────── 200 OK {code:0, data:[...]} ────────────│
       │                                                  │
       │ ──── (token expired) GET /api/... ─────────────▶│
       │ ◀────── 401 Unauthorized ───────────────────────│
       │                                                  │
       │ ──── POST /api/auth/login (re-login) ──────────▶│
       │ ◀────── 200 OK {data: {token: <new>}} ──────────│

Full curl Walkthrough

1) Log in

curl -sS -X POST https://waf.example.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{
        "username": "admin",
        "password": "YOUR_PASSWORD"
      }'

Response:

{
  "code": 0,
  "message": "ok",
  "data": {
    "token": "eyJhbGciOi...",
    "user": { "uuid": "abc-123", "username": "admin", "role_id": 1 }
  }
}

2) Call an endpoint with the token

TOKEN='eyJhbGciOi...'
curl -sS https://waf.example.com/api/sys/users \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Accept-Language: en-US'

3) Log out

curl -sS -X POST https://waf.example.com/api/auth/logout \
  -H "Authorization: Bearer $TOKEN"

The token is invalidated immediately; subsequent use yields 401.

CLI Multi-Profile Mechanism

The CLI persists credentials in ~/.zcloud/credentials.toml:

active = "default"

[profiles.default]
api_url = "https://waf.example.com"
username = "admin"
token = "eyJhbGciOi..."
oem_id = "default"

[profiles.prod]
api_url = "https://prod.example.com"
username = "ops"
token = "eyJhbGciOi..."

Switch profiles:

zcloud config profiles list
zcloud config profiles activate prod

# Note: profiles create accepts only the positional <name>, no flags
zcloud config profiles create staging
zcloud --profile staging config set api_url https://staging.example.com

zcloud auth login updates the active profile's token; profiles are isolated.

401 Auto-Retry Pattern

Token expiration is normal. Wrap your client to "401 → relogin → retry".

Python pseudo-code

import requests

class CloudWAF:
    def __init__(self, base_url, username, password):
        self.base = base_url.rstrip('/')
        self.username = username
        self.password = password
        self.token = None
        self._login()

    def _login(self):
        r = requests.post(f"{self.base}/api/auth/login", json={
            "username": self.username,
            "password": self.password,
        })
        r.raise_for_status()
        self.token = r.json()["data"]["token"]

    def call(self, method, path, **kwargs):
        headers = kwargs.pop("headers", {})
        headers["Authorization"] = f"Bearer {self.token}"
        url = f"{self.base}{path}"
        r = requests.request(method, url, headers=headers, **kwargs)
        if r.status_code == 401:
            self._login()  # relogin
            headers["Authorization"] = f"Bearer {self.token}"
            r = requests.request(method, url, headers=headers, **kwargs)
        r.raise_for_status()
        return r.json()

Go pseudo-code

type Client struct {
    BaseURL  string
    User     string
    Password string
    Token    string
    HTTP     *http.Client
}

func (c *Client) login() error {
    body, _ := json.Marshal(map[string]string{"username": c.User, "password": c.Password})
    resp, err := c.HTTP.Post(c.BaseURL+"/api/auth/login", "application/json", bytes.NewReader(body))
    if err != nil { return err }
    defer resp.Body.Close()
    var out struct{ Data struct{ Token string } }
    json.NewDecoder(resp.Body).Decode(&out)
    c.Token = out.Data.Token
    return nil
}

func (c *Client) Do(req *http.Request) (*http.Response, error) {
    req.Header.Set("Authorization", "Bearer "+c.Token)
    resp, err := c.HTTP.Do(req)
    if err != nil { return nil, err }
    if resp.StatusCode == 401 {
        resp.Body.Close()
        if err := c.login(); err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+c.Token)
        return c.HTTP.Do(req)
    }
    return resp, nil
}

Machine Integration Pattern (compatibility fallback)

Machine integrations should use API Keys by default. Use the “username/password → short-lived token → 401 auto-relogin” pattern only when API Keys cannot be issued yet or when keeping an old pipeline compatible.

zcloud auth login is an interactive command (it reads the password through util.ReadPassword without echo) and does not accept --username/--password flags. CI pipelines and third-party systems should call the login REST API directly to obtain a token, then pass it to the CLI through the ZCLOUD_TOKEN environment variable.

Recommended approach:

  1. Evaluate API Key first: if the job only calls REST APIs, issue an API Key directly; continue below only when an old CLI flow must be reused
  2. Create a dedicated account: in the console, create a ci-bot user with the minimum required permissions
  3. Store credentials in secrets: CLOUDWAF_USERNAME / CLOUDWAF_PASSWORD go in GitHub Actions / GitLab CI secrets
  4. Exchange credentials for a token at job start: call POST /api/auth/login with the password base64-encoded
  5. Subsequent CLI commands read ZCLOUD_TOKEN — no need to write the local credentials file
  6. Always wrap calls with the 401 retry pattern: long jobs may outlive a token; on 401, refresh by re-running the login REST call
# Gitea Actions example
jobs:
  publish-cert:
    steps:
      - run: |
          curl -fsSL ${{ vars.WAF_URL }}/api/cli/install.sh | sh
          zcloud config set api_url ${{ vars.WAF_URL }}

          # 1) Exchange credentials for a token via REST (password base64-encoded)
          PWD_B64=$(printf '%s' "${{ secrets.WAF_PASS }}" | base64 -w0)
          TOKEN=$(curl -fsS "${{ vars.WAF_URL }}/api/auth/login" \
            -H 'Content-Type: application/json' \
            -d "{\"username\":\"${{ secrets.WAF_USER }}\",\"password\":\"$PWD_B64\"}" \
            | jq -r '.data.token')

          # 2) Export ZCLOUD_TOKEN; the CLI picks it up automatically
          export ZCLOUD_TOKEN="$TOKEN"
          zcloud guard certs upload --name star --cert ./fullchain.pem --key ./private.key

          # 3) Revoke the token when the job ends
          zcloud auth logout

Security Guidance

Troubleshooting

Symptom Resolution
Login returns non-zero code Inspect message; typical causes: account locked, wrong password, password expired
Repeated 401s Verify the full token was pasted; verify the Authorization header format
API Key call returns 401 Verify the header is Authorization: ApiKey zck_<prefix>.<secret> and confirm the key is not expired or revoked
API Key call returns code=1053 (HTTP 403) scopes do not cover the permission required by the endpoint. Note: scope matching is exact string equality — writing guard.* matches nothing; enumerate every leaf permission key
API Key call returns code=1056 (HTTP 403) OEM boundary violation: the request hostname does not belong to the OEM bound to the API Key. Retry against a hostname under the same OEM
Login returns 200 but data.token is empty Server misconfiguration; contact operations
Multiple CLI processes evicting each other's token Isolate them with separate profiles

Cloud WAF · dual-channel authentication with Bearer sessions and API Key machine credentials