Skip to content

PlaceOrderAsync masks CERTInext response body; sandbox rate-limit surfaces as misleading "Inactive Account User." #8

@spbsoluble

Description

@spbsoluble

Summary

Two related diagnostic issues observed during integration of the plugin
with the Keyfactor AnyCA REST Gateway running in a kind-based lab (kfclab):

  1. CERTInextClient.PlaceOrderAsync throws with only meta.ErrorMessage
    in the exception text. The raw HTTP response body is never logged, so
    when CERTInext returns a generic / mis-categorized error the operator
    has no recourse short of attaching a network sniffer.

  2. The CERTInext sandbox at https://sandbox-us-api.certinext.io/emSignHub-API
    appears to apply a burst rate limit that surfaces as the error
    string "Inactive Account User.". This is exceptionally misleading —
    it reads like an account-state condition, but the same access key /
    account works perfectly fine seconds before and after.

The combination of the two means an operator hitting the rate limit
spends a lot of time chasing a non-issue (verifying the account, rotating
keys, contacting CERTInext support) when the actual root cause is "submit
fewer orders per unit time."

Repro / evidence

Lab: kfclab (kind, k3s, Command 26.2.1, AnyCA REST Gateway 25.5.0,
certinext-caplugin built locally from main post the IDomainValidatorFactory
field-type fix — private volatile object _domainValidatorFactory;).

Sandbox account: AccessKey auth mode, AccountNumber=4873378853,
GroupNumber=3162337784, OrganizationNumber=8258978.

Timeline

Time (PDT) Source Action Result
16:23 kfclab gateway pod First smoke run, 16 enrollments fired in tight succession First 2 (DV SSL × PFX/CSR) submitted to CERTInext in pending-approval state; remaining 14 returned Inactive Account User.
16:31 kfclab gateway pod Second smoke run, same 16 entries All 16 return Inactive Account User.
09:34 local Mac (xUnit) LifecycleTests.Enroll_Synchronize_Revoke_FullLifecycle (single enroll) PASS, OrderNumber 7518968666 created in pending_approval
09:35 local Mac (xUnit) New KfclabFieldProbeTests.Probe_FieldByField, two back-to-back enrollments PASS, OrderNumbers 8744449464 and 2924858122 created in pending_approval
09:50 kfclab gateway pod Smoke run trimmed to one CERTInext canary PASS
09:55 kfclab gateway pod Smoke run with full 16 entries restored First 10 PASS in pending_approval, last 6 (OV Wildcard / OV UCC / OV Wildcard UCC × PFX/CSR) HTTP-timed out — unrelated client-side issue

The kfclab gateway and the local probe Mac use identical plugin
config values (verified down to the SHA256 of the DLL inside the pod
matching the locally built artifact). The only difference between the
runs that fail vs. pass is submission velocity.

I also wrote a field-by-field probe that submits both the
integration-test default config (SignerIp=127.0.0.1, mobile=
0000000000, SignerPlace=Gateway, RequestorName with literal quote
chars — see issue 3 below) and the kfclab field shape (SignerIp=0.0.0.0,
empty mobile, SignerPlace=Lab, unquoted RequestorName). Both succeed.
Field shape is not the cause.

Exception text observed on the gateway side

fail: AnyGatewayREST.Controllers.CertificateV1Controller[0]
      Unsuccessful Request CERTInext order failed: Inactive Account User.. See gateway logs for details.
         at Keyfactor.Extensions.CAPlugin.CERTInext.Client.CERTInextClient.PlaceOrderAsync(...) in .../CERTInextClient.cs:line 213
         at Keyfactor.Extensions.CAPlugin.CERTInext.Client.CERTInextClient.EnrollCertificateAsync(...) in .../CERTInextClient.cs:line 566
         at Keyfactor.Extensions.CAPlugin.CERTInext.CERTInextCAPlugin.EnrollNewAsync(...) in .../CERTInextCAPlugin.cs:line 912
         at Keyfactor.Extensions.CAPlugin.CERTInext.CERTInextCAPlugin.Enroll(...) in .../CERTInextCAPlugin.cs:line 575

