-
-
Notifications
You must be signed in to change notification settings - Fork 89
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
Add support for storing OAuth2.0 tokens in AWS Secrets Manager #114
Conversation
WIP, todo: - Test stability on my own server - Create secret if it does not exist
Previously the code `appconfig_without_aws_secrets = AppConfig._PARSER` created a shallow copy, which meant that the subsequent code `appconfig_without_aws_secrets.remove_option(account, key)` removed the OAuth 2.0 tokens from the config in memory. Now we create a deep copy of the config, remove the tokens from the deep copy, and write the deep copy to disk. So they remain stored in memory.
AWS Secrets Manager functionality will be built into the proxy once simonrob/email-oauth2-proxy#114 is merged.
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.
Thanks for this contribution! I've had a quick look and added some initial thoughts/suggestions.
@simonrob thanks for the careful review and thoughtful feedback! I plan to iteratively revise & respond to each point over the holidays, and will tag you in a comment when I've finished the first round of revisions. |
@simonrob I did a pass through all your comments. Thanks again for the feedback!
I will await your feedback on the clarifying questions before tackling the items that require more extensive testing with the AWS SDK. |
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.
Here I've suggested a minor reorganisation of the requirements file to include the core no-GUI requirements so that this file can be relied upon to install all dependencies.
Co-authored-by: Simon Robinson <[email protected]>
- The app will no longer crash if it hits an AWS error - The CLI argument is no longer necessary, it will attempt to use AWS if aws_secret is set in config - All the AWS-specific code is contained in four new static methods, which should make it easier to maintain and ensure there are no side-effects when not using AWS Secrets.
- Without exiting proxy - Without parsing error message - With helpful messages in proxy log
@simonrob I've substantially refactored the code in commits 5b7d63b and 2c9c6be in order to:
I'm much happier with this new design: I believe the code is more robust and more maintainable. It's ready for a new round of code review when you have a chance. |
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.
Thanks for the update!
I think this is very nearly ready to go – I went through line by line this time and made lots of very minor edits just for consistency with the rest of the proxy's code style and linting settings. There is one question about the debug log method, and I also made a separate commit to add a global list of tokens that should be stored in a secrets manager.
Could you check that this updated version works and I haven't inadvertently introduced any new issues?
Co-authored-by: Simon Robinson <[email protected]>
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.
@simonrob thank you for putting in another round of detailed review and feedback! I am accepting all of your suggestions and will make a couple of my own.
Co-authored-by: Simon Robinson <[email protected]>
emailproxy.py
Outdated
aws_secrets[account_secret][account] = {key: appconfig_to_save.get(account, key) for key in | ||
AppConfig._SECRETS_MANAGER_KEYS} |
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.
One thing we haven't covered so far is what would/should happen if the required keys are not present (either in the secret, or in the proxy's cache).
I don't know if/how this could happen from the AWS side, but the proxy can (and does) delete values when they become invalid, which would cause this method to fail.
One simple solution here would be to use a fallback value in get()
and skip keys if they have no value (and entire accounts if there are no valid keys). What do you think?
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.
Another related question: my assumption is that token renewal is handled via a local copy of the proxy - i.e., you receive an authentication failure when using your remote server, which prompts you to open a local copy and authenticate, updating the AWS secret. Is that correct? If so, how do you trigger a token update from AWS Secrets Manager on the remote server?
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
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 addressed the first question about required keys in a review below.
Another related question: my assumption is that token renewal is handled via a local copy of the proxy - i.e., you receive an authentication failure when using your remote server, which prompts you to open a local copy and authenticate, updating the AWS secret. Is that correct? If so, how do you trigger a token update from AWS Secrets Manager on the remote server?
I think there are 3 options:
- handle token renewal locally and use SIGHUP to reload config on the remote server.
- handle token renewal locally and restart the remote server's proxy to reload config
- handle the token renewal by ssh-ing into the remote server.
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 was wondering whether it is worth triggering AppConfig._aws_secrets_fetch()
(probably via a more generic method in the AppConfig class) after a login failure for an account using AWS Secrets. This way, a login failure on the server would trigger checking for updated credentials that have been provided via a local login, and there would be no need to log in to the server at all.
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.
Thanks @simonrob! Happy to test that out, although looking at my work week I'm not sure how soon I'll get to it this week.
Since we've completed the core functionality, do you think it would be worthwhile to merge this PR and split the reload functionality into a new PR? Or would you prefer to keep it in a single PR?
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.
Looking at this made me reconsider the
allow_catch_all_accounts
advanced configuration option. For consistency, having anaws_secret
value in any parent account (e.g., the global [@], or, say [@gmail.com]) should use that secret to store values for all child accounts within that group. I'll think about how is best to address this.
The simplest brute solution would be to only support aws_secret
functionality for fully-specified accounts, and return a warning & ignore the aws_secret
if it's specified for a catch-all account. Up to you how valuable it would be to get these two features to work together, but I don't foresee using that functionality personally.
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.
On second thought, I think the functionality already implemented so far will work fine for catch-all accounts (unless I'm mistaken). It's the reload step that is trickier.
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 the catch-all functionality is quite valuable, but it should be relatively straightforward to incorporate that into the existing PR. I'll take a look when I get chance.
How did you get on with the reloading suggestion?
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've had a busy week and haven't had a chance to test the reloading approach you suggested, but I look forward to testing it soon—perhaps this weekend.
Re: catch-all with AWS Secrets, I agree it will be best to integrate! I haven't used catch-all accounts before because I only use the proxy for a single account, but I suppose I can define a catch-all that applies to that single account in order to test.
@simonrob I've finished manually testing the code in commit 15f049c, stepping through each of the scenarios covered by our error handling logic. I've pasted the associated logged errors in the description at the top of this PR. All is working correctly in my tests. I will now address the new comment you opened above re: what would/should happen if the required keys are not present. Once that's resolved, I think this PR is ready to merge. |
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.
One thing we haven't covered so far is what would/should happen if the required keys are not present (either in the secret, or in the proxy's cache).
I don't know if/how this could happen from the AWS side, but the proxy can (and does) delete values when they become invalid, which would cause this method to fail.
One simple solution here would be to use a fallback value in get() and skip keys if they have no value (and entire accounts if there are no valid keys). What do you think?
I manually tested what would happen if I edited the secret value in AWS Secrets Manager to remove a key (e.g. remove token_salt
). As you predicted, this causes a crash because we don't have a fallback value.
I agree that a missing key should not cause a crash. The proxy should have the same behaviour as it would if the local config had a missing key.
I've proposed two changes that would set a default value of an empty string ''
when fetching/storing the token keys.
I also posed a question below about whether we need to add additional error handling
logic around remove_option
.
emailproxy.py
Outdated
aws_secrets[account_secret][account] = {key: appconfig_to_save.get(account, key) for key in | ||
AppConfig._SECRETS_MANAGER_KEYS} |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
Co-authored-by: Simon Robinson <[email protected]>
@simonrob based on @thiagomacieira's question in #33, I wonder if you'd like to adjust the way that we're specifying this feature in the config, in order to provide future flexibility for alternative token storage strategies. Right now, we're enabling AWS Secrets Manager token storage by adding the following to config:
We could potentially modify the config syntax to "future-proof" it, ex:
In the future, if for example someone implemented token storage in an alternative local file, it could use the same config key, ex:
|
That's an interesting idea, but I wonder whether it is a little overly complex. Looking at the current implementation, is there actually any need to have a separate AWS Secrets Manager ID for each account entry, or could this be a global setting? If not, both the AWS feature and the ability to store tokens in a local cache file could as you point out be implemented in a very similar way. I'd probably make this a little more generic, and have the What do you think – could this all be handled via a global configuration parameter? (Or perhaps a command-line option – e.g., |
Although I don't personally store the credentials for different accounts in different locations, that's because I have only one account and am using the proxy in a single-user setting. I implemented it as an account-specific parameter because it seemed plausible to me that one might like to have separate access controls for the different accounts. Suppose I'm running a remote server for two different users, each of whom I'd like to empower to update their own credentials from their local computers. If it's a global config parameter, then each user must have read/write access to each other's tokens. If it's an account-specific config parameter, then each user could have individual access controls to their own secrets. Do you think the JSON value for |
I think it becomes overly complex once additional external token stores come into the picture. I'm also not sure how often the sort of situation you've outlined would actually happen - remember each user would also have to have their own copy of the proxy to re-authenticate periodically and then synchronise with the server. Is that likely? I think it's probably more common that someone running that setup would either be an individual (like yourself) or an organisation that uses one of the O365 methods that removes the need for local OAuth. If this is made a global setting then everything becomes much simpler (and easier to maintain) - if you want to add a new token store you are simply provided with a ConfigParser object, and you write all of its contents to whatever storage you like. There are no issues with any other proxy features either (except possibly client secret encryption, but it's unlikely you'd be using that in this case). |
Not sure! In this case we're entirely in the realm of speculation regarding future use cases that neither of us will personally face.
Ultimately you're the maintainer of the package and I will gladly defer to you on the tradeoffs regarding flexibility vs complexity/maintainability. Do you want to refactor the current PR to make the token storage a global setting? |
This change has a potentially negative impact on the use of AWS Secrets Manager, in that it is no-longer possible to specify a different ARN or secret ID per account in the proxy's configuration file; instead, a command-line parameter (`--cache-store`) is introduced to specify a cache location (either an existing ARN or a new secret name (that must start with `aws_emailproxy@`)). However, there are several benefits to the new approach, including its generally simpler structure, and the fact that the cache location can now be redirected more easily to, for example, a local temporary file, or (with further development) any other secret management platform that can accept a JSON object.
I spent some time just now thinking about how to balance the complexity, multi-account inheritance, desire for other external token caches and ongoing maintainability. 2e6b641 is what I ended up with – this adds a new What do you think? |
@michaelstepner I'm pretty happy with this new version, but it'd be useful to get your feedback if you have time. |
@michaelstepner any thoughts about this? |
I plan to merge this next week, so if you have any feedback or issues please do let me know before then. |
Thanks @michaelstepner for all the work on this – I think it's time to merge. |
* Create AWS Secret and user to read/write secret * Automatically configure AWS credentials and region * Avoid terraform error: AWS secret pending deletion * Store aws_secret ARN in proxy config * Transfer OAuth2 tokens to AWS Secrets Manager * Check off support for secrets manager in README * Add price of AWS Secret to README * Allow configuring git repo; run with --aws-secrets * Add auto-filled 'aws_secret' to config_example * Remove python script to ul/dl AWS Secrets AWS Secrets Manager functionality will be built into the proxy once simonrob/email-oauth2-proxy#114 is merged. * Revise TF config and README given upstream updates - requirements-aws-secrets.txt now includes requirements-no-gui.txt - argument --aws-secrets is no longer needed * Automatically launch the proxy using systemctl Refs: - simonrob/email-oauth2-proxy#2 (comment) - https://www.shubhamdipt.com/blog/how-to-create-a-systemd-service-in-linux/ * Explain how to update placeholder config values * Change license to 0BSD * Change cost estimate and credit to Simon * Upgrade terraform to latest version * Update email proxy config to reflect new version * Upgrade to Amazon Linux 2023 * Improve README command formatting * Update thanks to Simon Robinson
@simonrob I'm so grateful that you implemented the improved version of this idea with the I just updated my Terraform AWS configuration email-oauth2-proxy-aws to use the Thanks again! |
It wouldn't have happened at all without your idea and contribution, so no worries at all. Really glad to hear it works well with your tool; thanks again for the work on this. |
Closes #33, which features a substantive discussion about the design of this feature.
Objective
Provides an option to store tokens remotely in AWS Secrets Manager. By storing the authorization tokens remotely, this facilitates the administration of the proxy in a server-based configuration:
Implementation
This implementation endeavours to minimize the complexity added to the proxy:
aws_secret
:The only dependency is
boto3
, and it is only loaded if one or more accounts have theaws_secret
option specified in their config. This new dependency is documented in requirements-aws-secrets.txtIt adds new methods to the
AppConfig
class to handle the option of loading/saving OAuth 2.0 tokens from the remote AWS Secrets Manager.AppConfig._load()
andAppConfig.save()
if an account has theaws_secret
option specified, which minimizes the risk that this new feature affects existing functionality.aws_secrets
.If the proxy fails to access the AWS Secrets Manager, it logs an error but continues functioning normally: without fetching the remote secret, or without storing the secret remotely.
Manual Testing
I've tested this new feature quite carefully using the Terraform config in PR #1 of michaelstepner/email-oauth2-proxy-aws.
I've also verified that the proxy will seamlessly create the specified AWS Secret if it does not yet exist, using the Terraform config in PR #2 of michaelstepner/email-oauth2-proxy-aws.
This lists the various cases that are handled, along with the associated debug log error messages (
--debug
enabled) tested manually on commit 15f049c:boto3
is not installedsecretsmanager:GetSecretValue
permissionssecretsmanager:CreateSecret
permissionssecretsmanager:PutSecretValue
permissionssecretsmanager:PutSecretValue
permissions