Repository navigation
feat(credentials): add sovereign cloud process (#8762) #373
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ | |
| from contextlib import contextmanager | ||
| from dataclasses import dataclass, field | ||
| from pathlib import Path | ||
| from typing import Callable, Dict, Iterator, List, Optional | ||
| from typing import Callable, Dict, Iterator, List, Optional, Union | ||
|
|
||
| from pyoaev.credential.errors import ( | ||
| CredentialErrorCode, | ||
|
|
@@ -125,13 +125,22 @@ def _aws_region_env(region: Optional[str]) -> Dict[str, str]: | |
| return {"AWS_REGION": region, "AWS_DEFAULT_REGION": region} | ||
|
|
||
|
|
||
| def _aws_endpoint_env(endpoint_url: Optional[str]) -> Dict[str, str]: | ||
| # Single host for every service: fits a mocked cloud or a proxy. A real | ||
| # AWS partition (GovCloud, China) is reached through its region alone. | ||
| if not endpoint_url: | ||
| return {} | ||
| return {"AWS_ENDPOINT_URL": endpoint_url, "AWS_ENDPOINT_URL_STS": endpoint_url} | ||
|
|
||
|
|
||
| def _aws_access_key( | ||
| secret: AwsAccessKeySecret, workspace: _Workspace | ||
| ) -> Dict[str, str]: | ||
| env = { | ||
| **_aws_region_env(secret.aws_default_region), | ||
| "AWS_ACCESS_KEY_ID": secret.aws_access_key_id, | ||
| "AWS_SECRET_ACCESS_KEY": secret.aws_secret_access_key, | ||
| **_aws_endpoint_env(secret.aws_endpoint_url), | ||
| } | ||
| if secret.aws_session_token: | ||
| env["AWS_SESSION_TOKEN"] = secret.aws_session_token | ||
|
|
@@ -144,6 +153,7 @@ def _aws_assume_role( | |
| env = { | ||
| **_aws_region_env(secret.aws_default_region), | ||
| "AWS_PROFILE": AWS_TARGET_PROFILE, | ||
| **_aws_endpoint_env(secret.aws_endpoint_url), | ||
| } | ||
| profile: Dict[str, Optional[str]] = {"role_arn": secret.aws_role_arn} | ||
| if secret.aws_source_identity_type == AwsSourceIdentityType.INSTANCE_DEFAULT: | ||
|
|
@@ -181,14 +191,48 @@ def _azure_authority_host(environment: str) -> str: | |
| raise _UnsupportedAzureEnvironmentError() from None | ||
|
|
||
|
|
||
| def _azure_sovereign_env( | ||
| secret: Union[AzureServicePrincipalSecret, AzureManagedIdentitySecret], | ||
| workspace: _Workspace, | ||
| ) -> Dict[str, str]: | ||
| """Authority host and trusted CA of the credential, shared by both methods. | ||
|
|
||
| ``REQUESTS_CA_BUNDLE`` (Python SDK) and ``SSL_CERT_FILE`` (Go, stratus | ||
| ignores the former) replace the default trust store of their client: the | ||
| certificate must contain every CA needed by the authority host. | ||
|
|
||
| ``AZURE_POD_IDENTITY_AUTHORITY_HOST`` is deliberately not set: despite its | ||
| name, azure-identity uses it as the managed identity (IMDS) endpoint, not | ||
| as the Entra authority. Pointing it to a login server would break the | ||
| managed identity token requests. | ||
| """ | ||
| # The environment is always validated, even when the host is overridden. | ||
| env = {"AZURE_AUTHORITY_HOST": _azure_authority_host(secret.azure_environment)} | ||
| if secret.azure_authority_host_uri: | ||
| # The Go SDK requires the trailing slash. | ||
| authority_host = secret.azure_authority_host_uri | ||
| if not authority_host.endswith("/"): | ||
| authority_host += "/" | ||
| env["AZURE_AUTHORITY_HOST"] = authority_host | ||
| if secret.azure_requests_certificate: | ||
| bundle = str( | ||
| workspace.write( | ||
| secret.azure_requests_certificate, prefix="azure-ca-", suffix=".pem" | ||
| ) | ||
| ) | ||
| env["REQUESTS_CA_BUNDLE"] = bundle | ||
| env["SSL_CERT_FILE"] = bundle | ||
| return env | ||
|
|
||
|
|
||
| def _azure_service_principal( | ||
| secret: AzureServicePrincipalSecret, workspace: _Workspace | ||
| ) -> Dict[str, str]: | ||
| env = { | ||
| "AZURE_CLIENT_ID": secret.azure_client_id, | ||
| "AZURE_CLIENT_SECRET": secret.azure_client_secret, | ||
| "AZURE_TENANT_ID": secret.azure_tenant_id, | ||
| "AZURE_AUTHORITY_HOST": _azure_authority_host(secret.azure_environment), | ||
| **_azure_sovereign_env(secret, workspace), | ||
| } | ||
| if secret.azure_subscription_id: | ||
| env["AZURE_SUBSCRIPTION_ID"] = secret.azure_subscription_id | ||
|
|
@@ -198,7 +242,7 @@ def _azure_service_principal( | |
| def _azure_managed_identity( | ||
| secret: AzureManagedIdentitySecret, workspace: _Workspace | ||
| ) -> Dict[str, str]: | ||
| env = {"AZURE_AUTHORITY_HOST": _azure_authority_host(secret.azure_environment)} | ||
| env = _azure_sovereign_env(secret, workspace) | ||
| # Without a client id, the system-assigned identity is used. | ||
| if secret.azure_client_id: | ||
| env["AZURE_CLIENT_ID"] = secret.azure_client_id | ||
|
|
@@ -215,7 +259,9 @@ def _gcp_env( | |
| "GOOGLE_CLOUD_UNIVERSE_DOMAIN": scope, | ||
| } | ||
| if project_id: | ||
| # Stratus and Terraform read the former, Prowler the latter. | ||
| env["GOOGLE_PROJECT"] = project_id | ||
| env["GOOGLE_CLOUD_PROJECT"] = project_id | ||
| return env | ||
|
|
||
|
|
||
|
|
@@ -225,7 +271,12 @@ def _gcp_service_account( | |
| credentials_file = workspace.write( | ||
| secret.gcp_private_key_json, prefix="gcp-credentials-", suffix=".json" | ||
| ) | ||
| return _gcp_env(credentials_file, secret.gcp_scope, secret.gcp_project_id) | ||
| env = _gcp_env(credentials_file, secret.gcp_scope, secret.gcp_project_id) | ||
| if secret.gcp_custom_resource_manager_uri: | ||
| env["GOOGLE_RESOURCE_MANAGER_CUSTOM_ENDPOINT"] = ( | ||
| secret.gcp_custom_resource_manager_uri | ||
| ) | ||
|
Comment on lines
+276
to
+278
|
||
| return env | ||
|
|
||
|
|
||
| def _gcp_oauth2(secret: GcpOAuth2Secret, workspace: _Workspace) -> Dict[str, str]: | ||
|
|
@@ -241,7 +292,17 @@ def _gcp_oauth2(secret: GcpOAuth2Secret, workspace: _Workspace) -> Dict[str, str | |
| prefix="gcp-credentials-", | ||
| suffix=".json", | ||
| ) | ||
| return _gcp_env(credentials_file, secret.gcp_scope, secret.gcp_project_id) | ||
| env = _gcp_env(credentials_file, secret.gcp_scope, secret.gcp_project_id) | ||
| if secret.gcp_custom_token_uri: | ||
| env["GOOGLE_OAUTH2_TOKEN_URI_OVERRIDE"] = secret.gcp_custom_token_uri | ||
| if secret.gcp_custom_iam_uri: | ||
| env["GOOGLE_IAM_CUSTOM_ENDPOINT"] = secret.gcp_custom_iam_uri | ||
|
Comment on lines
+296
to
+299
Comment on lines
+296
to
+299
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (blocking): Which consumer reads google-auth ignores them ( |
||
| if secret.gcp_custom_oauth_access_token: | ||
| # Terraform (stratus) reads the former, Prowler and gcloud the latter. | ||
| # Prowler then prefers the token over the credentials file. | ||
| env["GOOGLE_OAUTH_ACCESS_TOKEN"] = secret.gcp_custom_oauth_access_token | ||
| env["CLOUDSDK_AUTH_ACCESS_TOKEN"] = secret.gcp_custom_oauth_access_token | ||
| return env | ||
|
|
||
|
|
||
| _MATERIALIZERS: Dict[SecretType, Callable[..., Dict[str, str]]] = { | ||
|
|
||
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.
suggestion (non-blocking): Validate the new URL fields at parse time (http(s) scheme, https for the Azure authority, no whitespace/userinfo) and raise
InvalidResolvedSecretError, like_ini_valuedoes.Today
"https://host "becomes"https://host /"and failsThere 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.
Done in 5abb50e: the URL fields are validated at parse time in
resolved.py(http/https scheme, https only for the Azure authority, host required, no whitespace, no userinfo) and raiseInvalidResolvedSecretErrorwithout quoting the value."https://host "is now rejected.