The exception message "CERTInext order failed: Inactive Account User.. See gateway logs for details." (note the doubled period — meta.ErrorMessage already ends in ., the plugin appends .) is all the operator sees. There is no log entry containing the raw GenerateOrderSSL response body, so we don't know whether CERTInext returned additional structured detail (errorCode, retryAfter, accountId mismatch, etc.).

Asks

1. Log the raw response body on meta.IsSuccess=false

CERTInextClient.cs:212-215:

if (result.Meta != null && !result.Meta.IsSuccess)
    throw new Exception(
        $"CERTInext order failed: {result.Meta.ErrorMessage ?? result.Meta.ErrorCode}. " +
        "See gateway logs for details.");

Before the throw, please add a Logger.LogWarning (or LogError) that
captures the raw resp.Content (or resp.RawBytes for binary safety) so
the operator can see exactly what CERTInext returned. Same change is
warranted in SubmitCsrAsync (line 248), TrackOrderAsync, and any
other path that throws on a non-success meta block.

Suggested log shape (one of):

Logger.LogWarning(
    "CERTInext API non-success. Path={Path}, HttpStatus={Status}, " +
    "ErrorCode={Code}, ErrorMessage={Msg}, RawBody={Body}",
    Constants.Api.GenerateOrderSslPath,
    (int)resp.StatusCode,
    result.Meta.ErrorCode,
    result.Meta.ErrorMessage,
    Truncate(resp.Content, 4096));

The truncation matters — some responses can be very large. ~4 KB is
plenty for diagnostic context without flooding logs.

2. Document "Inactive Account User." as a rate-limit surface

Either in the README's "Troubleshooting" section or as a callout in
PlaceOrderAsync's XML doc comment, please note:

The CERTInext sandbox returns "Inactive Account User." as a generic
error string for several conditions, including rate-limit / burst-quota
rejection. If you see this error and you have confirmed the account is
active (e.g. by a successful Ping immediately before), throttle order
submissions to one per ~1-2 seconds and retry. Sustained 16+ orders in
under 10 seconds consistently trips the limit on the US sandbox.

A second-best alternative would be a defensive client-side retry in
PlaceOrderAsync when meta.ErrorMessage matches "Inactive Account User"
(case-insensitive). I'm less keen on this — it papers over the misleading
error string and burns the operator's order quota on retries — but it
would at least surface as a LogWarning that names the condition.

3. IntegrationTestFixture.LoadEnvFile does not strip surrounding quotes

Latent bug, low priority, but worth fixing while looking at the file.

IntegrationTestFixture.cs:161-163:

string key = line.Substring(0, idx).Trim();
string val = line.Substring(idx + 1).Trim();
result[key] = val;

A .env_certinext line like:

CERTINEXT_REQUESTOR_NAME="Keyfactor Plugin Test"

is parsed as the 23-character string "Keyfactor Plugin Test",
quote chars and all. The integration tests then send that to CERTInext
as the requestor name. CERTInext appears tolerant of the literal quotes
(it treats RequestorName as a free-text display field), but the same
file consumed by anything that respects quote-as-string-delimiter
semantics (kfclab's secrets pipeline did, for instance) will produce a
mismatch with the live integration test's request shape, complicating
side-by-side debugging.

Suggested fix:

string val = line.Substring(idx + 1).Trim();
if (val.Length >= 2 &&
    ((val[0] == '"' && val[val.Length - 1] == '"') ||
     (val[0] == '\'' && val[val.Length - 1] == '\'')))
{
    val = val.Substring(1, val.Length - 2);
}

Context

This was diagnosed during the kfclab effort to validate the upstream
Phase 2 fix (IDomainValidatorFactory field re-typing) end-to-end. The
fix itself works correctly — the plugin loads cleanly and orders are
submitted to CERTInext. The two issues above are the diagnostic and
documentation gaps that surfaced during validation.

Happy to send a PR for any of (1) / (2) / (3). Let me know which you'd
like to take and which you'd prefer I leave to your team.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions