KEY-WP-0045: value-safe identity-facts export for NK-WP-0054-T04
keycape identity-facts --config PATH and make identity-facts print client registry, directory bind and admin-surface facts without credential values or secret reference names. Fixture and committed-config tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 308701@bnt-lap001 Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
This commit is contained in:
parent
1af86580e3
commit
a1ab8ff807
7 changed files with 433 additions and 1 deletions
7
Makefile
7
Makefile
|
|
@ -3,7 +3,7 @@ IMAGE_REPO ?= coulomb/key-cape
|
|||
IMAGE_TAG ?= latest
|
||||
IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG)
|
||||
|
||||
.PHONY: dev seed build test lint contract-test image push image-tag tokenstatus-export tokenstatus-check
|
||||
.PHONY: dev seed build test lint contract-test image push image-tag tokenstatus-export tokenstatus-check identity-facts
|
||||
|
||||
PLAYBOOK_VALIDATOR ?= ../net-kingdom/tools/playbook-capability-contract/playbook_contract_validator.py
|
||||
|
||||
|
|
@ -25,6 +25,11 @@ tokenstatus-export:
|
|||
tokenstatus-check:
|
||||
python3 tools/export_tokenstatus_client.py --check
|
||||
|
||||
# KEY-WP-0045: value-safe identity facts for NetKingdom's service-identity check.
|
||||
IDENTITY_CONFIG ?= config/service-clients.example.yaml
|
||||
identity-facts:
|
||||
@cd src && go run ./cmd/keycape identity-facts --config $(abspath $(IDENTITY_CONFIG))
|
||||
|
||||
lint:
|
||||
cd src && go vet ./...
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
| workplan | KEY-WP-0042 | proposed | — | workplans/KEY-WP-0042-online-access-token-status.md |
|
||||
| workplan | KEY-WP-0043 | proposed | — | workplans/KEY-WP-0043-per-run-agent-identity.md |
|
||||
| workplan | KEY-WP-0044 | proposed | — | workplans/KEY-WP-0044-audit-correlation-and-intake.md |
|
||||
| workplan | KEY-WP-0045 | active | — | workplans/KEY-WP-0045-service-identity-facts-export.md |
|
||||
| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md |
|
||||
| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md |
|
||||
| task | ADHOC-2026-09-27-T01 | done | — | workplans/ADHOC-2026-09-27.md |
|
||||
|
|
@ -235,4 +236,7 @@
|
|||
| task | KEY-WP-0044-T01 | todo | — | workplans/KEY-WP-0044-audit-correlation-and-intake.md |
|
||||
| task | KEY-WP-0044-T02 | todo | — | workplans/KEY-WP-0044-audit-correlation-and-intake.md |
|
||||
| task | KEY-WP-0044-T03 | wait | — | workplans/KEY-WP-0044-audit-correlation-and-intake.md |
|
||||
| task | KEY-WP-0045-T01 | done | — | workplans/KEY-WP-0045-service-identity-facts-export.md |
|
||||
| task | KEY-WP-0045-T02 | wait | — | workplans/KEY-WP-0045-service-identity-facts-export.md |
|
||||
| task | KEY-WP-0045-T03 | todo | — | workplans/KEY-WP-0045-service-identity-facts-export.md |
|
||||
| intake | KEY-IN-0001 | closed | — | intakes/intakes.md |
|
||||
|
|
|
|||
220
src/cmd/keycape/identityfacts.go
Normal file
220
src/cmd/keycape/identityfacts.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"keycape/internal/config"
|
||||
)
|
||||
|
||||
// identity-facts (KEY-WP-0045, NK-WP-0054-T04) exports the identity facts a
|
||||
// KeyCape config establishes, for NetKingdom's service-identity conformance
|
||||
// check. It is value-safe by construction: it copies only allow-listed
|
||||
// registration fields and reports every credential-bearing field (client
|
||||
// secretRef, LLDAP bindPW, privacyIDEA adminToken, Authelia clientSecret,
|
||||
// privateKeyPem) as a kind or a boolean, never its content. Like check-config it
|
||||
// reads no secret, key file or upstream, so it can run over a committed config
|
||||
// or, run by the founder inside the pod, over the deployed one.
|
||||
|
||||
const identityFactsSchema = "keycape-identity-facts/v0.1"
|
||||
|
||||
type identityFacts struct {
|
||||
Schema string `json:"schema"`
|
||||
Issuer string `json:"issuer"`
|
||||
Environment string `json:"environment"`
|
||||
TokenLifetime string `json:"token_lifetime"`
|
||||
Clients []clientFacts `json:"clients"`
|
||||
DirectoryBind directoryBindFacts `json:"directory_bind"`
|
||||
FactorService factorFacts `json:"factor_service"`
|
||||
UpstreamOIDC upstreamFacts `json:"upstream_oidc"`
|
||||
AdminSurfaces []adminSurface `json:"admin_surfaces"`
|
||||
Findings []string `json:"findings"`
|
||||
}
|
||||
|
||||
type clientFacts struct {
|
||||
ClientID string `json:"client_id"`
|
||||
PrincipalType string `json:"principal_type"`
|
||||
ClientType string `json:"client_type"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
Audience string `json:"audience"`
|
||||
AudienceSource string `json:"audience_source"`
|
||||
Scopes []string `json:"scopes"`
|
||||
ServiceSubject string `json:"service_subject,omitempty"`
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
Roles []string `json:"roles"`
|
||||
TokenLifetime string `json:"token_lifetime"`
|
||||
MFARequired *bool `json:"mfa_required,omitempty"`
|
||||
Credential string `json:"credential"`
|
||||
}
|
||||
|
||||
type directoryBindFacts struct {
|
||||
URL string `json:"url"`
|
||||
BindDN string `json:"bind_dn"`
|
||||
BaseDN string `json:"base_dn"`
|
||||
PasswordInConfig bool `json:"password_in_config"`
|
||||
BindIsDirectoryAdmin bool `json:"bind_is_directory_admin"`
|
||||
}
|
||||
|
||||
type factorFacts struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Realm string `json:"realm"`
|
||||
AdminCredential string `json:"admin_credential"`
|
||||
RequireForAll bool `json:"require_for_all"`
|
||||
}
|
||||
|
||||
type upstreamFacts struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
}
|
||||
|
||||
type adminSurface struct {
|
||||
Path string `json:"path"`
|
||||
Enabled string `json:"enabled"`
|
||||
Authority string `json:"authority"`
|
||||
}
|
||||
|
||||
// principalType maps a registration to the SecurityCanon principal type: a
|
||||
// client that only uses client_credentials is a SERVICE; anything that signs a
|
||||
// directory user in is presented on behalf of a HUMAN.
|
||||
func principalType(grants []string) string {
|
||||
for _, g := range grants {
|
||||
if g != "client_credentials" {
|
||||
return "HUMAN"
|
||||
}
|
||||
}
|
||||
if len(grants) == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
return "SERVICE"
|
||||
}
|
||||
|
||||
func credentialKind(secretRef string) string {
|
||||
switch {
|
||||
case secretRef == "":
|
||||
return "none"
|
||||
case strings.HasPrefix(secretRef, "env:"):
|
||||
return "env-ref"
|
||||
default:
|
||||
return "other-ref"
|
||||
}
|
||||
}
|
||||
|
||||
func presence(inline string, file string) string {
|
||||
switch {
|
||||
case inline != "":
|
||||
return "inline"
|
||||
case file != "":
|
||||
return "file"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
func orEmpty(s []string) []string {
|
||||
if s == nil {
|
||||
return []string{}
|
||||
}
|
||||
out := append([]string(nil), s...)
|
||||
return out
|
||||
}
|
||||
|
||||
func buildIdentityFacts(cfg *config.Config) identityFacts {
|
||||
facts := identityFacts{
|
||||
Schema: identityFactsSchema,
|
||||
Issuer: cfg.Issuer,
|
||||
Environment: cfg.Environment,
|
||||
TokenLifetime: cfg.TokenLifetime,
|
||||
Clients: []clientFacts{},
|
||||
Findings: []string{},
|
||||
}
|
||||
for _, c := range cfg.Clients {
|
||||
cf := clientFacts{
|
||||
ClientID: c.ClientID,
|
||||
PrincipalType: principalType(c.GrantTypes),
|
||||
ClientType: c.ClientType,
|
||||
GrantTypes: orEmpty(c.GrantTypes),
|
||||
Audience: c.Audience,
|
||||
AudienceSource: "registration",
|
||||
Scopes: orEmpty(c.AllowedScopes),
|
||||
ServiceSubject: c.ServiceSubject,
|
||||
Tenant: c.Tenant,
|
||||
Roles: orEmpty(c.Roles),
|
||||
TokenLifetime: c.TokenLifetime,
|
||||
MFARequired: c.MFARequired,
|
||||
Credential: credentialKind(c.SecretRef),
|
||||
}
|
||||
if cf.Audience == "" {
|
||||
// token.go accessAudience: the access token falls back to the client id.
|
||||
cf.Audience, cf.AudienceSource = c.ClientID, "client-id-default"
|
||||
}
|
||||
if cf.TokenLifetime == "" {
|
||||
cf.TokenLifetime = cfg.TokenLifetime
|
||||
}
|
||||
facts.Clients = append(facts.Clients, cf)
|
||||
}
|
||||
sort.Slice(facts.Clients, func(i, j int) bool { return facts.Clients[i].ClientID < facts.Clients[j].ClientID })
|
||||
|
||||
bind := strings.ToLower(strings.TrimSpace(cfg.LLDAP.BindDN))
|
||||
facts.DirectoryBind = directoryBindFacts{
|
||||
URL: cfg.LLDAP.URL,
|
||||
BindDN: cfg.LLDAP.BindDN,
|
||||
BaseDN: cfg.LLDAP.BaseDN,
|
||||
PasswordInConfig: cfg.LLDAP.BindPW != "",
|
||||
BindIsDirectoryAdmin: strings.HasPrefix(bind, "uid=admin,") || strings.HasPrefix(bind, "cn=admin,"),
|
||||
}
|
||||
if facts.DirectoryBind.BindIsDirectoryAdmin {
|
||||
facts.Findings = append(facts.Findings, "directory bind uses the directory administrator account, not a read-only bind identity")
|
||||
}
|
||||
facts.FactorService = factorFacts{
|
||||
BaseURL: cfg.PrivacyIDEA.BaseURL,
|
||||
Realm: cfg.PrivacyIDEA.Realm,
|
||||
AdminCredential: presence(cfg.PrivacyIDEA.AdminToken, cfg.PrivacyIDEA.AdminTokenFile),
|
||||
RequireForAll: cfg.PrivacyIDEA.RequireForAll,
|
||||
}
|
||||
if facts.FactorService.AdminCredential == "inline" {
|
||||
facts.Findings = append(facts.Findings, "privacyIDEA admin token is inline in the config rather than a mounted file")
|
||||
}
|
||||
facts.UpstreamOIDC = upstreamFacts{
|
||||
BaseURL: cfg.Authelia.BaseURL,
|
||||
ClientID: cfg.Authelia.ClientID,
|
||||
ClientSecret: presence(cfg.Authelia.ClientSecret, ""),
|
||||
}
|
||||
|
||||
tokenStatus := "no"
|
||||
if cfg.TokenStatus.Enabled {
|
||||
tokenStatus = "yes"
|
||||
}
|
||||
// Routes are fixed in cmd/keycape/main.go; these are the only ones that
|
||||
// change or reveal state beyond the caller's own sign-in.
|
||||
facts.AdminSurfaces = []adminSurface{
|
||||
{Path: "/platform/authentication-policy", Enabled: "when KEYCAPE_POLICY_PATH is set (deployment env, not config)",
|
||||
Authority: "KeyCape token with azp user-engine-portal, role platform-operator, assurance aal2 with mfa within 300s"},
|
||||
{Path: "/introspect", Enabled: tokenStatus, Authority: "confidential client authentication; only tokens issued to the caller"},
|
||||
{Path: "/revoke", Enabled: tokenStatus, Authority: "confidential client authentication; only tokens issued to the caller"},
|
||||
}
|
||||
return facts
|
||||
}
|
||||
|
||||
func runIdentityFacts(args []string, out io.Writer) int {
|
||||
fs := flag.NewFlagSet("identity-facts", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
cfgPath := fs.String("config", "", "path to YAML config file")
|
||||
if err := fs.Parse(args); err != nil || *cfgPath == "" {
|
||||
_, _ = io.WriteString(out, "{\"error\":\"usage: keycape identity-facts --config PATH\"}\n")
|
||||
return 1
|
||||
}
|
||||
cfg, err := config.Load(*cfgPath)
|
||||
if err != nil {
|
||||
// The parse error can quote config content; say only that it failed.
|
||||
_, _ = io.WriteString(out, "{\"error\":\"config does not load\"}\n")
|
||||
return 1
|
||||
}
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(buildIdentityFacts(cfg))
|
||||
return 0
|
||||
}
|
||||
89
src/cmd/keycape/identityfacts_test.go
Normal file
89
src/cmd/keycape/identityfacts_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runFacts(t *testing.T, path string) (identityFacts, string) {
|
||||
t.Helper()
|
||||
var out bytes.Buffer
|
||||
if code := runIdentityFacts([]string{"--config", path}, &out); code != 0 {
|
||||
t.Fatalf("exit %d: %s", code, out.String())
|
||||
}
|
||||
var facts identityFacts
|
||||
if err := json.Unmarshal(out.Bytes(), &facts); err != nil {
|
||||
t.Fatalf("output is not JSON: %v", err)
|
||||
}
|
||||
return facts, out.String()
|
||||
}
|
||||
|
||||
func TestIdentityFactsFixtureIsValueSafe(t *testing.T) {
|
||||
facts, raw := runFacts(t, "testdata/identity-facts.yaml")
|
||||
if strings.Contains(raw, "CANARY") {
|
||||
t.Fatalf("a credential value or reference name reached the export:\n%s", raw)
|
||||
}
|
||||
if facts.Schema != identityFactsSchema || len(facts.Clients) != 2 {
|
||||
t.Fatalf("unexpected facts: %+v", facts)
|
||||
}
|
||||
browser, svc := facts.Clients[0], facts.Clients[1]
|
||||
if svc.ClientID != "svc-reader" || svc.PrincipalType != "SERVICE" || svc.Credential != "env-ref" ||
|
||||
svc.Audience != "svc-reader" || svc.AudienceSource != "client-id-default" ||
|
||||
!reflect.DeepEqual(svc.Scopes, []string{"thing:read"}) || svc.TokenLifetime != "5m" {
|
||||
t.Fatalf("service client: %+v", svc)
|
||||
}
|
||||
if browser.ClientID != "browser-app" || browser.PrincipalType != "HUMAN" || browser.Credential != "none" ||
|
||||
browser.Audience != "resource-api" || browser.AudienceSource != "registration" ||
|
||||
browser.TokenLifetime != "15m" || browser.MFARequired == nil || !*browser.MFARequired {
|
||||
t.Fatalf("browser client: %+v", browser)
|
||||
}
|
||||
d := facts.DirectoryBind
|
||||
if d.BindDN != "uid=admin,ou=people,dc=example,dc=test" || !d.PasswordInConfig || !d.BindIsDirectoryAdmin {
|
||||
t.Fatalf("directory bind: %+v", d)
|
||||
}
|
||||
if facts.FactorService.AdminCredential != "inline" || facts.UpstreamOIDC.ClientSecret != "inline" {
|
||||
t.Fatalf("credential kinds: %+v %+v", facts.FactorService, facts.UpstreamOIDC)
|
||||
}
|
||||
if len(facts.Findings) != 2 || len(facts.AdminSurfaces) != 3 || facts.AdminSurfaces[1].Enabled != "yes" {
|
||||
t.Fatalf("findings/admin surfaces: %+v %+v", facts.Findings, facts.AdminSurfaces)
|
||||
}
|
||||
}
|
||||
|
||||
// The committed registration fragments establish the token scopes of the
|
||||
// clients NetKingdom's keycape declaration lists as unknown (NK-WP-0054-T04).
|
||||
func TestIdentityFactsCommittedServiceClients(t *testing.T) {
|
||||
facts, raw := runFacts(t, "../../../config/service-clients.example.yaml")
|
||||
if strings.Contains(raw, "_CLIENT_SECRET") || strings.Contains(raw, "env:") {
|
||||
t.Fatalf("secret reference names reached the export")
|
||||
}
|
||||
want := map[string][]string{
|
||||
"secrets-engine-approval": {"approval:read", "approval:consume"},
|
||||
"secrets-engine-requester": {"approval:create"},
|
||||
"approval-engine-operator": {"approval:create", "approval:read", "approval:approve", "approval:revoke", "approval:supersede", "approval:observe", "approval:emit"},
|
||||
"informed-decision-sitting-requester": {"approval:create"},
|
||||
"secrets-engine-openbao": {"openbao:login"},
|
||||
}
|
||||
seen := 0
|
||||
for _, c := range facts.Clients {
|
||||
if scopes, ok := want[c.ClientID]; ok {
|
||||
seen++
|
||||
if !reflect.DeepEqual(c.Scopes, scopes) || c.PrincipalType != "SERVICE" {
|
||||
t.Fatalf("%s: %+v", c.ClientID, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if seen != len(want) {
|
||||
t.Fatalf("found %d of %d clients", seen, len(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityFactsRefusesUnloadableConfigQuietly(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if code := runIdentityFacts([]string{"--config", "testdata/missing.yaml"}, &out); code != 1 ||
|
||||
!strings.Contains(out.String(), "config does not load") {
|
||||
t.Fatalf("exit %d: %s", code, out.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,10 @@ func main() {
|
|||
os.Exit(runCheckConfig(os.Args[2:], os.Stdout))
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "identity-facts" {
|
||||
os.Exit(runIdentityFacts(os.Args[2:], os.Stdout))
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 && os.Args[1] == "probe-upstream-issuer" {
|
||||
if err := authelia.RunIssuerProbe(context.Background(), os.Args[2:], os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
|
|
|||
39
src/cmd/keycape/testdata/identity-facts.yaml
vendored
Normal file
39
src/cmd/keycape/testdata/identity-facts.yaml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Synthetic fixture for keycape identity-facts. Every credential-shaped field holds
|
||||
# a CANARY marker so the test can prove no such value reaches the export.
|
||||
issuer: "https://issuer.example.test"
|
||||
environment: "test"
|
||||
tokenLifetime: "15m"
|
||||
privateKeyPem: "/etc/keycape/key.pem"
|
||||
lldap:
|
||||
url: "ldap://lldap.example.test:3890"
|
||||
bindDN: "uid=admin,ou=people,dc=example,dc=test"
|
||||
bindPW: "CANARY-bind-password"
|
||||
baseDN: "dc=example,dc=test"
|
||||
authelia:
|
||||
baseURL: "https://auth.example.test"
|
||||
clientId: "keycape"
|
||||
clientSecret: "CANARY-upstream-secret"
|
||||
privacyidea:
|
||||
baseURL: "http://pi.example.test:8080"
|
||||
realm: "example"
|
||||
adminToken: "CANARY-admin-token"
|
||||
requireForAll: true
|
||||
tokenStatus:
|
||||
enabled: true
|
||||
clients:
|
||||
- clientId: "svc-reader"
|
||||
allowedScopes: ["thing:read"]
|
||||
grantTypes: ["client_credentials"]
|
||||
clientType: "confidential"
|
||||
secretRef: "env:CANARY_SVC_READER_SECRET"
|
||||
serviceSubject: "service:reader"
|
||||
tenant: "tenant:platform"
|
||||
roles: ["reader"]
|
||||
tokenLifetime: "5m"
|
||||
- clientId: "browser-app"
|
||||
audience: "resource-api"
|
||||
redirectUris: ["https://app.example.test/cb"]
|
||||
allowedScopes: ["openid", "thing:approve"]
|
||||
grantTypes: ["authorization_code"]
|
||||
clientType: "public"
|
||||
mfaRequired: true
|
||||
71
workplans/KEY-WP-0045-service-identity-facts-export.md
Normal file
71
workplans/KEY-WP-0045-service-identity-facts-export.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
id: KEY-WP-0045
|
||||
type: workplan
|
||||
title: "Value-safe identity facts export for the service-identity conformance check"
|
||||
domain: infotech
|
||||
repo: key-cape
|
||||
status: active
|
||||
owner: claude-code
|
||||
topic_slug: custodian
|
||||
created: "2026-10-28"
|
||||
updated: "2026-10-28"
|
||||
flavor: extension
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
Accepted from net-kingdom NK-WP-0054-T04 (State Hub message 74fdd863). NetKingdom's
|
||||
`tools/service-identity/validate.py` compares declared identity facts with observed
|
||||
ones under `canon/standards/service-identity-privilege_v0.1.md`; KeyCape supplies the
|
||||
observed side as non-secret metadata. Nothing here reads a Secret, changes a live
|
||||
deployment or registers anything.
|
||||
|
||||
## Export identity facts from a KeyCape config
|
||||
|
||||
```task
|
||||
id: KEY-WP-0045-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
`keycape identity-facts --config PATH` (`src/cmd/keycape/identityfacts.go`) and
|
||||
`make identity-facts [IDENTITY_CONFIG=path]` print schema
|
||||
`keycape-identity-facts/v0.1`: per client the id, principal type, client type, grant
|
||||
types, access-token audience (and whether it is registered or the client-id
|
||||
default), scopes, service subject, tenant, roles, lifetime, MFA requirement and the
|
||||
credential *kind*; the LLDAP bind DN, base DN and whether the bind is the directory
|
||||
administrator; the privacyIDEA and Authelia credential kinds; and the fixed admin
|
||||
surfaces. Credential fields and secret reference names are never copied. Tests:
|
||||
`src/cmd/keycape/identityfacts_test.go` over a canary fixture and the committed
|
||||
`config/service-clients.example.yaml`.
|
||||
|
||||
## Live view of the deployed registry
|
||||
|
||||
```task
|
||||
id: KEY-WP-0045-T02
|
||||
status: wait
|
||||
priority: medium
|
||||
```
|
||||
|
||||
The deployed config is the `config.yaml` key of Secret `sso/keycape-config`, and the
|
||||
running image is distroless and predates this subcommand, so an agent cannot produce
|
||||
the live view. Founder-run, streaming the config through the exporter without
|
||||
writing it to disk:
|
||||
|
||||
kubectl -n sso get secret keycape-config -o jsonpath='{.data.config\.yaml}' \
|
||||
| base64 -d | (cd ~/key-cape/src && go run ./cmd/keycape identity-facts --config /dev/stdin) \
|
||||
> ~/keycape-identity-facts.live.json
|
||||
|
||||
The output is value-safe and can be handed to NetKingdom. Waiting on the founder.
|
||||
|
||||
## Directory bind identity
|
||||
|
||||
```task
|
||||
id: KEY-WP-0045-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
The NetKingdom reference config binds LLDAP as `uid=admin,ou=people,...`, the
|
||||
directory administrator; KeyCape only needs read access (`internal/adapters/lldap`).
|
||||
A read-only bind account is a cross-repo change (LLDAP owner, custody, rollout) and
|
||||
needs a NetKingdom/founder decision before any work.
|
||||
Loading…
Add table
Add a link
Reference in a new issue