toga-ai 1.0.812 → 1.0.813
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/cloud-secrets-manager.md +71 -0
- package/knowledge/2.0/apps/saml/architecture.md +9 -3
- package/knowledge/2.0/apps/saml/workflows/rotating-the-saml-sp-certificate.md +75 -9
- package/knowledge/2.0/standards/ssl-certificate-trust-model.md +28 -6
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/prudential/profile.md +2 -1
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki |
|
|
16
16
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the |
|
|
17
17
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. |
|
|
18
|
+
| [_Cloud Secrets Manager helper (_Cloud::getSecret)](features/cloud-secrets-manager.md) | `_Cloud::getSecret($secretName, $awsRegion = null): ?array` reads a value from **AWS Secrets Manager** for the 2.0 stack. |
|
|
18
19
|
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP |
|
|
19
20
|
| [_Config group access — the two-argument "optional" form does NOT rescue a missing GROUP](features/config-group-access.md) | `_Config::<group>('<property>')` reads a value out of the active `Config/<ENVIRONMENT>.ini`, and **it throws when the requested group is absent from the ini.** |
|
|
20
21
|
| [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the **authoritative registry of which host serves which client's app in which environment**. |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: _Cloud Secrets Manager helper (_Cloud::getSecret)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-15
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Cloud.php
|
|
13
|
+
related:
|
|
14
|
+
- ./cloud-s3-helpers.md
|
|
15
|
+
- ../../saml/workflows/rotating-the-saml-sp-certificate.md
|
|
16
|
+
- ../../saml/architecture.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
`_Cloud::getSecret($secretName, $awsRegion = null): ?array` reads a value from **AWS Secrets
|
|
22
|
+
Manager** for the 2.0 stack. It lets code pull secret material (SSL certs/keys, credentials) at
|
|
23
|
+
request time instead of shipping it on disk or in `production.ini`. First consumer: the SAML
|
|
24
|
+
gateway's `/acs`, which builds SP encryption credentials from Secrets Manager (see the SAML SP
|
|
25
|
+
certificate rotation workflow).
|
|
26
|
+
|
|
27
|
+
## Key files / entry points
|
|
28
|
+
|
|
29
|
+
- `_underscore/Cloud.php` — `_Cloud::getSecret(string $secretName, ?string $awsRegion = null): ?array`.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
1. Builds a `SecretsManagerClient` and calls `GetSecretValue` for `$secretName`.
|
|
34
|
+
2. **Auth is the AWS default provider chain** — the EC2 instance role in prod, a local
|
|
35
|
+
profile/env in dev. It deliberately does **NOT** use the shared static `_Config::cloud`
|
|
36
|
+
access key that the S3 helpers use. Reason: keep the blast radius small — leaking that one
|
|
37
|
+
shared key must not also expose the SAML SP private keys. Grant the reading role its own
|
|
38
|
+
Secrets Manager permission instead.
|
|
39
|
+
3. `$awsRegion` defaults to `null`; pass the secret's region explicitly (SAML passes
|
|
40
|
+
`'us-east-1'`).
|
|
41
|
+
4. Returns the secret's JSON decoded to an **assoc array**. For the SSL secrets the keys are
|
|
42
|
+
`certificate`, `privateKey`, `certificateChain` (optional), `passphrase` (optional).
|
|
43
|
+
5. **Caches per PHP process** (function-static) — repeated reads of the same secret in one
|
|
44
|
+
request hit the cache, not the API.
|
|
45
|
+
6. **Never throws.** Any failure (missing secret, no permission, bad JSON) logs and returns
|
|
46
|
+
`null`, so a caller can skip one unusable secret and keep going rather than 500 the request.
|
|
47
|
+
|
|
48
|
+
## Client variations
|
|
49
|
+
|
|
50
|
+
None — shared core helper.
|
|
51
|
+
|
|
52
|
+
## Gotchas / known issues
|
|
53
|
+
|
|
54
|
+
- **Returns `null` on failure, does not throw.** A caller MUST null-check the result; a silent
|
|
55
|
+
`null` means "secret unavailable", not "empty secret".
|
|
56
|
+
- **The role needs its own Secrets Manager grant.** Because it does not use the `_Config::cloud`
|
|
57
|
+
key, an instance/role with no `secretsmanager:GetSecretValue` gets `null` for every secret.
|
|
58
|
+
The `saml-production` EB instance role needed an inline policy added (see the SAML architecture
|
|
59
|
+
Deployment notes).
|
|
60
|
+
- **A committed AWS access key + secret were found in a code comment in `Cloud.php` `initialize()`
|
|
61
|
+
and removed (2026-09-15).** Treat that key as leaked — it must be rotated separately from this
|
|
62
|
+
change. Never store credential VALUES in source or comments; reference secret names only.
|
|
63
|
+
|
|
64
|
+
## Change history
|
|
65
|
+
- 2026-09-15 — Added `_Cloud::getSecret()` Secrets Manager reader: default-provider-chain auth
|
|
66
|
+
(not the shared `_Config::cloud` key), per-process cache, returns decoded assoc array or `null`
|
|
67
|
+
on any failure (never throws). First used by SAML `/acs` to source SP encryption certs. Also
|
|
68
|
+
removed a committed AWS access key/secret from a `Cloud.php` `initialize()` comment — treat as
|
|
69
|
+
leaked, rotate separately. (jcardinal)
|
|
70
|
+
</content>
|
|
71
|
+
</invoke>
|
|
@@ -6,7 +6,7 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/index.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- saml/Config/production.ini
|
|
16
16
|
- saml/.platform/hooks/prebuild/git.sh
|
|
17
17
|
- saml/.ebextensions/git.php
|
|
18
|
+
- _underscore/Cloud.php
|
|
18
19
|
related:
|
|
19
20
|
- 2.0/apps/_underscore/architecture.md
|
|
20
21
|
---
|
|
@@ -49,7 +50,7 @@ Legacy `/?meta`, `/?acs`, `/?sls` query-style paths are rewritten automatically.
|
|
|
49
50
|
2. **Version + time check** — RelayState JSON: `v` (currently `1`), `time`, `domain` UUID, optional `urlParameters`. Validated against `MAX_TIME_TO_LIVE` (300s).
|
|
50
51
|
3. **Parse SAMLResponse** — base64-decoded, deserialized via LightSaml.
|
|
51
52
|
4. **Status check** — proceeds only on `STATUS_SUCCESS`.
|
|
52
|
-
5. **Assertion decryption (
|
|
53
|
+
5. **Assertion decryption (multi-cert, loaded from Secrets Manager at request time).** `/acs` loads the SP encryption cert+key pairs named in `ENCRYPTION_SECRET_NAMES` from AWS Secrets Manager (via `_Cloud::getSecret()`) at request time — **no key material lives on disk under `_underscore/Assets/ssl/`**. It builds an `X509Credential` per usable secret and calls `$reader->decryptMultiAssertion([...$credentials], $ctx)`. LightSaml's `decryptMulti()` tries each credential and only re-throws once all fail (fails closed), so an assertion encrypted to any current SP key decrypts, and rotation is just editing the secret list. See the SP cert-rotation workflow.
|
|
53
54
|
6. **Resolve client + environment** — `_Model_Core_Domain` by RelayState `domain` UUID → `_Model_Core_Client` + `_Model_Core_Environment`. Dev on real AWS instance downgrades to beta.
|
|
54
55
|
7. **Dynamic DB registration** — SQL joins `Clients`, `Databases`, `DatabaseHosts`, `Environments` for the environment slug. Calls `_Database::register()` + `connect()` for `DB_CLIENT`, `DB_CLIENT_LOGS`, `DB_CLIENT_ARCHIVE`.
|
|
55
56
|
8. **Identity mapping** — `_Model_<ClientIdentifier>_ClientAuthentication::getAuthenticatedSsoUser($assertion)`. Wrapped in try/catch; exceptions sent to Sentry.
|
|
@@ -58,10 +59,11 @@ Legacy `/?meta`, `/?acs`, `/?sls` query-style paths are rewritten automatically.
|
|
|
58
59
|
|
|
59
60
|
### Failure responses
|
|
60
61
|
`Invalid SAMLResponse (Code = 1–4)`, `SAML Authentication Failed`, `User Authentication Failed`.
|
|
62
|
+
Two codes cover the Secrets Manager cert path: **Code 5** — no usable SP credential (none of `ENCRYPTION_SECRET_NAMES` resolved to a valid cert+key); **Code 6** — a credential was built but every decrypt attempt failed (assertion not encrypted to any current SP key).
|
|
61
63
|
|
|
62
64
|
## /meta SP credential (signing vs encryption)
|
|
63
65
|
|
|
64
|
-
`/meta` publishes TWO independent `KeyDescriptor`s: `use="signing"` and `use="encryption"`. They can carry **different** certs, which lets the SP encryption cert rotate without touching the signing cert. The private key that signs our **outbound** AuthnRequests lives in `_underscore` (`_Model_Core_ClientAuthentication::PATH_TO_PRIVATE_KEY`) and is **global — one key for all client IdPs**, so the signing cert cannot flip until every IdP has loaded the new metadata.
|
|
66
|
+
`/meta` publishes TWO independent `KeyDescriptor`s: `use="signing"` and `use="encryption"`. They can carry **different** certs, which lets the SP encryption cert rotate without touching the signing cert. The private key that signs our **outbound** AuthnRequests lives in `_underscore` (`_Model_Core_ClientAuthentication::PATH_TO_PRIVATE_KEY`) and is **global — one key for all client IdPs**, so the signing cert cannot flip until every IdP has loaded the new metadata. The signing cert path is still a class constant in `saml/Controller/Index.php` (`PATH_TO_SIGNING_CERTIFICATE`). For encryption, only `PATH_TO_ENCRYPTION_CERTIFICATE_NEW` remains — `/meta` still reads it to publish the current encryption `KeyDescriptor`. The old on-disk constants `PATH_TO_ENCRYPTION_PRIVATE_KEY_NEW`, `PATH_TO_ENCRYPTION_PRIVATE_KEY_OLD`, and `PATH_TO_ENCRYPTION_CERTIFICATE_OLD` are **removed** — `/acs` no longer reads encryption key material from disk (it comes from Secrets Manager, see /acs step 5). Full procedure: SP cert-rotation workflow.
|
|
65
67
|
|
|
66
68
|
## Per-client SSO extension pattern
|
|
67
69
|
|
|
@@ -75,6 +77,7 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
75
77
|
- **`_underscore` pulled at deploy time** via `.ebextensions/git.php` + prebuild hook — clones branch `_production`. Deploy `saml` after `_underscore` merges to pick up framework changes.
|
|
76
78
|
- **CodePipeline:** Source (GitHub `_production`) → ManualApproval → Deploy (EB).
|
|
77
79
|
- **Composer deps** installed at postdeploy via `015_install_composer.sh`.
|
|
80
|
+
- **Secrets Manager read access.** The shared `ElasticBeanstalk-EC2-Instance-Profile` carries an inline `SecretsManagerReadAll` policy (`secretsmanager:GetSecretValue` + `DescribeSecret`, `Resource: "*"`) so `/acs` can read the SP encryption secrets at runtime. Developer chose account-wide `Resource: "*"` deliberately (shared profile across EB apps); verified effective access with the IAM policy simulator.
|
|
78
81
|
- **Non-prod shared-ALB self-registration (2026-08-27).** Postdeploy hook
|
|
79
82
|
`060_register_instance_to_shared_application_load_balancer.sh` + `ebs/register_instance_to_shared_application_load_balancer.php`
|
|
80
83
|
register a non-production `saml` instance into the target group named exactly its EB env name;
|
|
@@ -95,6 +98,8 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
95
98
|
- **SAML signature verification is commented out — this is the repo's top security priority.** The IdP X509 cert verification block in the `/acs` flow is disabled, so the gateway accepts any well-formed, status-success `SAMLResponse` without proving it was signed by the expected IdP. The only remaining gate is the encrypted RelayState, which authenticates the *originating TOGa request* — not the *asserting IdP*. Threat model: an attacker who can craft or replay a `SAMLResponse` (or a malicious/compromised IdP) can forge an identity assertion for any user, since nothing binds the assertion to a trusted signing key. Re-enabling per-IdP X509 signature verification is the single highest-impact security fix for this repo.
|
|
96
99
|
- **Plaintext secrets in source — move to AWS SSM Parameter Store.** `saml/Config/production.ini` stores credentials in plaintext, and a Sentry DSN is hardcoded in `saml/Controller/Index.php`. Both are checked into the repo. Migrate these to AWS SSM Parameter Store (SecureString) and load at runtime; the source tree should reference parameter names only, never literal credential values.
|
|
97
100
|
- **`/meta` cert lines must be `trim()`-ed (CRLF).** The `.crt` files in `_underscore/Assets/ssl` are CRLF-encoded; the cert-format code does `explode("\n", ...)` and indents each line, so without a per-line `trim()` a stray `\r` ends up on every base64 line inside `<ds:X509Certificate>`. Trim each line in both the signing and encryption blocks.
|
|
101
|
+
- **SP encryption certs load from Secrets Manager at request time.** `/acs` holds no encryption key material on disk (`_underscore/Assets/ssl/` no longer carries the SP encryption key). A rotation is now an edit to the `ENCRYPTION_SECRET_NAMES` list + the secrets themselves, not a redeploy of on-disk `.crt`/`.key` files.
|
|
102
|
+
- **Committed AWS key must be rotated.** A hardcoded AWS key was removed from `_underscore/Cloud.php`; because it was committed to git history it is compromised — rotate it. Still open: the plaintext creds in `production.ini` and the hardcoded Sentry DSN (see next gotcha).
|
|
98
103
|
- **SLS not implemented.** `/sls` falls through to the default banner.
|
|
99
104
|
- **`set_exception_handler(null)` at top of `saml()`.** Unhandled exceptions output raw PHP errors. All exceptions from `getAuthenticatedSsoUser()` are caught and sent to Sentry.
|
|
100
105
|
|
|
@@ -103,3 +108,4 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
|
|
|
103
108
|
- 2026-06-25 — Sharpened signature-verification gotcha with explicit threat model and flagged it as the repo's top security priority; documented plaintext secrets in `production.ini` + hardcoded Sentry DSN in `Controller/Index.php` with SSM Parameter Store recommendation (jcardinal)
|
|
104
109
|
- 2026-08-27 — Documented non-prod shared-ALB self-registration postdeploy hook (`060_...sh` + `ebs/register_instance_to_shared_application_load_balancer.php`); skips `production`, requires `aws/aws-sdk-php` + two IAM actions; cross-linked worker2 ALB auto-registration feature (jcardinal)
|
|
105
110
|
- 2026-09-04 — `/acs` now dual-credential decrypt (`decryptMultiAssertion([new, old])`) for SP encryption-cert overlap; documented `/meta`'s two KeyDescriptors (signing vs encryption) and the global outbound-signing key; added the CRLF `/meta` cert-line `trim()` gotcha. (jcardinal)
|
|
111
|
+
- 2026-09-15 — `/acs` encryption certs now load from AWS Secrets Manager at request time via `_Cloud::getSecret()` (`ENCRYPTION_SECRET_NAMES`, no on-disk key material); added failure Codes 5 (no usable SP credential) and 6 (all decrypts failed); removed dead disk constants (`PATH_TO_ENCRYPTION_PRIVATE_KEY_NEW/_OLD`, `PATH_TO_ENCRYPTION_CERTIFICATE_OLD`), kept `PATH_TO_ENCRYPTION_CERTIFICATE_NEW` for `/meta`; documented the `SecretsManagerReadAll` inline policy on the shared EC2 instance profile; flagged the committed AWS key removed from `_underscore/Cloud.php` for rotation. (jcardinal)
|
|
@@ -6,16 +6,18 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
|
-
- _underscore/Assets/ssl/togahub_com.crt
|
|
13
|
-
- _underscore/Assets/ssl/togahub_private_key.key
|
|
14
|
-
- _underscore/Model/Core/ClientAuthentication.php
|
|
15
12
|
- saml/Controller/Index.php
|
|
13
|
+
- _underscore/Cloud.php
|
|
14
|
+
- _underscore/Model/Core/ClientAuthentication.php
|
|
15
|
+
- _underscore/Assets/ssl/togahub_com.crt
|
|
16
16
|
related:
|
|
17
17
|
- 2.0/apps/saml/architecture.md
|
|
18
18
|
- 2.0/apps/saml/workflows/onboarding-a-new-sso-client.md
|
|
19
|
+
- 2.0/apps/_underscore/features/cloud-secrets-manager.md
|
|
20
|
+
- 2.0/standards/ssl-certificate-trust-model.md
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Summary
|
|
@@ -78,11 +80,65 @@ uses and **rotates encryption first, signing later**:
|
|
|
78
80
|
### Remaining steps / follow-ups (not yet done)
|
|
79
81
|
1. **Flip signing to the new cert** once every IdP has loaded the new metadata — advertise the
|
|
80
82
|
new signing cert in `/meta` and switch (d) to the new key. Coordinate the date.
|
|
81
|
-
2. **Log which SP key decrypted** (
|
|
82
|
-
see which IdPs have cut over and when it is safe to drop the
|
|
83
|
-
3. **
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
2. **Log which SP key decrypted** (secret name / identifier only — never assertion data) so you
|
|
84
|
+
can see which IdPs have cut over and when it is safe to drop a cert from the list.
|
|
85
|
+
3. **Dedupe the two `/meta` cert-format blocks** into a shared helper (non-blocking).
|
|
86
|
+
|
|
87
|
+
(Done 2026-09-15: the `/acs` decrypt is now wrapped and fails closed with coded responses —
|
|
88
|
+
`Invalid SAMLResponse (Code = 5)` when no credential builds, `Code = 6` when all decrypts fail.)
|
|
89
|
+
|
|
90
|
+
## Multi-cert decryption from Secrets Manager (2026-09-15)
|
|
91
|
+
|
|
92
|
+
`/acs` no longer reads SP encryption keys from disk. It now sources **N** SP certs from AWS
|
|
93
|
+
Secrets Manager and decrypts against all of them at once, so a partner can move to a different
|
|
94
|
+
cert while other IdPs stay on the existing one — a superset of the earlier new/old overlap.
|
|
95
|
+
|
|
96
|
+
- **Two class constants in `saml/Controller/Index.php` drive it:**
|
|
97
|
+
- `ENCRYPTION_SECRET_NAMES` — an **ordered list of Secrets Manager secret NAMES** (not values).
|
|
98
|
+
- `ENCRYPTION_SECRET_REGION` — `'us-east-1'`.
|
|
99
|
+
- **How `/acs` builds credentials:** for each name it calls `_Cloud::getSecret($name, $region)`
|
|
100
|
+
(see the [_Cloud Secrets Manager helper](../../_underscore/features/cloud-secrets-manager.md)),
|
|
101
|
+
builds an `X509Credential`, then calls `decryptMultiAssertion($credentials, ...)`. An assertion
|
|
102
|
+
encrypted to ANY listed cert decrypts.
|
|
103
|
+
- **Fails closed with coded responses:** `Invalid SAMLResponse (Code = 5)` if no usable
|
|
104
|
+
credential can be built at all; `Code = 6` if credentials build but every decrypt fails. Each
|
|
105
|
+
per-secret build is wrapped in try/catch — a bad/unreadable secret is skipped and logged, so
|
|
106
|
+
one broken secret does not 500 every login.
|
|
107
|
+
- **Passphrase-aware, format-tolerant key loading:** PEM stored with escaped `\n` / `\r\n` is
|
|
108
|
+
normalized to real newlines before loading. The secret's `passphrase` is passed to
|
|
109
|
+
`KeyHelper::createPrivateKey` **only** when the private-key PEM header is the
|
|
110
|
+
`ENCRYPTED PRIVATE KEY` variant (ACM public-cert exports are always encrypted); a
|
|
111
|
+
plaintext key loads with no passphrase. A large "how this works" comment sits above
|
|
112
|
+
`ENCRYPTION_SECRET_NAMES` in the controller.
|
|
113
|
+
- **To add or retire a cert: edit `ENCRYPTION_SECRET_NAMES` only** (and add/update the secret in
|
|
114
|
+
Secrets Manager) — no file swap on disk, no `_underscore` cert-file change. This replaces the
|
|
115
|
+
old "swap the `.crt`/`.key` files + redeploy" step for the encryption credential.
|
|
116
|
+
- **Dead constants removed** from `saml/Controller/Index.php`:
|
|
117
|
+
`PATH_TO_ENCRYPTION_PRIVATE_KEY_NEW`, `PATH_TO_ENCRYPTION_PRIVATE_KEY_OLD`,
|
|
118
|
+
`PATH_TO_ENCRYPTION_CERTIFICATE_OLD`. `PATH_TO_ENCRYPTION_CERTIFICATE_NEW` **stays** — `/meta`
|
|
119
|
+
still reads it from disk to publish the encryption `KeyDescriptor`.
|
|
120
|
+
- **The `saml-production` EB instance role needed a Secrets Manager grant** — see the SAML
|
|
121
|
+
architecture Deployment notes.
|
|
122
|
+
|
|
123
|
+
### Secret names in `ENCRYPTION_SECRET_NAMES` (WHERE only — never the values)
|
|
124
|
+
|
|
125
|
+
AWS Secrets Manager, account **654654170868**, region **us-east-1**. Each secret is JSON with
|
|
126
|
+
keys `certificate`, `privateKey` (plaintext OR encrypted), `certificateChain` (optional),
|
|
127
|
+
`passphrase` (optional; present only for an encrypted key):
|
|
128
|
+
|
|
129
|
+
- `toga/ssl/togahub_com-selfsigned-2036` — the self-signed 10-year cert; kept for clients OK
|
|
130
|
+
with self-signed.
|
|
131
|
+
- `toga/ssl/togahub_com-casigned-2027-03-31` — publicly-trusted (ACM exportable) cert.
|
|
132
|
+
- `toga/ssl/togahub_com-casigned-2026-09-16` — publicly-trusted (ACM exportable) cert.
|
|
133
|
+
|
|
134
|
+
### Why a public cert was added (Prudential)
|
|
135
|
+
|
|
136
|
+
Prudential's IdP **refuses a self-signed cert, pins our exact leaf, and cannot auto-refresh our
|
|
137
|
+
metadata by URL** — that combination forces a publicly-trusted cert. The correct source is an
|
|
138
|
+
**ACM exportable public cert** (gives cert + private key), not ACM Private CA. Prudential moves
|
|
139
|
+
to the public cert while other IdPs stay on the self-signed one; all coexist in the `/acs`
|
|
140
|
+
multi-cert array. For the full trust-model rationale see the
|
|
141
|
+
[SSL certificate trust-model strategy](../../standards/ssl-certificate-trust-model.md).
|
|
86
142
|
|
|
87
143
|
## Current credential (as of 2026-08-27, staged — cutover pending)
|
|
88
144
|
|
|
@@ -149,6 +205,16 @@ every pinning partner to reinstall on each renewal. The dedicated self-signed 10
|
|
|
149
205
|
**decouples the SP credential from TLS renewals** — that is the whole point of the change.
|
|
150
206
|
|
|
151
207
|
## Change history
|
|
208
|
+
- 2026-09-15 — `/acs` now sources SP encryption certs from **AWS Secrets Manager** (via
|
|
209
|
+
`_Cloud::getSecret`), not disk: two new constants `ENCRYPTION_SECRET_NAMES` (ordered secret
|
|
210
|
+
NAMES) + `ENCRYPTION_SECRET_REGION`, N-cert `decryptMultiAssertion`, fails closed with
|
|
211
|
+
`Code = 5`/`Code = 6`, per-secret try/catch, and passphrase-aware/format-tolerant key loading
|
|
212
|
+
(passphrase only for `ENCRYPTED PRIVATE KEY`). To add/retire a cert, edit the list only.
|
|
213
|
+
Removed dead disk-path constants (`PATH_TO_ENCRYPTION_PRIVATE_KEY_NEW/_OLD`,
|
|
214
|
+
`PATH_TO_ENCRYPTION_CERTIFICATE_OLD`); `PATH_TO_ENCRYPTION_CERTIFICATE_NEW` stays for `/meta`.
|
|
215
|
+
Added a publicly-trusted (ACM exportable) cert for Prudential, which refuses self-signed + pins
|
|
216
|
+
our leaf + cannot auto-pull metadata; self-signed and public certs now coexist in the array.
|
|
217
|
+
(jcardinal)
|
|
152
218
|
- 2026-08-27 — Created while replacing the SAML SP credential. Documented the SP cert's dual use
|
|
153
219
|
(signing AuthnRequests / decrypting assertions), the new self-signed 10-year cert identity and
|
|
154
220
|
its AWS Secrets Manager location, the pre-cutover TLS-usage safety check, and the
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-09-15
|
|
9
9
|
owners: [jcardinal]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -24,6 +24,8 @@ infra). It was established while replacing the 2.0 SAML SP credential — see th
|
|
|
24
24
|
**Critical rule:** if a partner installs/pins OUR specific cert, use a **self-signed, long-lived**
|
|
25
25
|
cert. If AWS terminates the TLS for browsers/generic clients, use **ACM public, auto-renewing**.
|
|
26
26
|
Never reuse a public-CA TLS cert as a pinned bilateral credential.
|
|
27
|
+
**Exception:** a bilateral partner that **refuses a self-signed cert AND cannot auto-refresh
|
|
28
|
+
metadata** gets an **ACM *exportable* public cert** (see case 1's carve-out) — not ACM Private CA.
|
|
27
29
|
|
|
28
30
|
## The decision
|
|
29
31
|
|
|
@@ -33,6 +35,13 @@ Family), PunchOut endpoints a buyer pins. Use a **self-signed** cert with a mult
|
|
|
33
35
|
(we generate 10-year). Distribute the public cert to the partner; import cert + key into the
|
|
34
36
|
service. Because trust is by pinning, no public CA is involved and validity can be long.
|
|
35
37
|
|
|
38
|
+
**Carve-out — a partner that refuses self-signed AND can't auto-refresh metadata.** Some IdPs
|
|
39
|
+
reject a self-signed SP cert but also won't re-pull our metadata on a schedule, so a short public
|
|
40
|
+
cert would silently break them at every renewal. For these, use an **ACM *exportable* public
|
|
41
|
+
cert** (publicly-trusted so they accept it; exportable so we can install cert+key into the SP).
|
|
42
|
+
Do **not** reach for ACM Private CA (~$400/mo) to solve this — the exportable public cert covers
|
|
43
|
+
it. You still redeploy on each ~198-day renewal (case 3's tradeoff), but the partner is happy.
|
|
44
|
+
|
|
36
45
|
### 2. Standard HTTPS, AWS-terminated → ACM public, non-exportable, auto-renewing
|
|
37
46
|
Browsers / generic clients reach the endpoint through an ALB, CloudFront, or API Gateway that AWS
|
|
38
47
|
terminates. Use an **ACM public certificate** (non-exportable, auto-renewing). Distribute nothing;
|
|
@@ -40,15 +49,28 @@ renewal is invisible.
|
|
|
40
49
|
|
|
41
50
|
### 3. Standard HTTPS, NOT AWS-terminated → ACM exportable, or move behind an ALB
|
|
42
51
|
A server terminates its own TLS (e.g. the legacy Vision box). Options: an **ACM *exportable*
|
|
43
|
-
public cert** (a real 2025 AWS feature) — but
|
|
52
|
+
public cert** (a real 2025 AWS feature) — but only ~198-day validity today, so you must redeploy on
|
|
44
53
|
every renewal — OR move the endpoint behind an ALB to get free auto-renew (option 2).
|
|
45
54
|
|
|
55
|
+
### Hybrid end-state (bilateral endpoints with mixed partners)
|
|
56
|
+
A single bilateral endpoint can end up serving **both** kinds of partner at once: some accept and
|
|
57
|
+
pin our self-signed long-lived cert (case 1), others refuse self-signed and need an exportable
|
|
58
|
+
public cert (the case-1 carve-out). These coexist. The SAML SP `/acs` handler already supports
|
|
59
|
+
this: it holds **multiple** SP encryption cert+key pairs (`ENCRYPTION_SECRET_NAMES` in AWS Secrets
|
|
60
|
+
Manager) and tries each on decrypt, so a self-signed keeper and a public-cert refuser can be live
|
|
61
|
+
side by side — you add/rotate one partner's cert without disturbing the other. See the
|
|
62
|
+
[saml architecture](../apps/saml/architecture.md) `/acs` flow.
|
|
63
|
+
|
|
46
64
|
## Why long-lived ⇒ self-signed
|
|
47
65
|
|
|
48
|
-
**Every publicly-trusted cert, exportable or not, is
|
|
49
|
-
Forum
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
**Every publicly-trusted cert, exportable or not, is short-lived and shrinking** under the
|
|
67
|
+
CA/Browser Forum schedule: ~198 days today (what ACM issues now), stepping down to a **mandatory
|
|
68
|
+
~47-day floor by 2029**. The often-quoted "10 days" is not the cert lifetime — it is the
|
|
69
|
+
**domain-validation reuse window** (how long a prior validation can be reused before you must
|
|
70
|
+
re-validate); the ~6-day Let's Encrypt cert is an **optional** short-lived offering, not the CA/B
|
|
71
|
+
requirement. Only **self-signed / private-CA** certs can be multi-year. So "long-lived AND
|
|
72
|
+
distributable" necessarily means self-signed (or AWS Private CA at ~$400/mo — rejected here as
|
|
73
|
+
overkill).
|
|
52
74
|
|
|
53
75
|
The pain of "shrinking public-cert validity" is really a **distribution problem**: automate
|
|
54
76
|
distribution via ACM and it disappears; a partner who pins the leaf breaks on every rotation.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 84 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 69 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -11,11 +11,12 @@ apps:
|
|
|
11
11
|
- library
|
|
12
12
|
- toga2-commerce
|
|
13
13
|
- toga25-supply
|
|
14
|
+
- saml
|
|
14
15
|
project: _Underscore
|
|
15
16
|
client: prudential
|
|
16
17
|
type: profile
|
|
17
18
|
status: active
|
|
18
|
-
updated: 2026-09-
|
|
19
|
+
updated: 2026-09-15
|
|
19
20
|
owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
|
|
20
21
|
files: []
|
|
21
22
|
related:
|
package/package.json
CHANGED