1package approle 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "sync/atomic" 8 "time" 9 10 "github.com/hashicorp/errwrap" 11 "github.com/hashicorp/vault/sdk/framework" 12 "github.com/hashicorp/vault/sdk/helper/consts" 13 "github.com/hashicorp/vault/sdk/logical" 14) 15 16func pathTidySecretID(b *backend) *framework.Path { 17 return &framework.Path{ 18 Pattern: "tidy/secret-id$", 19 20 Callbacks: map[logical.Operation]framework.OperationFunc{ 21 logical.UpdateOperation: b.pathTidySecretIDUpdate, 22 }, 23 24 HelpSynopsis: pathTidySecretIDSyn, 25 HelpDescription: pathTidySecretIDDesc, 26 } 27} 28 29// tidySecretID is used to delete entries in the whitelist that are expired. 30func (b *backend) tidySecretID(ctx context.Context, req *logical.Request) (*logical.Response, error) { 31 // If we are a performance standby forward the request to the active node 32 if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { 33 return nil, logical.ErrReadOnly 34 } 35 36 if !atomic.CompareAndSwapUint32(b.tidySecretIDCASGuard, 0, 1) { 37 resp := &logical.Response{} 38 resp.AddWarning("Tidy operation already in progress.") 39 return resp, nil 40 } 41 42 s := req.Storage 43 44 go func() { 45 defer atomic.StoreUint32(b.tidySecretIDCASGuard, 0) 46 47 logger := b.Logger().Named("tidy") 48 49 checkCount := 0 50 51 defer func() { 52 if b.testTidyDelay > 0 { 53 logger.Trace("done checking entries", "num_entries", checkCount) 54 } 55 }() 56 57 // Don't cancel when the original client request goes away 58 ctx = context.Background() 59 60 tidyFunc := func(secretIDPrefixToUse, accessorIDPrefixToUse string) error { 61 logger.Trace("listing role HMACs", "prefix", secretIDPrefixToUse) 62 63 roleNameHMACs, err := s.List(ctx, secretIDPrefixToUse) 64 if err != nil { 65 return err 66 } 67 68 logger.Trace("listing accessors", "prefix", accessorIDPrefixToUse) 69 70 // List all the accessors and add them all to a map 71 accessorHashes, err := s.List(ctx, accessorIDPrefixToUse) 72 if err != nil { 73 return err 74 } 75 accessorMap := make(map[string]bool, len(accessorHashes)) 76 for _, accessorHash := range accessorHashes { 77 accessorMap[accessorHash] = true 78 } 79 80 time.Sleep(b.testTidyDelay) 81 82 secretIDCleanupFunc := func(secretIDHMAC, roleNameHMAC, secretIDPrefixToUse string) error { 83 checkCount++ 84 lock := b.secretIDLock(secretIDHMAC) 85 lock.Lock() 86 defer lock.Unlock() 87 88 entryIndex := fmt.Sprintf("%s%s%s", secretIDPrefixToUse, roleNameHMAC, secretIDHMAC) 89 secretIDEntry, err := s.Get(ctx, entryIndex) 90 if err != nil { 91 return errwrap.Wrapf(fmt.Sprintf("error fetching SecretID %q: {{err}}", secretIDHMAC), err) 92 } 93 94 if secretIDEntry == nil { 95 logger.Error("entry for secret id was nil", "secret_id_hmac", secretIDHMAC) 96 return nil 97 } 98 99 if secretIDEntry.Value == nil || len(secretIDEntry.Value) == 0 { 100 return fmt.Errorf("found entry for SecretID %q but actual SecretID is empty", secretIDHMAC) 101 } 102 103 var result secretIDStorageEntry 104 if err := secretIDEntry.DecodeJSON(&result); err != nil { 105 return err 106 } 107 108 // If a secret ID entry does not have a corresponding accessor 109 // entry, revoke the secret ID immediately 110 accessorEntry, err := b.secretIDAccessorEntry(ctx, s, result.SecretIDAccessor, secretIDPrefixToUse) 111 if err != nil { 112 return errwrap.Wrapf("failed to read secret ID accessor entry: {{err}}", err) 113 } 114 if accessorEntry == nil { 115 logger.Trace("found nil accessor") 116 if err := s.Delete(ctx, entryIndex); err != nil { 117 return errwrap.Wrapf(fmt.Sprintf("error deleting secret ID %q from storage: {{err}}", secretIDHMAC), err) 118 } 119 return nil 120 } 121 122 // ExpirationTime not being set indicates non-expiring SecretIDs 123 if !result.ExpirationTime.IsZero() && time.Now().After(result.ExpirationTime) { 124 logger.Trace("found expired secret ID") 125 // Clean up the accessor of the secret ID first 126 err = b.deleteSecretIDAccessorEntry(ctx, s, result.SecretIDAccessor, secretIDPrefixToUse) 127 if err != nil { 128 return errwrap.Wrapf("failed to delete secret ID accessor entry: {{err}}", err) 129 } 130 131 if err := s.Delete(ctx, entryIndex); err != nil { 132 return errwrap.Wrapf(fmt.Sprintf("error deleting SecretID %q from storage: {{err}}", secretIDHMAC), err) 133 } 134 135 return nil 136 } 137 138 // At this point, the secret ID is not expired and is valid. Delete 139 // the corresponding accessor from the accessorMap. This will leave 140 // only the dangling accessors in the map which can then be cleaned 141 // up later. 142 salt, err := b.Salt(ctx) 143 if err != nil { 144 return err 145 } 146 delete(accessorMap, salt.SaltID(result.SecretIDAccessor)) 147 148 return nil 149 } 150 151 for _, roleNameHMAC := range roleNameHMACs { 152 logger.Trace("listing secret ID HMACs", "role_hmac", roleNameHMAC) 153 secretIDHMACs, err := s.List(ctx, fmt.Sprintf("%s%s", secretIDPrefixToUse, roleNameHMAC)) 154 if err != nil { 155 return err 156 } 157 for _, secretIDHMAC := range secretIDHMACs { 158 err = secretIDCleanupFunc(secretIDHMAC, roleNameHMAC, secretIDPrefixToUse) 159 if err != nil { 160 return err 161 } 162 } 163 } 164 165 // Accessor indexes were not getting cleaned up until 0.9.3. This is a fix 166 // to clean up the dangling accessor entries. 167 if len(accessorMap) > 0 { 168 for _, lock := range b.secretIDLocks { 169 lock.Lock() 170 defer lock.Unlock() 171 } 172 for accessorHash, _ := range accessorMap { 173 logger.Trace("found dangling accessor, verifying") 174 // Ideally, locking on accessors should be performed here too 175 // but for that, accessors are required in plaintext, which are 176 // not available. The code above helps but it may still be 177 // racy. 178 // ... 179 // Look up the secret again now that we have all the locks. The 180 // lock is held when writing accessor/secret so if we have the 181 // lock we know we're not in a 182 // wrote-accessor-but-not-yet-secret case, which can be racy. 183 var entry secretIDAccessorStorageEntry 184 entryIndex := accessorIDPrefixToUse + accessorHash 185 se, err := s.Get(ctx, entryIndex) 186 if err != nil { 187 return err 188 } 189 if se != nil { 190 err = se.DecodeJSON(&entry) 191 if err != nil { 192 return err 193 } 194 195 // The storage entry doesn't store the role ID, so we have 196 // to go about this the long way; fortunately we shouldn't 197 // actually hit this very often 198 var found bool 199 searchloop: 200 for _, roleNameHMAC := range roleNameHMACs { 201 secretIDHMACs, err := s.List(ctx, fmt.Sprintf("%s%s", secretIDPrefixToUse, roleNameHMAC)) 202 if err != nil { 203 return err 204 } 205 for _, v := range secretIDHMACs { 206 if v == entry.SecretIDHMAC { 207 found = true 208 logger.Trace("accessor verified, not removing") 209 break searchloop 210 } 211 } 212 } 213 if !found { 214 logger.Trace("could not verify dangling accessor, removing") 215 err = s.Delete(ctx, entryIndex) 216 if err != nil { 217 return err 218 } 219 } 220 } 221 } 222 } 223 224 return nil 225 } 226 227 err := tidyFunc(secretIDPrefix, secretIDAccessorPrefix) 228 if err != nil { 229 logger.Error("error tidying global secret IDs", "error", err) 230 return 231 } 232 err = tidyFunc(secretIDLocalPrefix, secretIDAccessorLocalPrefix) 233 if err != nil { 234 logger.Error("error tidying local secret IDs", "error", err) 235 return 236 } 237 }() 238 239 resp := &logical.Response{} 240 resp.AddWarning("Tidy operation successfully started. Any information from the operation will be printed to Vault's server logs.") 241 return logical.RespondWithStatusCode(resp, req, http.StatusAccepted) 242} 243 244// pathTidySecretIDUpdate is used to delete the expired SecretID entries 245func (b *backend) pathTidySecretIDUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 246 return b.tidySecretID(ctx, req) 247} 248 249const pathTidySecretIDSyn = "Trigger the clean-up of expired SecretID entries." 250const pathTidySecretIDDesc = `SecretIDs will have expiration time attached to them. The periodic function 251of the backend will look for expired entries and delete them. This happens once in a minute. Invoking 252this endpoint will trigger the clean-up action, without waiting for the backend's periodic function.` 253