Skip to content

Commit 687e98e

Browse files
committed
Feat: Update Bootstrap and Recovery Logic for crash / restart proofing
1 parent c186c58 commit 687e98e

1 file changed

Lines changed: 135 additions & 38 deletions

File tree

vault-manager/cmd/main.go

Lines changed: 135 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
// and unseals Vault if needed, sets up the PKI CA chain, provisions
33
// per-service AppRoles and policies, then serves a gRPC API so other
44
// services can fetch or rotate their credentials at runtime.
5+
//
6+
// On first run the unseal key(s) and auth credentials are written to
7+
// --bootstrap-file so subsequent restarts (of vault-manager or of Vault
8+
// itself) can recover without operator intervention.
59
package main
610

711
import (
12+
"errors"
813
"fmt"
914
"os"
1015
"os/signal"
@@ -15,6 +20,7 @@ import (
1520
"github.com/sirupsen/logrus"
1621

1722
"github.com/persys-dev/persys-cloud/vault-manager/internal/approle"
23+
"github.com/persys-dev/persys-cloud/vault-manager/internal/bootstrap"
1824
"github.com/persys-dev/persys-cloud/vault-manager/internal/config"
1925
"github.com/persys-dev/persys-cloud/vault-manager/internal/pki"
2026
"github.com/persys-dev/persys-cloud/vault-manager/internal/policy"
@@ -28,34 +34,23 @@ func main() {
2834
config.Log.Fatal("no valid services found in --services")
2935
}
3036

31-
baseClient, err := vaultclient.New(cfg.VaultAddr, "")
32-
if err != nil {
33-
config.Log.Fatal(err)
34-
}
35-
vaultclient.WaitUntilReady(baseClient)
37+
// Sealed Vault is fine — we unseal from bootstrap state next.
38+
vaultclient.WaitUntilReady(cfg.VaultAddr)
3639

37-
rootToken, err := bootstrapOrUnseal(cfg)
40+
workClient, state, err := recoverOrBootstrap(cfg)
3841
if err != nil {
3942
config.Log.Fatal(err)
4043
}
4144

42-
rootClient, err := vaultclient.New(cfg.VaultAddr, rootToken)
43-
if err != nil {
45+
if err := provision(workClient, cfg); err != nil {
4446
config.Log.Fatal(err)
4547
}
4648

47-
workClient := rootClient
48-
if cfg.Secure {
49-
config.Log.Println("--secure enabled: creating bootstrap AppRole and switching off root token")
50-
workClient, err = vaultclient.SwitchToSecure(rootClient, cfg)
51-
if err != nil {
52-
config.Log.Fatal(err)
53-
}
54-
}
55-
56-
if err := provision(workClient, cfg); err != nil {
57-
config.Log.Fatal(err)
49+
// Persist latest state after successful provision.
50+
if err := bootstrap.Save(cfg.BootstrapFile, state); err != nil {
51+
config.Log.Fatalf("save bootstrap state to %s: %v", cfg.BootstrapFile, err)
5852
}
53+
config.Log.Printf("Bootstrap state saved to %s", cfg.BootstrapFile)
5954

6055
secrets, err := approle.GatherSecrets(workClient, cfg)
6156
if err != nil {
@@ -77,39 +72,141 @@ func main() {
7772
waitForShutdown()
7873
}
7974

80-
// bootstrapOrUnseal initializes and unseals Vault if it hasn't been set up
81-
// yet, then returns the root token to use for provisioning: the freshly
82-
// generated one, or VAULT_ROOT_TOKEN if Vault was already initialized.
83-
func bootstrapOrUnseal(cfg *config.Config) (string, error) {
75+
// recoverOrBootstrap is the restart-safe entry point.
76+
//
77+
// 1. Load bootstrap file if present.
78+
// 2. Uninitialized Vault → init, unseal, persist keys + root token.
79+
// 3. Initialized + sealed → unseal with stored keys.
80+
// 4. Authenticate: manager AppRole (preferred) → stored root token → VAULT_ROOT_TOKEN.
81+
// 5. --secure without manager creds → hand off, revoke root, persist manager creds.
82+
func recoverOrBootstrap(cfg *config.Config) (*vault.Client, *bootstrap.State, error) {
83+
state, err := bootstrap.Load(cfg.BootstrapFile)
84+
if err != nil && !errors.Is(err, bootstrap.ErrNotFound) {
85+
return nil, nil, fmt.Errorf("load bootstrap state from %s: %w", cfg.BootstrapFile, err)
86+
}
87+
if state == nil {
88+
state = &bootstrap.State{}
89+
config.Log.Printf("No bootstrap state at %s (first run or missing volume)", cfg.BootstrapFile)
90+
} else {
91+
config.Log.Printf("Loaded bootstrap state from %s (unseal_keys=%d manager=%v root=%v)",
92+
cfg.BootstrapFile, len(state.UnsealKeys), state.HasManagerCreds(), state.RootToken != "")
93+
}
94+
8495
initialized, err := vaultclient.IsInitialized(cfg.VaultAddr)
8596
if err != nil {
86-
return "", err
97+
return nil, nil, err
8798
}
8899

89100
if !initialized {
90-
config.Log.Println("Vault not initialized. Initializing...")
91-
initResult, err := vaultclient.Initialize(cfg.VaultAddr)
101+
return firstTimeInit(cfg, state)
102+
}
103+
104+
if err := vaultclient.EnsureUnsealed(cfg.VaultAddr, state.UnsealKeys); err != nil {
105+
return nil, nil, err
106+
}
107+
108+
client, err := authenticate(cfg, state)
109+
if err != nil {
110+
return nil, nil, err
111+
}
112+
113+
if cfg.Secure && !state.HasManagerCreds() {
114+
config.Log.Println("--secure enabled: creating bootstrap AppRole and switching off root token")
115+
handoff, err := vaultclient.SwitchToSecure(client, cfg)
92116
if err != nil {
93-
return "", err
117+
return nil, nil, err
94118
}
95-
fmt.Println("Vault initialized credentials (store securely):")
96-
fmt.Printf("unseal_key: %s\n", initResult.UnsealKey)
97-
fmt.Printf("root_token: %s\n", initResult.RootToken)
98-
if err := vaultclient.Unseal(cfg.VaultAddr, initResult.UnsealKey); err != nil {
99-
return "", err
119+
state.ManagerRoleID = handoff.RoleID
120+
state.ManagerSecretID = handoff.SecretID
121+
state.RootToken = ""
122+
client = handoff.Client
123+
}
124+
125+
return client, state, nil
126+
}
127+
128+
func firstTimeInit(cfg *config.Config, state *bootstrap.State) (*vault.Client, *bootstrap.State, error) {
129+
config.Log.Println("Vault not initialized. Initializing...")
130+
initResult, err := vaultclient.Initialize(cfg.VaultAddr)
131+
if err != nil {
132+
return nil, nil, err
133+
}
134+
135+
fmt.Println("Vault initialized credentials (also saved to bootstrap file):")
136+
fmt.Printf("unseal_keys: %v\n", initResult.UnsealKeys)
137+
fmt.Printf("root_token: %s\n", initResult.RootToken)
138+
139+
if err := vaultclient.UnsealAll(cfg.VaultAddr, initResult.UnsealKeys); err != nil {
140+
return nil, nil, err
141+
}
142+
config.Log.Println("Vault initialized and unsealed.")
143+
144+
state.UnsealKeys = initResult.UnsealKeys
145+
state.RootToken = initResult.RootToken
146+
147+
// Persist immediately so a crash between init and provision is recoverable.
148+
if err := bootstrap.Save(cfg.BootstrapFile, state); err != nil {
149+
return nil, nil, fmt.Errorf("save bootstrap state after init to %s: %w", cfg.BootstrapFile, err)
150+
}
151+
config.Log.Printf("Bootstrap state saved to %s", cfg.BootstrapFile)
152+
153+
client, err := vaultclient.New(cfg.VaultAddr, initResult.RootToken)
154+
if err != nil {
155+
return nil, nil, err
156+
}
157+
158+
if cfg.Secure {
159+
config.Log.Println("--secure enabled: creating bootstrap AppRole and switching off root token")
160+
handoff, err := vaultclient.SwitchToSecure(client, cfg)
161+
if err != nil {
162+
return nil, nil, err
163+
}
164+
state.ManagerRoleID = handoff.RoleID
165+
state.ManagerSecretID = handoff.SecretID
166+
state.RootToken = ""
167+
client = handoff.Client
168+
169+
if err := bootstrap.Save(cfg.BootstrapFile, state); err != nil {
170+
return nil, nil, fmt.Errorf("save bootstrap state after secure handoff: %w", err)
100171
}
101-
config.Log.Println("Vault initialized and unsealed.")
102-
return initResult.RootToken, nil
103172
}
104173

105-
rootToken := strings.TrimSpace(os.Getenv("VAULT_ROOT_TOKEN"))
174+
return client, state, nil
175+
}
176+
177+
// authenticate picks the best available credential source.
178+
//
179+
// Priority:
180+
// 1. Manager AppRole from bootstrap file (restart after --secure)
181+
// 2. Root token from bootstrap file
182+
// 3. VAULT_ROOT_TOKEN environment variable
183+
func authenticate(cfg *config.Config, state *bootstrap.State) (*vault.Client, error) {
184+
if state.HasManagerCreds() {
185+
config.Log.Println("Authenticating with stored manager AppRole credentials")
186+
client, err := vaultclient.LoginAppRole(cfg.VaultAddr, state.ManagerRoleID, state.ManagerSecretID)
187+
if err != nil {
188+
return nil, fmt.Errorf("manager AppRole login: %w", err)
189+
}
190+
return client, nil
191+
}
192+
193+
rootToken := strings.TrimSpace(state.RootToken)
106194
if rootToken == "" {
107-
return "", fmt.Errorf("VAULT_ROOT_TOKEN required when Vault is already initialized")
195+
rootToken = strings.TrimSpace(os.Getenv("VAULT_ROOT_TOKEN"))
108196
}
109-
return rootToken, nil
197+
if rootToken == "" {
198+
return nil, fmt.Errorf(
199+
"vault is initialized but no credentials available: "+
200+
"ensure %s contains unseal_keys and root_token/manager creds, "+
201+
"or set VAULT_ROOT_TOKEN (file missing usually means the volume was not persisted)",
202+
cfg.BootstrapFile,
203+
)
204+
}
205+
206+
config.Log.Println("Authenticating with root token")
207+
return vaultclient.New(cfg.VaultAddr, rootToken)
110208
}
111209

112-
// provision ensures the PKI chain, service policies, and AppRoles all exist.
113210
func provision(client *vault.Client, cfg *config.Config) error {
114211
if err := pki.Ensure(client, cfg); err != nil {
115212
return err

0 commit comments

Comments
 (0)