···
"github.com/bluesky-social/indigo/atproto/syntax"
···
type OpenBaoManager struct {
···
-
func NewOpenBaoManager(address, roleID, secretID string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
-
return nil, fmt.Errorf("address cannot be empty")
-
return nil, fmt.Errorf("role_id cannot be empty")
-
return nil, fmt.Errorf("secret_id cannot be empty")
config := vault.DefaultConfig()
-
config.Address = address
client, err := vault.NewClient(config)
return nil, fmt.Errorf("failed to create openbao client: %w", err)
-
// Authenticate using AppRole
-
err = authenticateAppRole(client, roleID, secretID)
-
return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
manager := &OpenBaoManager{
mountPath: "spindle", // default KV v2 mount path
-
stopCh: make(chan struct{}),
···
-
go manager.tokenRenewalLoop()
-
// authenticateAppRole authenticates the client using AppRole method
-
func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
-
authData := map[string]interface{}{
-
resp, err := client.Logical().Write("auth/approle/login", authData)
-
return fmt.Errorf("failed to login with AppRole: %w", err)
-
if resp == nil || resp.Auth == nil {
-
return fmt.Errorf("no auth info returned from AppRole login")
-
client.SetToken(resp.Auth.ClientToken)
-
// stop stops the token renewal goroutine
-
func (v *OpenBaoManager) Stop() {
-
// tokenRenewalLoop runs in a background goroutine to automatically renew or re-authenticate tokens
-
func (v *OpenBaoManager) tokenRenewalLoop() {
-
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
-
ctx := context.Background()
-
if err := v.ensureValidToken(ctx); err != nil {
-
v.logger.Error("openbao token renewal failed", "error", err)
-
// ensureValidToken checks if the current token is valid and renews or re-authenticates if needed
-
func (v *OpenBaoManager) ensureValidToken(ctx context.Context) error {
-
defer v.tokenMu.Unlock()
-
// check current token info
-
tokenInfo, err := v.client.Auth().Token().LookupSelf()
-
// token is invalid, need to re-authenticate
-
v.logger.Warn("token lookup failed, re-authenticating", "error", err)
-
return v.reAuthenticate()
-
if tokenInfo == nil || tokenInfo.Data == nil {
-
return v.reAuthenticate()
-
ttlRaw, ok := tokenInfo.Data["ttl"]
-
return v.reAuthenticate()
-
switch t := ttlRaw.(type) {
-
return v.reAuthenticate()
-
// if TTL is less than 5 minutes, try to renew
-
v.logger.Info("token ttl low, attempting renewal", "ttl_seconds", ttl)
-
renewResp, err := v.client.Auth().Token().RenewSelf(3600) // 1h
-
v.logger.Warn("token renewal failed, re-authenticating", "error", err)
-
return v.reAuthenticate()
-
if renewResp == nil || renewResp.Auth == nil {
-
v.logger.Warn("token renewal returned no auth info, re-authenticating")
-
return v.reAuthenticate()
-
v.logger.Info("token renewed successfully", "new_ttl_seconds", renewResp.Auth.LeaseDuration)
-
// reAuthenticate performs a fresh authentication using AppRole
-
func (v *OpenBaoManager) reAuthenticate() error {
-
v.logger.Info("re-authenticating with approle")
-
err := authenticateAppRole(v.client, v.roleID, v.secretID)
-
return fmt.Errorf("re-authentication failed: %w", err)
-
v.logger.Info("re-authentication successful")
func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
-
defer v.tokenMu.RUnlock()
if err := ValidateKey(secret.Key); err != nil {
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
-
fmt.Println(v.mountPath, secretPath)
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err == nil && existing != nil {
return ErrKeyAlreadyPresent
···
"created_by": secret.CreatedBy.String(),
-
_, err = v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData)
return fmt.Errorf("failed to store secret in openbao: %w", err)
func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
-
defer v.tokenMu.RUnlock()
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err != nil || existing == nil {
···
return fmt.Errorf("failed to delete secret from openbao: %w", err)
func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) {
-
defer v.tokenMu.RUnlock()
repoPath := v.buildRepoPath(repo)
-
secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []LockedSecret{}, nil
···
-
secretPath := path.Join(repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
-
continue // Skip secrets we can't read
if secretData == nil || secretData.Data == nil {
···
secrets = append(secrets, secret)
func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) {
-
defer v.tokenMu.RUnlock()
repoPath := v.buildRepoPath(repo)
-
secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []UnlockedSecret{}, nil
···
-
secretPath := path.Join(repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
···
valueStr, ok := data["value"].(string)
-
continue // skip secrets without values
createdAtStr, ok := data["created_at"].(string)
···
secrets = append(secrets, secret)
-
// buildRepoPath creates an OpenBao path for a repository
func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string {
// convert DidSlashRepo to a safe path by replacing special characters
repoPath := strings.ReplaceAll(string(repo), "/", "_")
···
return fmt.Sprintf("repos/%s", repoPath)
-
// buildSecretPath creates an OpenBao path for a specific secret
func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string {
return path.Join(v.buildRepoPath(repo), key)
···
"github.com/bluesky-social/indigo/atproto/syntax"
···
type OpenBaoManager struct {
···
+
// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy
+
// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200")
+
// The proxy handles all authentication automatically via Auto-Auth
+
func NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
+
if proxyAddress == "" {
+
return nil, fmt.Errorf("proxy address cannot be empty")
config := vault.DefaultConfig()
+
config.Address = proxyAddress
client, err := vault.NewClient(config)
return nil, fmt.Errorf("failed to create openbao client: %w", err)
manager := &OpenBaoManager{
mountPath: "spindle", // default KV v2 mount path
···
+
if err := manager.testConnection(); err != nil {
+
return nil, fmt.Errorf("failed to connect to bao proxy: %w", err)
+
logger.Info("successfully connected to bao proxy", "address", proxyAddress)
+
// testConnection verifies that we can connect to the proxy
+
func (v *OpenBaoManager) testConnection() error {
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
// try token self-lookup as a quick way to verify proxy works
+
// and is authenticated
+
_, err := v.client.Auth().Token().LookupSelfWithContext(ctx)
+
return fmt.Errorf("proxy connection test failed: %w", err)
func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
if err := ValidateKey(secret.Key); err != nil {
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
+
v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath)
+
// Check if secret already exists
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err == nil && existing != nil {
+
v.logger.Debug("secret already exists", "path", secretPath)
return ErrKeyAlreadyPresent
···
"created_by": secret.CreatedBy.String(),
+
v.logger.Debug("writing secret to openbao", "path", secretPath, "mount", v.mountPath)
+
resp, err := v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData)
+
v.logger.Error("failed to write secret", "path", secretPath, "error", err)
return fmt.Errorf("failed to store secret in openbao: %w", err)
+
v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime)
+
v.logger.Debug("verifying secret was written", "path", secretPath)
+
readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
+
v.logger.Error("failed to verify secret after write", "path", secretPath, "error", err)
+
return fmt.Errorf("secret not found after writing to %s/%s: %w", v.mountPath, secretPath, err)
+
if readBack == nil || readBack.Data == nil {
+
v.logger.Error("secret verification returned empty data", "path", secretPath)
+
return fmt.Errorf("secret verification failed: empty data returned for %s/%s", v.mountPath, secretPath)
+
v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version)
func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
+
// check if secret exists
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err != nil || existing == nil {
···
return fmt.Errorf("failed to delete secret from openbao: %w", err)
+
v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key)
func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) {
repoPath := v.buildRepoPath(repo)
+
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []LockedSecret{}, nil
···
+
secretPath := fmt.Sprintf("%s/%s", repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
+
v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err)
if secretData == nil || secretData.Data == nil {
···
secrets = append(secrets, secret)
+
v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets))
func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) {
repoPath := v.buildRepoPath(repo)
+
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []UnlockedSecret{}, nil
···
+
secretPath := fmt.Sprintf("%s/%s", repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
+
v.logger.Warn("failed to read secret", "path", secretPath, "error", err)
···
valueStr, ok := data["value"].(string)
+
v.logger.Warn("secret missing value", "path", secretPath)
createdAtStr, ok := data["created_at"].(string)
···
secrets = append(secrets, secret)
+
v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets))
+
// buildRepoPath creates a safe path for a repository
func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string {
// convert DidSlashRepo to a safe path by replacing special characters
repoPath := strings.ReplaceAll(string(repo), "/", "_")
···
return fmt.Sprintf("repos/%s", repoPath)
+
// buildSecretPath creates a path for a specific secret
func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string {
return path.Join(v.buildRepoPath(repo), key)