smcli: self-healing sessions — refresh-token grant + auto-relogin
ci / vet (pull_request) Successful in 1m9s
ci / vet (pull_request) Successful in 1m9s
Vault access tokens expire (~10min); consumers hit 401s after container recreates. Now: refresh_token persisted at login and rotated on every refresh (Vaultwarden semantics); 401 on an authed call refreshes once and retries; if refresh is unavailable, full relogin (password+TOTP injected by the sm shims from the TSGCOO vault-account env) is attempted before failing. cmdLogin passes the TOTP seed again (regression). QA: corrupted access_token -> TSGCOO sm read self-healed end-to-end. Ticket: https://projects.knownelement.com/issues/832
This commit is contained in:
+31
-1
@@ -21,6 +21,8 @@ type Client struct {
|
|||||||
HTTP *http.Client
|
HTTP *http.Client
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
|
TOTPSecret string
|
||||||
|
reloginDone bool
|
||||||
|
|
||||||
AccessToken string
|
AccessToken string
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
@@ -180,7 +182,7 @@ func (c *Client) Login() error {
|
|||||||
if t.AccessToken == "" {
|
if t.AccessToken == "" {
|
||||||
// 2FA retry path (provider 0 = authenticator TOTP)
|
// 2FA retry path (provider 0 = authenticator TOTP)
|
||||||
if strings.Contains(string(out), "Two factor required") {
|
if strings.Contains(string(out), "Two factor required") {
|
||||||
secret := os.Getenv("SM_TOTP_SECRET")
|
secret := c.TOTPSecret
|
||||||
if secret != "" {
|
if secret != "" {
|
||||||
code, terr := totpNow(secret, time.Now())
|
code, terr := totpNow(secret, time.Now())
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
@@ -327,3 +329,31 @@ func (c *Client) DeleteCipher(id string) error {
|
|||||||
_, _ = c.api("PUT", "/api/ciphers/"+id+"/purge", map[string]any{}, true)
|
_, _ = c.api("PUT", "/api/ciphers/"+id+"/purge", map[string]any{}, true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selfRelogin performs the full login+unlock using SM_* env credentials
|
||||||
|
// (injected by the sm shims from the TSGCOO vault-account env). Saves state.
|
||||||
|
func (c *Client) selfRelogin() error {
|
||||||
|
if c.Password == "" {
|
||||||
|
c.Password = os.Getenv("SM_PASSWORD")
|
||||||
|
}
|
||||||
|
if c.TOTPSecret == "" {
|
||||||
|
c.TOTPSecret = os.Getenv("SM_TOTP_SECRET")
|
||||||
|
}
|
||||||
|
if c.Password == "" {
|
||||||
|
return errors.New("relogin unavailable: SM_PASSWORD not set")
|
||||||
|
}
|
||||||
|
if err := c.Login(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Unlock(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s, err := loadState(); err == nil {
|
||||||
|
s.AccessToken = c.AccessToken
|
||||||
|
s.UserSymKey = toHex(c.UserSymKey)
|
||||||
|
s.StretchedKey = toHex(c.StretchedKey)
|
||||||
|
s.MasterKey = toHex(c.MasterKey)
|
||||||
|
_ = saveState(s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func loadState() (*State, error) {
|
|||||||
func newClientFromState(s *State) (*Client, error) {
|
func newClientFromState(s *State) (*Client, error) {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
Server: s.Server, Email: s.Email,
|
Server: s.Server, Email: s.Email,
|
||||||
|
Password: os.Getenv("SM_PASSWORD"), TOTPSecret: os.Getenv("SM_TOTP_SECRET"),
|
||||||
AccessToken: s.AccessToken, RefreshToken: s.RefreshToken,
|
AccessToken: s.AccessToken, RefreshToken: s.RefreshToken,
|
||||||
KDFType: s.KDFType, KDFIter: s.KDFIter, KDFMemory: s.KDFMemory, KDFParallel: s.KDFParallel,
|
KDFType: s.KDFType, KDFIter: s.KDFIter, KDFMemory: s.KDFMemory, KDFParallel: s.KDFParallel,
|
||||||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||||
@@ -114,14 +115,15 @@ func newClientFromState(s *State) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdLogin(server, email, password string) error {
|
func cmdLogin(server, email, password string) error {
|
||||||
c := &Client{Server: server, Email: email, Password: password,
|
c := &Client{Server: server, Email: email, Password: password, TOTPSecret: os.Getenv("SM_TOTP_SECRET"),
|
||||||
HTTP: &http.Client{Timeout: 30 * time.Second}}
|
HTTP: &http.Client{Timeout: 30 * time.Second}}
|
||||||
if err := c.Login(); err != nil {
|
if err := c.Login(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s := &State{
|
s := &State{
|
||||||
Version: stateVersion, Server: server, Email: email,
|
Version: stateVersion, Server: server, Email: email,
|
||||||
AccessToken: c.AccessToken, RefreshToken: c.RefreshToken,
|
AccessToken: c.AccessToken,
|
||||||
|
RefreshToken: c.RefreshToken,
|
||||||
KDFType: c.KDFType, KDFIter: c.KDFIter, KDFMemory: c.KDFMemory, KDFParallel: c.KDFParallel,
|
KDFType: c.KDFType, KDFIter: c.KDFIter, KDFMemory: c.KDFMemory, KDFParallel: c.KDFParallel,
|
||||||
MasterKey: toHex(c.MasterKey),
|
MasterKey: toHex(c.MasterKey),
|
||||||
StretchedKey: toHex(c.StretchedKey),
|
StretchedKey: toHex(c.StretchedKey),
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
name: knel-secretsmanager
|
name: knel-secretsmanager
|
||||||
services:
|
services:
|
||||||
smcli:
|
smcli:
|
||||||
image: git.knownelement.com/knel/knel-secretsmanager-cli@sha256:46d80c0a0ef53a9303dd54b3799a64a61cd4e4dc892282bc4d8a220378117ce9
|
image: git.knownelement.com/knel/knel-secretsmanager-cli@sha256:8abfc55dfa7ca9e70b286a249b7ca823531bd55da29bed70d3354a58ec4d8fec
|
||||||
container_name: ukrrs-secretsmgr-cli
|
container_name: ukrrs-secretsmgr-cli
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: ["sleep", "infinity"]
|
entrypoint: ["sleep", "infinity"]
|
||||||
|
|||||||
Reference in New Issue
Block a user