Template
Add Volcengine CDN certificate renewal runbook
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import a local certificate/key pair into Volcengine Certificate Service.
|
||||
|
||||
This script deliberately reads certificate material from files at runtime so
|
||||
private keys do not need to appear in shell arguments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_dotenv(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
name, value = line.split("=", 1)
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
values[name.strip()] = value
|
||||
return values
|
||||
|
||||
|
||||
def sha256_hex(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def hmac_bytes(key: bytes, text: str) -> bytes:
|
||||
return hmac.new(key, text.encode("utf-8"), hashlib.sha256).digest()
|
||||
|
||||
|
||||
def hmac_hex(key: bytes, text: str) -> str:
|
||||
return hmac.new(key, text.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def sign_headers(
|
||||
*,
|
||||
access_key: str,
|
||||
secret_key: str,
|
||||
region: str,
|
||||
service: str,
|
||||
host: str,
|
||||
method: str,
|
||||
uri: str,
|
||||
query: str,
|
||||
body: str,
|
||||
) -> dict[str, str]:
|
||||
payload_hash = sha256_hex(body)
|
||||
now = dt.datetime.now(dt.UTC)
|
||||
x_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
short_date = now.strftime("%Y%m%d")
|
||||
|
||||
canonical_headers = (
|
||||
f"content-type:application/json\n"
|
||||
f"host:{host}\n"
|
||||
f"x-content-sha256:{payload_hash}\n"
|
||||
f"x-date:{x_date}\n"
|
||||
)
|
||||
signed_headers = "content-type;host;x-content-sha256;x-date"
|
||||
canonical_request = "\n".join(
|
||||
[
|
||||
method,
|
||||
uri,
|
||||
query,
|
||||
canonical_headers,
|
||||
signed_headers,
|
||||
payload_hash,
|
||||
]
|
||||
)
|
||||
scope = f"{short_date}/{region}/{service}/request"
|
||||
string_to_sign = "\n".join(
|
||||
[
|
||||
"HMAC-SHA256",
|
||||
x_date,
|
||||
scope,
|
||||
sha256_hex(canonical_request),
|
||||
]
|
||||
)
|
||||
|
||||
# Volcengine OpenAPI uses the raw SK as the first HMAC key for this API.
|
||||
k_date = hmac_bytes(secret_key.encode("utf-8"), short_date)
|
||||
k_region = hmac_bytes(k_date, region)
|
||||
k_service = hmac_bytes(k_region, service)
|
||||
k_signing = hmac_bytes(k_service, "request")
|
||||
signature = hmac_hex(k_signing, string_to_sign)
|
||||
|
||||
authorization = (
|
||||
f"HMAC-SHA256 Credential={access_key}/{scope}, "
|
||||
f"SignedHeaders={signed_headers}, Signature={signature}"
|
||||
)
|
||||
return {
|
||||
"Authorization": authorization,
|
||||
"Content-Type": "application/json",
|
||||
"Host": host,
|
||||
"X-Content-Sha256": payload_hash,
|
||||
"X-Date": x_date,
|
||||
}
|
||||
|
||||
|
||||
def import_certificate(args: argparse.Namespace) -> dict:
|
||||
env = read_dotenv(Path(args.env_path))
|
||||
access_key = env.get(f"{args.profile_prefix}_VOLCENGINE_ACCESS_KEY", "")
|
||||
secret_key = env.get(f"{args.profile_prefix}_VOLCENGINE_SECRET_KEY", "")
|
||||
if not access_key or not secret_key:
|
||||
raise RuntimeError(
|
||||
f"Missing {args.profile_prefix}_VOLCENGINE_ACCESS_KEY or "
|
||||
f"{args.profile_prefix}_VOLCENGINE_SECRET_KEY in {args.env_path}"
|
||||
)
|
||||
|
||||
certificate = Path(args.certificate_path).read_text(encoding="utf-8")
|
||||
private_key = Path(args.private_key_path).read_text(encoding="utf-8")
|
||||
body = json.dumps(
|
||||
{
|
||||
"CertificateInfo": {
|
||||
"CertificateChain": certificate,
|
||||
"PrivateKey": private_key,
|
||||
},
|
||||
"ProjectName": args.project_name,
|
||||
"Repeatable": True,
|
||||
"Tag": args.domain,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
service = "certificate_service"
|
||||
host = "open.volcengineapi.com"
|
||||
query = "Action=ImportCertificate&Version=2024-10-01"
|
||||
headers = sign_headers(
|
||||
access_key=access_key,
|
||||
secret_key=secret_key,
|
||||
region=args.region,
|
||||
service=service,
|
||||
host=host,
|
||||
method="POST",
|
||||
uri="/",
|
||||
query=query,
|
||||
body=body,
|
||||
)
|
||||
|
||||
request = urllib.request.Request(
|
||||
f"https://{host}/?{query}",
|
||||
data=body.encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=args.timeout) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
# Do not print Authorization, AK, SK, certificate, or private key.
|
||||
raise RuntimeError(f"Volcengine API request failed with HTTP status {exc.code}") from None
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--domain", required=True)
|
||||
parser.add_argument("--certificate-path", required=True)
|
||||
parser.add_argument("--private-key-path", required=True)
|
||||
parser.add_argument("--env-path", default=".env")
|
||||
parser.add_argument("--profile-prefix", default="INTELLIGROW")
|
||||
parser.add_argument("--region", default="cn-beijing")
|
||||
parser.add_argument("--project-name", default="default")
|
||||
parser.add_argument("--timeout", type=int, default=30)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
result = import_certificate(args)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user