If the IdP is misconfigured, unreachable, or the admin group is a typo, the
operator is locked out of their own server with no way back short of editing
JSON on disk. A fallback that only matters when everything else is broken is
exactly the thing you cannot add later - by then you cannot get in to add it.
Stored as PBKDF2-HMAC-SHA256 from the standard library (Go 1.24+ has it, so no
dependency), 600k iterations, per-credential salt. A password rather than a
bearer token on purpose: a break-glass credential is the one most likely to end
up in a backup or a config-management repo, and a hash survives that where a
token does not. There is no email reset flow and should not be -
--set-admin-password on the host is the reset, and whoever can run it already
has the machine.
The password is read from stdin, never a flag, so it stays out of shell history
and the process list; piping still works for automation.
Details the tests pin, each for a reason:
- the username is compared in constant time too, or a fast rejection is a
timing oracle for which usernames exist;
- the *stored* iteration count is used, so raising the constant later does not
lock out existing passwords;
- the throttle grows with consecutive failures but stays bounded and forgives
after a quiet minute - a break-glass credential an attacker can lock out is
a denial of service against the one person who needs it;
- sessions are MAC-checked before anything in them is read, and rotating the
secret invalidates every one at once, which is how they are revoked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ce6d0c2f64
commit
89a5ff9139
@@ -16,6 +16,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"echo-lot.app/server/internal/adminauth"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -59,6 +61,12 @@ type Store struct {
|
||||
type fileData struct {
|
||||
Tokens []EnrollToken `json:"tokens"`
|
||||
Devices []Device `json:"devices"`
|
||||
// The break-glass admin. Absent until an operator sets one.
|
||||
LocalAdmin *adminauth.Credential `json:"local_admin,omitempty"`
|
||||
// Signing secret for admin session cookies. Persisted so sessions survive a restart;
|
||||
// deleting it from the state file invalidates every session at once, which is how an
|
||||
// operator revokes them.
|
||||
SessionSecret string `json:"session_secret,omitempty"`
|
||||
}
|
||||
|
||||
func Open(stateDir string) (*Store, error) {
|
||||
@@ -177,6 +185,50 @@ func (s *Store) DeleteDevice(id string) error {
|
||||
return errors.New("no such device")
|
||||
}
|
||||
|
||||
// SetLocalAdmin stores (or replaces) the break-glass admin password.
|
||||
func (s *Store) SetLocalAdmin(c adminauth.Credential) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.data.LocalAdmin = &c
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// LocalAdmin returns the configured break-glass admin, or nil.
|
||||
func (s *Store) LocalAdmin() *adminauth.Credential {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.data.LocalAdmin == nil {
|
||||
return nil
|
||||
}
|
||||
c := *s.data.LocalAdmin
|
||||
return &c
|
||||
}
|
||||
|
||||
// ClearLocalAdmin removes the break-glass admin.
|
||||
func (s *Store) ClearLocalAdmin() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.data.LocalAdmin = nil
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// SessionSecret returns the admin session signing secret, creating one on first use.
|
||||
func (s *Store) SessionSecret() ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.data.SessionSecret != "" {
|
||||
if b, err := hex.DecodeString(s.data.SessionSecret); err == nil && len(b) >= 32 {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
b, err := adminauth.NewSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.data.SessionSecret = hex.EncodeToString(b)
|
||||
return b, s.save()
|
||||
}
|
||||
|
||||
func (s *Store) DeviceByCredential(cred string) *Device {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user