-
Notifications
You must be signed in to change notification settings - Fork 169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add AWS Secrets Manager Store #267
Conversation
This is sort of similar to the S3 backend's index file. It's just meant as a speed hack. BTW, the S3 backend was implemented for exactly this purpose -- to avoid rate limits. I think this is a good approach, but it limits their compatibility with non-chamber tools, which has been complained about previously WRT case. Our stance has always been that we don't shoot for non-chamber compatibility, so it's fine with me.
Seems fine to me. I can't imagine a where someone would block
I'm fine leaving it stubbed out for now. Personally I find limited utility in Anyway, this all looks pretty good to me. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, you wrote tests and they seem to pass!
I'm going to just get a second opinion from one of the others here, and then we're good to merge. |
if len(value) == 0 { | ||
return err | ||
} | ||
if err != ErrSecretNotFound { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when err == ErrSecretNotFound
, shouldn't mustCreate
be set to true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no. This is a very tricky bit of code.
ErrSecretNotFound
is the Chamber internal error, but mustCreate
means that "we must create the underlying AWS Secrets Manager secret." Because this implementation doesn't have a 1:1 mapping of Chamber secret to AWS Secrets Manager secret, ErrSecretNotFound
doesn't mean we need to create the underlying Secrets Manager secret. In fact, we will not encounter ErrSecretNotFound
in this code path if the underlying AWS Secrets Manager secret needs to be created. If the AWS Secrets Manager secret does not exist, when we try to get it, we'll get the AWS error secretsmanager.ErrCodeResourceNotFoundException
instead. That is the only case in which the underlying AWS Secrets Manager secret needs to be created.
Does this make sense? I'll add a code comment to explain this here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks for the explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment via 44c774e
SecretString: aws.String(string(contents)), | ||
VersionStages: []*string{aws.String("AWSCURRENT"), aws.String("CHAMBER" + string(version))}, | ||
} | ||
_, err = s.svc.PutSecretValue(putSecretValueInput) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if we should prevent updates to secrets that have RotationEnabled=true
to prevent conflicts with secrets being managed by external Lambdas? Also, with regards to versions and stages, what's the rationale of introducing a CHAMBER+version
stage? I'm wondering if it would make sense to just leave the Chamber label out to prevent complications in the future should Chamber ever want to utilize this for a rotation strategy of its own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, from the docs on PutSecretValue
:
If another version of this secret already exists, then this operation does not automatically move any staging labels other than those that you explicitly specify in the
VersionStages
parameter
I'm wondering if we should just omit VersionStages
and let AWS set these automatically rather than introduce potential conflicts or errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if we should prevent updates to secrets that have RotationEnabled=true to prevent conflicts with secrets being managed by external Lambdas?
@eculver I'm not sure any design that supports Chamber functionality could reasonably interop with the AWS-provided Lambdas for rotating secrets. To use Secrets Manager as a Chamber store, we need to manually manage the secret's metadata together with the secret's name and value. None of the AWS-provided Lambdas for rotating secrets are going to be aware of that metadata management (and honestly, just the structure of the secret value is probably a barrier to using those AWS-provided Lambdas for rotation). Instead, if a user wishes to use the Secrets Manager store and enable automatic rotation, they'll need to use Chamber in their rotation Lambda -- meaning we wouldn't be able to prevent updates to secrets that have RotationEnabled=true
or those rotation Lambdas won't work. 🌀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the rationale of introducing a CHAMBER+version stage
This allows us to keep more than 2 versions of a secret. Without an additional staging label, all but the AWSCURRENT
and AWSPREVIOUS
versions of the secret are considered "deprecated" by Secrets Manager and subject to deletion. In my experience, these get deleted pretty darn quickly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure any design that supports Chamber functionality could reasonably interop with the AWS-provided Lambdas for rotating secrets.
This is exactly my point. Chamber can technically still operate on the secrets that are managed by Lambdas which would cause problems. I'm thinking that a simple check for if RotationEnabled=true
to ensure that the lack of interoperability doesn't cause problems.
This is my only concern at this point. Everything else is looking great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly my point. Chamber can technically still operate on the secrets that are managed by Lambdas which would cause problems. I'm thinking that a simple check for if
RotationEnabled=true
to ensure that the lack of interoperability doesn't cause problems.
@eculver I think I understand, but let me make sure. You're saying that if RotationEnabled=true
we can read the secrets, but we may not write the secrets, correct? (And this would only be applicable if the secret already exists, of course.) This approach makes sense to me now that the data structure is read-interoperable.
One tricky bit is that we don't know value of RotationEnabled
at this point in the code. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand, but let me make sure. You're saying that if
RotationEnabled=true
we can read the secrets, but we may not write the secrets, correct? (And this would only be applicable if the secret already exists, of course.) This approach makes sense to me now that the data structure is read-interoperable.
@nickatsegment Agree that |
@danmactough WFM. Just waiting on @eculver to confirm your rationale above. |
👋 @nickatsegment @eculver Any news on this? |
Thanks for following up. I just want to make a decision on the |
Hey @eculver @nickatsegment FYI, I've been thinking about @eculver's comments, and I have a reworked implementation almost ready that I think will be more interoperable with rotation and other key features of Secrets Manager. |
Ok @eculver @nickatsegment, I think this is ready for another review. 😅 Now, in addition to the change in structure, I've made the following additional changes to increase interoperability with non-Chamber secrets in Secrets Manager:
With these changes, you can create a new Secrets Manager secret using one of the "wizards" in the AWS console (such as "Credentials for RDS database") -- which will create a secret having a "port" property with a value that is a number instead of a string -- and Chamber can still read that secret. This means you can use The interoperability isn't 100% perfect, but it's pretty good! Nothing is "broken", at least by some definition of broken. 🙃 |
👋 @eculver @nickatsegment Please let me know if there's anything I can do to help land this -- my team is excited about adopting chamber with Secrets Manager integration. 🤗 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few more questions.
s = strconv.FormatFloat(value.Float(), 'f', -1, 64) | ||
case reflect.Bool: | ||
s = strconv.FormatBool(value.Bool()) | ||
default: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this also need to support int
types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope! No need to support int
types: https://golang.org/pkg/encoding/json/#Unmarshal
if len(value) == 0 { | ||
return err | ||
} | ||
if err != ErrSecretNotFound { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks for the explanation.
store/secretsmanagerstore.go
Outdated
|
||
if len(value) == 0 { | ||
if mustCreate { | ||
return err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err
will always be nil
here no? I'm not sure I understand what's going on here, could you add a comment explaining the logic here? It seems like if there's a branch for len(value) == 0
it should be checked earlier unless I'm just not following something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err will always be nil here no?
Ah, yes. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed via 744c0fc
// Delete removes a secret. Note this removes all versions of the secret. (True?) | ||
func (s *SecretsManagerStore) Delete(id SecretId) error { | ||
// delegate to Write | ||
return s.Write(id, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the semantics we want for delete? There is a DeleteSecret
action. I'm curious why that's not being used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another one of those name collisions causing confusion. 😞 This delete method doesn't map to deleting the secret from Secrets Manager because the Secrets Manager secret may contain more than one Chamber secret. They intentionally don't map 1:1 (because one of the design goals on this store implementation is to reduce the number of API calls).
So, instead of deleting the Secrets Manager secret, we want to remove the Chamber secret from the Secrets Manager secret. Make sense?
assert.Equal(t, "true", obj["isPhony"]) | ||
assert.Equal(t, "", obj["empty"]) | ||
assert.Equal(t, "", obj["nested"]) | ||
assert.Equal(t, "", obj["array"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this expected? Nested values are ignored?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional because ultimately we need to distill everything to name/value pairs of environment variables but we don't have a rule for translating nested values. I could definitely see adding that as an enhancement (for example, this nested
object could be expanded to NESTED__FOO=bar
or something).
583cf39
to
6d8f119
Compare
I want to highlight this commit I just pushed cbd2cc2 It changes the behavior a bit, but I think for the better. Currently, all keys get converted to lowercase on write and then converted to UPPERCASE when using the When using Secrets Manager as a backend, however, it's very surprising (and annoying) to have the keys converted to lowercase because when using the AWS SDK to fetch the secret where Chamber is not available (such as in a NodeJS Lambda function), it leads to this split brain for the developer -- when I use this secret one way (with Then I realized that if I wanted to be able to refer to secrets on It seems wrong for my Lambda to have to know about implementation details of Chamber. I think the change in cbd2cc2 -- in which the Happy to discuss. |
This issue has been automatically marked |
Sorry @danmactough , so many different irons in different fires around here I've lost track :D WRT to case, I think regardless of the reasoning for why some parts are uppercase, it would be weird if only one provider converted to uppercase, wouldn't you say? If anything, we should do the case transformation at a higher level?
I wasn't here when it was invented, but I think the rationale is simply that environment variables tend to be UPPER_CASE. |
No worries @nickatsegment. At least one of those fires has been all over the news, right? 🤑 🍾 🎊 On the case issue, I'm happy to revert cbd2cc2. To restate my thinking a bit, I think that converting all the secret keys to lowercase when using Secrets Manager makes the interoperability that we've been striving for less useful. Imagine that you use Chamber to manage a secret in Secrets Manager and when you use But honestly, normalizing the names in ANY way is kind of surprising. Like, why can't I have PascalCase or camelCase environment variable names if that's what I want? So, like you say maybe consistency is better at this point, even if it's not optimal, and perhaps an improvement in v3 😄 would be to find a way to preserve the original input for environment variable names. |
Using a type that matches the API rather than specifically an STS client allow us to pass in an STS mock
cbd2cc2
to
8d52e89
Compare
rebased and dropped cbd2cc2 |
@@ -0,0 +1,562 @@ | |||
package store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last thing, I promise :)
Would you be willing to be the maintainer of this backend for the time-being? Nothing formal or enforceable obviously. All PRs would still go through Segmenters for approval (we haven't quite figured out how to deal with giving external folks merge access). We'll just defer to you on issues and PRs.
If that sounds good, just add a comment to the top of this file:
package store | |
// Secrets Manager Store is maintained by Dan McTough https://github.com/danmactough. Thanks Dan! | |
package store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem. I'll just correct the spelling of my name. 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added via 53be3ab @nickatsegment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem. I'll just correct the spelling of my name. 🙃
Whoops! My bad, sorry.
OK, looks good to me. Just that question on notating maintainership :) Then we'll merge |
Re interoperability, basically chamber is not really designed to be interoperable: it's designed to work with secrets it has written, and makes no attempt to make its write format flexible or interoperable. Making it interoperable is perhaps a nice idea, but obviously would be a big change and require a bunch more knobs and dials that are all opportunities for error. In my mind, it's fundamentally a different tool at that point.
I believe the intention is to reduce the possibility of error and confusion. It's a tradeoff of flexibility in the name of simplicity and avoiding footguns. |
#267 added secret manager support, so I updated the readme to note that. That way people won't need to dig through the issues to figure out what the support status is.
Implements #84 (currently closed as stale)
It sounds from #84 like there's still some interest in adding support for AWS Secrets Manager as a store, but previous efforts stalled. I started working on this implementation as a proof-of-concept for work -- at a previous job, we used Chamber and liked it a lot, but at my current job, we use Secrets Manager for secrets.
I'm not entirely sure about the design of this implementation, so I'm very open to feedback. Specifically:
chamber exec
andchamber env
can be fulfilled with a single API call. This should eliminate the biggest pain point I've experienced with using Chamber -- getting rate-limited during a deployment. However, there's a greater risk of hitting the 4 KiB size limit per Secrets Manager secret.ListServices
. I wasn't sure what to do about the fact that we have lots of Secrets Manager secrets already, none of which are "services" that I would want to list here. Should we suggest that the service start with a prefix, such asservice/
, and limit theListServices
results to secrets starting with that prefix?I feel like I should also note that I don't write Go day-to-day, so please don't hesitate to point out things that are not conventional, poor implementation, etc. 😊