Skip to content

Commit 603d6bd

Browse files
committed
feat(scripts): add gateway/Command registration tooling
Add scripts/register/ — idempotent bash stages that provision the CERTInext plugin into the AnyCA REST Gateway and Keyfactor Command, driven by integration-manifest.json product_ids: 01 gateway certificate profiles (verified live) 02 gateway CA configuration (unverified) 03 gateway claims (unverified) 04 Command CA registration (unverified; CA config — opt-in) 05 Command template import (unverified) 06 enrollment patterns + template KeyRetention (verified live) 00 orchestrator (runs 01-06, SKIP_NN=1 / DRY_RUN=1) scripts/lib/command-auth.sh provides shared auth supporting three modes per side (session cookie, bearer token, OAuth2 client_credentials). Session cookies auto-route Command calls to /KeyfactorProxy (the Portal reverse proxy that injects the bearer); /KeyfactorAPI rejects cookies. GATEWAY_BASE_PATH is the gateway instance mount path (e.g. /certinext-0). Stage 06 encodes the verified EnrollmentPatterns schema: Template as integer, AllowedEnrollmentTypes (plural), required Policies:{}, TemplateDefault, role NAME strings, update via PUT /{id}; template retention via partial PUT. Includes scripts/register/README.md (auth, env contract, gotchas, schema) and Makefile targets (register, register-profiles, ...). bash 3.2 / zsh safe.
1 parent 1675b9e commit 603d6bd

10 files changed

Lines changed: 972 additions & 0 deletions

Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ REPORT_DIR := /tmp/certinext-coverage-report
2424
revoke-order \
2525
submit-csr \
2626
list-cas \
27+
register register-profiles register-ca-config register-claims \
28+
register-command-ca register-import register-enrollment \
2729
create-product \
2830
generate-order-igtf \
2931
generate-order-149-fresh \
@@ -299,6 +301,51 @@ submit-csr:
299301
list-cas:
300302
@scripts/list-cas.sh
301303

304+
# ---------------------------------------------------------------------------
305+
# register-* — provision profiles/templates into the AnyCA REST Gateway and
306+
# Keyfactor Command. These talk to Command/gateway (OAuth2 client_credentials),
307+
# NOT the CERTInext API — see scripts/lib/command-auth.sh for the env contract
308+
# (TOKEN_URL, OIDC_CLIENT_ID/SECRET, GATEWAY_HOST, COMMAND_HOST, ...).
309+
#
310+
# make register # full provisioning (stages 01..06)
311+
# make register DRY_RUN=1 # DRY_RUN forwards to every stage
312+
# make register SKIP_03=1 # skip a stage by number
313+
#
314+
# Per-stage (each idempotent; add DRY_RUN=1 for an offline preview):
315+
# make register-profiles # 01 gateway certificate profiles [CHECK=1]
316+
# make register-ca-config # 02 gateway CAConnection + Templates
317+
# make register-claims # 03 gateway access claims (IAM)
318+
# make register-command-ca # 04 register CA in Command
319+
# make register-import # 05 import templates into Command [CHECK=1]
320+
# make register-enrollment # 06 enrollment patterns + template KeyRetention
321+
#
322+
# Stages 01 and 06 are VERIFIED live; 02-05 are built from docs/reference
323+
# captures — validate against a live gateway/Command before relying on them.
324+
# Auth (cookie/token/OAuth), env vars, and gotchas: scripts/register/README.md.
325+
# NOTE: stage 04 (and stage 02's CA-connection PUT) touch the CA config, which
326+
# is fragile — leave it alone unless explicitly required.
327+
# ---------------------------------------------------------------------------
328+
register:
329+
@scripts/register/00-register-all.sh
330+
331+
register-profiles:
332+
@scripts/register/01-gateway-profiles.sh
333+
334+
register-ca-config:
335+
@scripts/register/02-gateway-ca-config.sh
336+
337+
register-claims:
338+
@scripts/register/03-gateway-claims.sh
339+
340+
register-command-ca:
341+
@scripts/register/04-command-register-ca.sh
342+
343+
register-import:
344+
@scripts/register/05-command-import-templates.sh
345+
346+
register-enrollment:
347+
@scripts/register/06-command-enrollment-patterns.sh
348+
302349
# ---------------------------------------------------------------------------
303350
# create-product — Create a custom product via API
304351
#

