Skip to content
Open
4 changes: 2 additions & 2 deletions .coverage-exclusions.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,12 +483,12 @@
{
"package": "internal/service",
"file": "internal/service/mint.go",
"line": 393,
"line": 419,
"occurrence": 1,
"source": "\t\treturn nil, ErrMintRoleNotPermitted",
"bucket": "C",
"confirmed": true,
"justification": "checkRoles' second, defence-in-depth role filter. Reaching it needs a role that the loop above accepted, meaning !seed.IsReservedAdminRole(r), and that seed.FilterUserRoles then dropped. FilterUserRoles (internal/seed/seed.go:148) drops exactly the roles for which IsReservedAdminRole(r) is true, so the two predicates are the same case-folded lookup on the same slice and the lengths cannot differ. The early return at internal/service/mint.go:382 means requested is never empty, which is FilterUserRoles' only other way to shorten the result."
"justification": "checkRoles' second, defence-in-depth role filter. Reaching it needs a role that the loop above accepted, meaning !seed.IsReservedAdminRole(r), and that seed.FilterUserRoles then dropped. FilterUserRoles (internal/seed/seed.go:148) drops exactly the roles for which IsReservedAdminRole(r) is true, so the two predicates are the same case-folded lookup on the same slice and the lengths cannot differ. The early return at internal/service/mint.go:407 means requested is never empty, which is FilterUserRoles' only other way to shorten the result."
},
{
"package": "internal/service",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Vault42 issues its own tokens and is an OAuth2 *client* of other providers. It i
| ![Go](https://img.shields.io/badge/Go-1.26.6-00ADD8?style=flat&logo=go&logoColor=white) | ![Vue](https://img.shields.io/badge/Vue-3.5.41-4FC08D?style=flat&logo=vuedotjs&logoColor=white) | ![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat&logo=dotnet&logoColor=white) | ![License](https://img.shields.io/badge/License-MIT-155724?style=flat&labelColor=000) |
| ![Go Tests](https://img.shields.io/badge/Tests-4910-155724?style=flat&labelColor=000) | ![Vue Tests](https://img.shields.io/badge/Tests-1305-155724?style=flat&labelColor=000) | ![C# Tests](https://img.shields.io/badge/Tests-264-155724?style=flat&labelColor=000) | ![Total](https://img.shields.io/badge/Total-6479_tests-155724?style=flat&labelColor=000) |
| ![Go Coverage](https://img.shields.io/badge/Coverage-100.00%25_reachable-155724?style=flat&labelColor=000) | ![Vue Coverage](https://img.shields.io/badge/Coverage-99.76%25-155724?style=flat&labelColor=000) | ![C# Coverage](https://img.shields.io/badge/Coverage-100.00%25-155724?style=flat&labelColor=000) | ![Locales](https://img.shields.io/badge/Locales-38-555?style=flat&labelColor=000) |
| ![Go Lines](https://img.shields.io/badge/Lines-48229-555?style=flat&labelColor=000) | ![Vue Lines](https://img.shields.io/badge/Lines-6800-555?style=flat&labelColor=000) | ![C# Lines](https://img.shields.io/badge/Lines-2435-555?style=flat&labelColor=000) | ![Standards](https://img.shields.io/badge/Standards-11-555?style=flat&labelColor=000) |
| ![Go Lines](https://img.shields.io/badge/Lines-48360-555?style=flat&labelColor=000) | ![Vue Lines](https://img.shields.io/badge/Lines-6801-555?style=flat&labelColor=000) | ![C# Lines](https://img.shields.io/badge/Lines-2435-555?style=flat&labelColor=000) | ![Standards](https://img.shields.io/badge/Standards-11-555?style=flat&labelColor=000) |
| ![Go Deps](https://img.shields.io/badge/Deps-3-555?style=flat&labelColor=000) | ![Vue Deps](https://img.shields.io/badge/Deps-3-555?style=flat&labelColor=000) | ![C# Deps](https://img.shields.io/badge/Deps-6-555?style=flat&labelColor=000) | ![Requirements](https://img.shields.io/badge/Requirements-456-555?style=flat&labelColor=000) |
| ![Go Transitive Deps](https://img.shields.io/badge/Transitive-15-555?style=flat&labelColor=000) | ![Vue Transitive Deps](https://img.shields.io/badge/Transitive-95-555?style=flat&labelColor=000) | ![C# Transitive Deps](https://img.shields.io/badge/Transitive-26-555?style=flat&labelColor=000) | ![Total Deps](https://img.shields.io/badge/Deps-148_total-555?style=flat&labelColor=000) |
<!-- /badges -->
Expand Down
1 change: 1 addition & 0 deletions charts/vault/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ data:
VAULT_MINT_MAX_TTL: {{ .Values.mint.maxTTL | quote }}
VAULT_MINT_ROLES: {{ join "," .Values.mint.allowedRoles | quote }}
VAULT_MINT_SCOPES: {{ join "," .Values.mint.allowedScopes | quote }}
VAULT_MINT_ALLOW_EMAIL: {{ .Values.mint.allowEmail | quote }}
VAULT_EMAIL_FROM: {{ .Values.emailFrom | quote }}
{{- if and (eq .Values.email.provider "sendgrid") (not .Values.secrets.keys.sendgridApiKey) }}
{{- fail "email.provider is sendgrid and secrets.keys.sendgridApiKey is empty. cmd/vault selects the SendGrid sender only when the provider name AND the API key are both present; with the key missing it falls through to SMTP, or to no sender at all when smtp.host is empty, and every verification, reset and one-time-code mail is silently dropped by a deployment that reports healthy. Name the key in the release Secret, or leave the provider on smtp." }}
Expand Down
9 changes: 8 additions & 1 deletion charts/vault/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ outboundAllowPrivate: false
# decision about the caller, not a feature toggle -- see docs/security.md.
#
# Every value below is the binary's own default, so a release that does not set
# mint.enabled is unchanged by this block existing. The four settings beside the
# mint.enabled is unchanged by this block existing. The six settings beside the
# switch are rendered with it because none of them works alone: without an
# audience the process refuses to start, and with empty allow-lists the endpoint
# mounts and grants nothing.
Expand Down Expand Up @@ -366,6 +366,13 @@ mint:
# comma-joined. Empty means no scope may be minted. Capability scopes such as
# kms:unwrap and mint:token are refused regardless of what is listed here.
allowedScopes: []
# -- Permit an email claim on a minted token (VAULT_MINT_ALLOW_EMAIL). Off by
# default. Every other minted claim is a name vault42 issued or allow-listed;
# an email is a caller-supplied identifier for a subject vault42 has never
# heard of, so turning this on says the operator trusts this client to assert
# who its users are. A request carrying an email while this is off is refused
# rather than stripped, so the misconfiguration is visible.
allowEmail: false

# -- Hosts an issuer's discovery document may name beyond the issuer's own
# domain (VAULT_OUTBOUND_ALLOWED_HOSTS), rendered comma-joined. Empty is the
Expand Down
1 change: 1 addition & 0 deletions cmd/vault/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ func main() {
MaxTTL: cfg.MintMaxTTL,
AllowedRoles: cfg.MintAllowedRoles,
AllowedScopes: cfg.MintAllowedScopes,
AllowEmail: cfg.MintAllowEmail,
}, mintMetrics)
if err != nil {
log.Fatalf("Failed to initialize mint service: %v", err)
Expand Down
6 changes: 6 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3165,6 +3165,7 @@ Mounted **only** when `VAULT_MINT_ENABLED=true`; otherwise the route does not ex
| `roles` | string[] | No | Every member must appear in `VAULT_MINT_ROLES` | Omit or send `[]` for no roles. The allow-list is empty by default, so a freshly enabled mint issues bare subject assertions |
| `scopes` | string[] | No | Every member must appear in `VAULT_MINT_SCOPES` | Same deny-by-default rule as `roles` |
| `ttl_seconds` | int | No | `0` or absent means `VAULT_MINT_TOKEN_TTL`; otherwise `0 < ttl <= VAULT_MINT_MAX_TTL`, itself capped at 900 in code | A value above the ceiling is **refused, not clamped**. Silently issuing something other than what was asked for hides a misconfigured caller until the day its tokens expire mid-flight |
| `email` | string | No | Requires `VAULT_MINT_ALLOW_EMAIL`; must pass the same validator as a registered address, at most 254 bytes | Lower-cased and trimmed before signing. Refused with `403 email_not_permitted` when the setting is off, rather than stripped. vault42 does not verify it and cannot: `/mint` asserts subjects it has never heard of, and the email is a claim about the same unknown subject |

Unknown keys are rejected (`DisallowUnknownFields`), so a typo in a field name fails the whole request with `400 invalid_request`.

Expand All @@ -3180,6 +3181,7 @@ Unknown keys are rejected (`DisallowUnknownFields`), so a typo in a field name f
"issuer": "https://vault42.example.com",
"roles": ["rider"],
"scopes": ["orders:read"],
"email": "legacy-user-8814@example.com",
"kid": "4f1c9e60-2a77-4e0f-9a3e-9c2b7f0d51aa",
"jti": "0f2b8c1d-6e4a-4c92-b8a1-2f7d3e5a90c4"
}
Expand All @@ -3195,6 +3197,7 @@ Unknown keys are rejected (`DisallowUnknownFields`), so a typo in a field name f
| `issuer` | string | `VAULT_ORIGIN`, the `iss` claim on the token |
| `roles` | string[] | Granted roles. Omitted when none were requested |
| `scopes` | string[] | Granted scopes. Omitted when none were requested |
| `email` | string | Echo of the asserted address as it was signed, lower-cased and trimmed. Omitted when the request carried none. Present so a caller can see what it actually asserted rather than what it meant to send |
| `kid` | string | Key id the assertion was signed under, resolvable against `GET /.well-known/jwks.json` |
| `jti` | string | The token's unique id, also recorded in the audit event so a downstream incident traces back to the exact assertion |

Expand All @@ -3212,6 +3215,7 @@ Unknown keys are rejected (`DisallowUnknownFields`), so a typo in a field name f
| `token_type` | `mint` |
| `minted_by` | The `client_id` of the client that requested the mint. This is the attribution a relying party can act on: the `token_minted` audit event names the same client, but that row lives in vault42's database and an RP cannot read it |
| `client_id` | **Absent, deliberately.** A minted token must not look like an authenticated service caller. The service document store treats the presence of this claim as proof of one and uses it as the ownership axis, so a minted token carrying it would be admitted as the minting client. That is why the attribution claim is spelled `minted_by`. See the security notes below |
| `email` | The address the caller asserted, present only when `VAULT_MINT_ALLOW_EMAIL` is on and the request carried one. **Not verified.** vault42 never looked this subject up, so the claim is the caller's statement rather than vault42's. A relying party must not treat it as proof of address ownership, and no vault42-issued login token carries this claim at all |
| `fingerprint`, `cnf` | Absent. A minted token is not device-bound and not sender-constrained |

There is no refresh token and no stored session behind a minted token. It cannot be exchanged, rotated, extended or revoked; vault42 keeps no record of it beyond the audit event.
Expand All @@ -3233,6 +3237,8 @@ There is no refresh token and no stored session behind a minted token. It cannot
| 403 | `insufficient_scope` | Token lacks the `mint:token` scope |
| 403 | `client_credentials_required` | Token has the scope but no `client_id` claim, so it is not a service client |
| 403 | `role_not_permitted` | A requested role is outside `VAULT_MINT_ROLES`, or is `admin` or `super_admin` in any casing |
| 403 | `email_not_permitted` | The request carried `email` and `VAULT_MINT_ALLOW_EMAIL` is off |
| 400 | `invalid_email` | The request carried an `email` the address validator rejected |
| 403 | `scope_not_permitted` | A requested scope is outside `VAULT_MINT_SCOPES`, or is one of the vault42 capability scopes |
| 429 | `rate_limit_exceeded` | Rate limit exceeded |
| 500 | `internal_error` | Signing or UUID generation failed |
Expand Down
8 changes: 4 additions & 4 deletions docs/badges.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"reachableCoverageNum": 100.00,
"packages": 43,
"goFiles": 192,
"goLines": 48229,
"testFiles": 928,
"goLines": 48360,
"testFiles": 929,
"directDeps": 3,
"transitiveDeps": 15,
"totalTests": 6479,
Expand All @@ -20,15 +20,15 @@
"tests": 4910,
"coverage": "100.00% of reachable",
"coverageNum": 100.00,
"lines": 48229,
"lines": 48360,
"deps": 3,
"transitiveDeps": 15
},
"vue": {
"tests": 1305,
"coverage": "99.76%",
"coverageNum": 99.76,
"lines": 6800,
"lines": 6801,
"deps": 3,
"transitiveDeps": 95
},
Expand Down
Loading
Loading