-
Notifications
You must be signed in to change notification settings - Fork 6
Use docker secrets for EDL credentials #134
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| secrets/ | ||
|
|
||
| # Byte-compiled / optimized / DLL files | ||
| __pycache__/ | ||
| *.py[cod] | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """Initialize Docker secrets with Earthdata credentials for local development. | ||
|
|
||
| Run this once prior to running "docker compose up --build". There is no need | ||
| to run it again unless your Earthdata credentials change. | ||
|
|
||
| Creates the following secret files within the ./secrets directory (git-ignored) | ||
| for use with Docker: | ||
|
|
||
| - earthdata-username.txt | ||
| - earthdata-password.txt | ||
|
|
||
| If necessary, creates the ./secrets directory with permissions 700, and creates | ||
| files within it with permissions 600. | ||
|
|
||
| Attempts to populate each secret as follows, in descending order of precedence: | ||
|
|
||
| 1. Looks for corresponding environment variable (EARTHDATA_USERNAME and | ||
| EARTHDATA_PASSWORD). | ||
| 2. Loads from .env.secrets file, if exists. | ||
| 3. Loads from .env file, if exists. | ||
| 4. Loads from netrc file, if exists (defaults to ~/.netrc, but can set NETRC env var) | ||
| 5. Prompts user as last resort. | ||
|
|
||
| This will ALWAYS overwrite existing secret files. | ||
| """ | ||
|
|
||
| # /// script | ||
| # requires-python = ">=3.12" | ||
| # dependencies = [ | ||
| # "python-dotenv>=1.2.1", | ||
| # ] | ||
| # /// | ||
| import logging | ||
| import netrc | ||
| import os | ||
| from getpass import getpass | ||
| from pathlib import Path | ||
| from typing import Mapping | ||
|
|
||
| from dotenv import dotenv_values | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
|
|
||
|
|
||
| def read_env() -> Mapping[str, str]: | ||
| """Read environment variables, env files, and netrc as unified 'environment'. | ||
|
|
||
| Returns a mapping of "environment variables", obtained from the following | ||
| sources in descending order of precedence: | ||
|
|
||
| - Environment variables (highest precedence) | ||
| - Variables from .env.secrets, if it exists | ||
| - Variables from .env, if it exists | ||
| - netrc file, if it exists (default location can be overridden via NETRC | ||
| environment variable): if an entry is found for host urs.earthdata.nasa.gov, | ||
| sets default values on returned mapping for EARTHDATA_USERNAME and | ||
| EARTHDATA_PASSWORD (i.e., only visible if these are not already set by | ||
| a source listed above, with higher precedence) | ||
| """ | ||
| env = { | ||
| **dotenv_values(".env"), | ||
| **dotenv_values(".env.secrets"), | ||
| **os.environ, | ||
| } | ||
|
|
||
| if auth := netrc.netrc(env.get("NETRC")).authenticators("urs.earthdata.nasa.gov"): | ||
| username, _, password = auth | ||
| env.setdefault("EARTHDATA_USERNAME", username) | ||
| env.setdefault("EARTHDATA_PASSWORD", password) | ||
|
|
||
| return env | ||
|
|
||
|
|
||
| def ensure_secrets_dir() -> Path: | ||
| """Make sure the secrets directory exists with mode 0o700 (rwx for user only).""" | ||
| path = Path("./secrets") | ||
| path.mkdir(mode=0o700, parents=True, exist_ok=True) | ||
|
|
||
| return path | ||
|
|
||
|
|
||
| def to_kebab_case(name: str) -> str: | ||
| """Convert variable name to kebab case. | ||
|
|
||
| Example: EARTHDATA_USERNAME becomes earthdata-username | ||
| """ | ||
| return name.lower().replace("_", "-") | ||
|
|
||
|
|
||
| def ask_user(prompt: str, sensitive: bool) -> str: | ||
| """Prompt user for a possibly sensitive value.""" | ||
| return getpass(prompt) if sensitive else input(prompt) | ||
|
|
||
|
|
||
| def write_secret(secrets_dir: Path, name: str, value: str) -> None: | ||
| """Write a secret (with only rw perms for user) to the secrets directory.""" | ||
| file = (secrets_dir / name).with_suffix(".txt") | ||
| file.write_text(value) | ||
| file.chmod(0o600) | ||
|
|
||
|
|
||
| def main(): | ||
| """Write Earthdata credentials as secrets for Docker. | ||
|
|
||
| Looks for EDL credentials in the environment, and if not found, prompts | ||
| user to enter values, then writes them as secret files for Docker to use. | ||
| """ | ||
| env = read_env() | ||
| secrets_dir = ensure_secrets_dir() | ||
|
|
||
| descriptors = ( | ||
| ("EARTHDATA_USERNAME", "Earthdata username: ", False), | ||
| ("EARTHDATA_PASSWORD", "Earthdata password: ", True), | ||
| ) | ||
|
|
||
| for name, prompt, sensitive in descriptors: | ||
| value = env.get(name) or ask_user(prompt, sensitive) | ||
| write_secret(secrets_dir, to_kebab_case(name), value) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 could all be a lot simpler if we simply instructed users to set the EARTHDATA_USERNAME and EARTHDATA_PASSWORD env vars. Then we wouldn't need the secret-writing script.
Uh oh!
There was an error while loading. Please reload this page.
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.
Unfortunately, using the env vars for the secrets is unreliable. There's a known issue for docker compose where it does not properly pass through the env vars to the secrets for docker, which is why I ended up going the route of the files, which is completely reliable, if a bit clunky.
It is possible for the env vars to be defined, but when building the image via docker compose, the secrets are populated as empty strings. In our case, that means the .netrc is written with blank values for the username and password, so the image is built without error, but when the app starts, it bombs because the creds are blank.
I had originally taken the approach you suggested above, but ran into this issue, which drove me nuts until I found the various issues around this. Some are marked as closed, but there still seems to be a problem, which might be macOS-specific, but I really don't know.
Unfortunately, the only reliable alternative was the file-based approach.
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.
However, if you feel this is perhaps too annoying, I'd be willing to avoid dealing with docker secrets since this is only for local development, so there's no serious security concern if we stick with env vars.
Let me know if that's what you'd prefer and I'll just close this PR without merging, and mark the related issue as "not planned".