scripts/lib/command-auth.sh

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env bash
2+
# Shared OAuth2 + REST helpers for Keyfactor Command / AnyCA REST Gateway
3+
# *provisioning* scripts (scripts/register/*).
4+
#
5+
# This is distinct from certinext-auth.sh: that helper signs CERTInext API
6+
# requests (SHA256 authKey). This one talks to Command and the gateway admin
7+
# API using an OAuth2 client_credentials bearer token.
8+
#
9+
# Usage:
10+
# . ~/.env_certinext
11+
# . "$(dirname "$0")/lib/command-auth.sh"
12+
# tok=$(gateway_token)
13+
# gw_curl "$tok" GET /config/certificateprofile
14+
#
15+
# Required env (set in ~/.env_certinext or exported before sourcing):
16+
# TOKEN_URL OAuth token endpoint (Authentik), e.g.
17+
# https://auth.127.0.0.1.nip.io/application/o/token/
18+
# OIDC_CLIENT_ID client_credentials client id
19+
# OIDC_CLIENT_SECRET client_credentials client secret
20+
# GATEWAY_HOST gateway ingress host (no scheme)
21+
# COMMAND_HOST Command ingress host (no scheme)
22+
# Optional env (defaults shown):
23+
# GATEWAY_SCHEME https
24+
# GATEWAY_BASE_PATH /AnyGatewayREST (gateway admin API prefix)
25+
# GATEWAY_SCOPE keyfactor-anyca-gateway
26+
# COMMAND_SCHEME https
27+
# CURL_INSECURE 1 (pass -k; set 0 to verify TLS)
28+
# CONFIGURATION_TENANT certinext-caplugin
29+
30+
GATEWAY_SCHEME="${GATEWAY_SCHEME:-https}"
31+
# GATEWAY_BASE_PATH is the gateway *instance* mount path, NOT a fixed value.
32+
# On a multi-tenant AnyCA REST Gateway each instance lives under its own path
33+
# (e.g. /certinext-0). Discover it from the Portal/Swagger URL. The historical
34+
# default /AnyGatewayREST only applies to single-instance gateways.
35+
GATEWAY_BASE_PATH="${GATEWAY_BASE_PATH:-/AnyGatewayREST}"
36+
GATEWAY_SCOPE="${GATEWAY_SCOPE:-keyfactor-anyca-gateway}"
37+
COMMAND_SCHEME="${COMMAND_SCHEME:-https}"
38+
# Command API base path. A Portal *session cookie* (COMMAND_COOKIE) only works
39+
# against /KeyfactorProxy — the Portal's reverse proxy that injects the bearer
40+
# token server-side. Direct bearer/OAuth auth uses /KeyfactorAPI. When unset,
41+
# cmd_base() resolves it at call time from whether a cookie is set (so it works
42+
# regardless of env-var ordering). Set COMMAND_BASE_PATH to force either path.
43+
COMMAND_BASE_PATH="${COMMAND_BASE_PATH:-}"
44+
CONFIGURATION_TENANT="${CONFIGURATION_TENANT:-certinext-caplugin}"
45+
CURL_INSECURE="${CURL_INSECURE:-1}"
46+
47+
_ca_require() {
48+
local missing=0 v
49+
for v in "$@"; do
50+
if [ -z "${!v:-}" ]; then
51+
echo "ERROR: required env var '$v' is not set" >&2
52+
missing=1
53+
fi
54+
done
55+
[ "$missing" -eq 0 ] || return 1
56+
}
57+
58+
# Base curl flags shared by every call (bash 3.2 compatible — global array).
59+
CA_CURL_OPTS=(-sS)
60+
[ "$CURL_INSECURE" = "1" ] && CA_CURL_OPTS+=(-k)
61+
62+
# oauth_token [scope] — fetch a client_credentials bearer token.
63+
# Echoes the raw access_token. Exits non-zero (and prints the body) on failure.
64+
oauth_token() {
65+
_ca_require TOKEN_URL OIDC_CLIENT_ID OIDC_CLIENT_SECRET || return 1
66+
local scope="${1:-}"
67+
local -a form=(
68+
--data-urlencode "grant_type=client_credentials"
69+
--data-urlencode "client_id=${OIDC_CLIENT_ID}"
70+
--data-urlencode "client_secret=${OIDC_CLIENT_SECRET}"
71+
)
72+
[ -n "$scope" ] && form+=(--data-urlencode "scope=${scope}")
73+
74+
local resp tok
75+
resp=$(curl "${CA_CURL_OPTS[@]}" -X POST "$TOKEN_URL" \
76+
-H "Content-Type: application/x-www-form-urlencoded" \
77+
"${form[@]}") || { echo "ERROR: token request failed" >&2; return 1; }
78+
tok=$(printf '%s' "$resp" | jq -r '.access_token // empty')
79+
if [ -z "$tok" ]; then
80+
echo "ERROR: no access_token in response:" >&2
81+
printf '%s\n' "$resp" >&2
82+
return 1
83+
fi
84+
printf '%s' "$tok"
85+
}
86+
87+
# Auth resolution order (per side):
88+
# 1. A browser-session cookie (GATEWAY_COOKIE / COMMAND_COOKIE) — paste the
89+
# full `cookie:` header value from devtools (Copy as cURL) when the UI uses
90+
# OIDC session cookies instead of bearer tokens. The *_token fns return
91+
# empty in this mode; gw_curl/cmd_curl send the Cookie header instead.
92+
# 2. An explicit pre-obtained bearer token (GATEWAY_TOKEN / COMMAND_TOKEN).
93+
# 3. OAuth2 client_credentials via oauth_token (needs OIDC_CLIENT_* + TOKEN_URL).
94+
gateway_token() {
95+
if [ -n "${GATEWAY_COOKIE:-}" ]; then return 0; fi # cookie mode
96+
if [ -n "${GATEWAY_TOKEN:-}" ]; then printf '%s' "$GATEWAY_TOKEN"; return 0; fi
97+
oauth_token "$GATEWAY_SCOPE"
98+
}
99+
command_token() {
100+
if [ -n "${COMMAND_COOKIE:-}" ]; then return 0; fi # cookie mode
101+
if [ -n "${COMMAND_TOKEN:-}" ]; then printf '%s' "$COMMAND_TOKEN"; return 0; fi
102+
oauth_token ""
103+
}
104+
105+
gw_base() {
106+
_ca_require GATEWAY_HOST || return 1
107+
printf '%s://%s%s' "$GATEWAY_SCHEME" "$GATEWAY_HOST" "$GATEWAY_BASE_PATH"
108+
}
109+
cmd_base() {
110+
_ca_require COMMAND_HOST || return 1
111+
local bp="$COMMAND_BASE_PATH"
112+
if [ -z "$bp" ]; then
113+
if [ -n "${COMMAND_COOKIE:-}" ]; then bp="/KeyfactorProxy"; else bp="/KeyfactorAPI"; fi
114+
fi
115+
printf '%s://%s%s' "$COMMAND_SCHEME" "$COMMAND_HOST" "$bp"
116+
}
117+
118+
# Display helpers for log headers: the base URL, or a clear "(unset)" note.
119+
gw_show() { if [ -n "${GATEWAY_HOST:-}" ]; then gw_base; else printf '(GATEWAY_HOST unset)'; fi; }
120+
cmd_show() { if [ -n "${COMMAND_HOST:-}" ]; then cmd_base; else printf '(COMMAND_HOST unset)'; fi; }
121+
122+
# gw_curl <token> <method> <path> [data] [extra curl args...]
123+
# Hits the gateway admin API. <path> is relative to GATEWAY_BASE_PATH
124+
# (e.g. /config/certificateprofile). Echoes the response body.
125+
gw_curl() {
126+
local tok="$1" method="$2" path="$3" data="${4:-}"; shift; shift; shift
127+
[ $# -gt 0 ] && shift || true
128+
# In cookie mode, mimic the browser exactly (XMLHttpRequest + CSRF header).
129+
local rw="APIClient"
130+
[ -n "${GATEWAY_COOKIE:-}" ] && rw="XMLHttpRequest"
131+
local -a args=("${CA_CURL_OPTS[@]}" -X "$method" "$(gw_base)$path"
132+
-H "x-keyfactor-requested-with: $rw"
133+
-H "Content-Type: application/json")
134+
if [ -n "${GATEWAY_COOKIE:-}" ]; then
135+
args+=(-H "Cookie: ${GATEWAY_COOKIE}" -H "x-requested-with: XMLHttpRequest")
136+
fi
137+
[ -n "$tok" ] && args+=(-H "Authorization: Bearer $tok")
138+
[ -n "$data" ] && args+=(-d "$data")
139+
args+=("$@")
140+
curl "${args[@]}"
141+
}
142+
143+
# cmd_curl <token> <method> <path> [data] [api-version] [extra curl args...]
144+
# Hits the Command KeyfactorAPI. <path> is relative to /KeyfactorAPI.
145+
cmd_curl() {
146+
local tok="$1" method="$2" path="$3" data="${4:-}" ver="${5:-1}"
147+
shift; shift; shift
148+
[ $# -gt 0 ] && shift || true
149+
[ $# -gt 0 ] && shift || true
150+
local rw="APIClient"
151+
[ -n "${COMMAND_COOKIE:-}" ] && rw="XMLHttpRequest"
152+
local -a args=("${CA_CURL_OPTS[@]}" -X "$method" "$(cmd_base)$path"
153+
-H "x-keyfactor-api-version: $ver"
154+
-H "x-keyfactor-requested-with: $rw"
155+
-H "Content-Type: application/json")
156+
if [ -n "${COMMAND_COOKIE:-}" ]; then
157+
args+=(-H "Cookie: ${COMMAND_COOKIE}" -H "x-requested-with: XMLHttpRequest")
158+
fi
159+
[ -n "$tok" ] && args+=(-H "Authorization: Bearer $tok")
160+
[ -n "$data" ] && args+=(-d "$data")
161+
args+=("$@")
162+
curl "${args[@]}"
163+
}
164+
165+
# manifest_product_ids [manifest-path] — emit product_ids one per line.
166+
manifest_product_ids() {
167+
local manifest="${1:-$REPO_ROOT/integration-manifest.json}"
168+
jq -r '.about.carest.product_ids[]' "$manifest"
169+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# Orchestrator — run the full gateway + Command registration in order.
3+
#
4+
# Each stage is an independent script and can be run on its own. This driver
5+
# runs them in sequence, skipping any stage whose script does not yet exist
6+
# (stages 02-06 are added incrementally) or whose SKIP_<NN> flag is set to 1.
7+
#
8+
# make register
9+
# SKIP_03=1 make register # skip claims
10+
# DRY_RUN=1 make register # forwarded to every stage
11+
set -euo pipefail
12+
13+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14+
15+
# stage number -> script basename
16+
STAGES=(
17+
"01:01-gateway-profiles.sh"
18+
"02:02-gateway-ca-config.sh"
19+
"03:03-gateway-claims.sh"
20+
"04:04-command-register-ca.sh"
21+
"05:05-command-import-templates.sh"
22+
"06:06-command-enrollment-patterns.sh"
23+
)
24+
25+
for entry in "${STAGES[@]}"; do
26+
num="${entry%%:*}"
27+
script="${entry#*:}"
28+
skip_var="SKIP_${num}"
29+
path="$SCRIPT_DIR/$script"
30+
31+
if [ "${!skip_var:-0}" = "1" ]; then
32+
echo ">> stage $num ($script): SKIPPED (${skip_var}=1)"
33+
continue
34+
fi
35+
if [ ! -x "$path" ]; then
36+
echo ">> stage $num ($script): not yet implemented — skipping"
37+
continue
38+
fi
39+
40+
echo ">> stage $num ($script): running"
41+
"$path"
42+
echo
43+
done
44+
45+
echo ">> registration complete"
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env bash
2+
# Stage 01 — register AnyCA REST Gateway certificate profiles.
3+
#
4+
# Creates (or updates) one gateway certificate profile per CERTInext product,
5+
# driven by .about.carest.product_ids in integration-manifest.json. Idempotent:
6+
# existing profiles are PUT-updated, new ones are POSTed.
7+
#
8+
# Env: see scripts/lib/command-auth.sh for the OAuth/host contract.
9+
# Optional:
10+
# KEY_ALGS_JSON override the key_algs object (default: lab set below)
11+
# MANIFEST path to integration-manifest.json (default: repo root)
12+
# CHECK 1 = after applying, diff result vs the captured reference
13+
# (docs/reference/gateway/certificate-profiles.json)
14+
# DRY_RUN 1 = print intended actions, make no write calls
15+
set -euo pipefail
16+
17+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19+
export REPO_ROOT
20+
21+
# shellcheck disable=SC1090
22+
[ -f ~/.env_certinext ] && . ~/.env_certinext
23+
# shellcheck source=../lib/command-auth.sh
24+
. "$SCRIPT_DIR/../lib/command-auth.sh"
25+
26+
MANIFEST="${MANIFEST:-$REPO_ROOT/integration-manifest.json}"
27+
DRY_RUN="${DRY_RUN:-0}"
28+
CHECK="${CHECK:-0}"
29+
30+
# Lab default key algorithms — matches docs/reference/gateway/certificate-profiles.json.
31+
DEFAULT_KEY_ALGS_JSON='{
32+
"rsa": { "bit_lengths": [2048, 3072, 4096, 6144, 8192] },
33+
"ecdsa": { "curves": ["1.2.840.10045.3.1.7", "1.3.132.0.34", "1.3.132.0.35"] },
34+
"ed25519": { "bit_lengths": [255] },
35+
"ed448": { "bit_lengths": [448] }
36+
}'
37+
KEY_ALGS_JSON="${KEY_ALGS_JSON:-$DEFAULT_KEY_ALGS_JSON}"
38+
39+
if ! echo "$KEY_ALGS_JSON" | jq -e . >/dev/null 2>&1; then
40+
echo "ERROR: KEY_ALGS_JSON is not valid JSON" >&2
41+
exit 1
42+
fi
43+
44+
echo "== Stage 01: gateway certificate profiles =="
45+
echo " gateway : $(gw_show)"
46+
echo " manifest: $MANIFEST"
47+
[ "$DRY_RUN" = "1" ] && echo " DRY_RUN : no write calls will be made"
48+
49+
PRODUCTS=()
50+
while IFS= read -r _p; do
51+
[ -n "$_p" ] && PRODUCTS+=("$_p")
52+
done < <(manifest_product_ids "$MANIFEST")
53+
[ "${#PRODUCTS[@]}" -gt 0 ] || { echo "ERROR: no product_ids in manifest" >&2; exit 1; }
54+
echo " products: ${#PRODUCTS[@]}"
55+
56+
if [ "$DRY_RUN" = "1" ]; then
57+
# Fully offline preview: no token, no listing.
58+
echo " (dry run) would upsert ${#PRODUCTS[@]} profiles with key_algs:"
59+
echo "$KEY_ALGS_JSON" | jq -c .
60+
for name in "${PRODUCTS[@]}"; do
61+
printf ' [DRY ] %s\n' "$name"
62+
done
63+
echo "== done (dry run): no calls made =="
64+
exit 0
65+
fi
66+
67+
TOK="$(gateway_token)"
68+
69+
# Snapshot existing profiles once: name -> id.
70+
EXISTING="$(gw_curl "$TOK" GET /config/certificateprofile)"
71+
if ! echo "$EXISTING" | jq -e 'type == "array"' >/dev/null 2>&1; then
72+
echo "ERROR: unexpected response listing certificate profiles:" >&2
73+
printf '%s\n' "$EXISTING" >&2
74+
exit 1
75+
fi
76+
77+
created=0 updated=0
78+
for name in "${PRODUCTS[@]}"; do
79+
existing_id="$(echo "$EXISTING" | jq -r --arg n "$name" \
80+
'.[] | select(.name == $n) | .id' | head -n1)"
81+
82+
body="$(jq -n --arg name "$name" --argjson algs "$KEY_ALGS_JSON" \
83+
'{name: $name, key_algs: $algs}')"
84+
85+
if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then
86+
body="$(echo "$body" | jq --argjson id "$existing_id" '. + {id: $id}')"
87+
printf ' [PUT ] %-40s (id=%s)\n' "$name" "$existing_id"
88+
if [ "$DRY_RUN" != "1" ]; then
89+
resp="$(gw_curl "$TOK" PUT /config/certificateprofile "$body")"
90+
echo "$resp" | jq -e 'has("error") or has("Message")' >/dev/null 2>&1 \
91+
&& { echo " ! update failed: $resp" >&2; }
92+
fi
93+
updated=$((updated + 1))
94+
else
95+
printf ' [POST] %-40s (new)\n' "$name"
96+
if [ "$DRY_RUN" != "1" ]; then
97+
resp="$(gw_curl "$TOK" POST /config/certificateprofile "$body")"
98+
echo "$resp" | jq -e 'has("error") or has("Message")' >/dev/null 2>&1 \
99+
&& { echo " ! create failed: $resp" >&2; }
100+
fi
101+
created=$((created + 1))
102+
fi
103+
done
104+
105+
echo "== done: $created created, $updated updated =="
106+
107+
if [ "$CHECK" = "1" ] && [ "$DRY_RUN" != "1" ]; then
108+
ref="$REPO_ROOT/docs/reference/gateway/certificate-profiles.json"
109+
echo "== CHECK: comparing live profile names vs $ref =="
110+
live_names="$(gw_curl "$TOK" GET /config/certificateprofile | jq -r '[.[].name] | sort')"
111+
# Reference only captured DV/OV (no EV); compare on the set the reference covers.
112+
ref_names="$(jq -r '[.[].name] | sort' "$ref")"
113+
missing="$(jq -n --argjson live "$live_names" --argjson ref "$ref_names" \
114+
'$ref - $live')"
115+
if [ "$(echo "$missing" | jq 'length')" -eq 0 ]; then
116+
echo " OK: all reference profiles present on the gateway"
117+
else
118+
echo " MISSING reference profiles: $missing" >&2
119+
exit 1
120+
fi
121+
fi

0 commit comments

Comments
 (0)