mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-16 03:48:09 +00:00
Storing secrets in azure keyvault (#326)
This commit is contained in:
@ -11,6 +11,8 @@ from azure.cli.core import CLIError
|
||||
from azure.common.client_factory import get_client_from_cli_profile
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import CheckGroupMembershipParameters
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from azure.keyvault.secrets import SecretClient
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
from azure.mgmt.subscription import SubscriptionClient
|
||||
from memoization import cached
|
||||
@ -134,3 +136,8 @@ def get_scaleset_principal_id() -> UUID:
|
||||
client = mgmt_client_factory(ResourceManagementClient)
|
||||
uid = client.resources.get_by_id(get_scaleset_identity_resource_path(), api_version)
|
||||
return UUID(uid.properties["principalId"])
|
||||
|
||||
|
||||
@cached
|
||||
def get_keyvault_client(vault_url: str) -> SecretClient:
|
||||
return SecretClient(vault_url=vault_url, credential=DefaultAzureCredential())
|
||||
|
@ -27,6 +27,7 @@ from memoization import cached
|
||||
from onefuzztypes.models import ADOTemplate, Report
|
||||
from onefuzztypes.primitives import Container
|
||||
|
||||
from ..secrets import get_secret_string_value
|
||||
from .common import Render, fail_task
|
||||
|
||||
|
||||
@ -54,7 +55,8 @@ class ADO:
|
||||
):
|
||||
self.config = config
|
||||
self.renderer = Render(container, filename, report)
|
||||
self.client = get_ado_client(self.config.base_url, self.config.auth_token)
|
||||
auth_token = get_secret_string_value(self.config.auth_token)
|
||||
self.client = get_ado_client(self.config.base_url, auth_token)
|
||||
self.project = self.render(self.config.project)
|
||||
|
||||
def render(self, template: str) -> str:
|
||||
|
@ -10,9 +10,10 @@ from github3 import login
|
||||
from github3.exceptions import GitHubException
|
||||
from github3.issues import Issue
|
||||
from onefuzztypes.enums import GithubIssueSearchMatch
|
||||
from onefuzztypes.models import GithubIssueTemplate, Report
|
||||
from onefuzztypes.models import GithubAuth, GithubIssueTemplate, Report
|
||||
from onefuzztypes.primitives import Container
|
||||
|
||||
from ..secrets import get_secret_obj
|
||||
from .common import Render, fail_task
|
||||
|
||||
|
||||
@ -26,9 +27,12 @@ class GithubIssue:
|
||||
):
|
||||
self.config = config
|
||||
self.report = report
|
||||
self.gh = login(
|
||||
username=config.auth.user, password=config.auth.personal_access_token
|
||||
)
|
||||
if isinstance(config.auth.secret, GithubAuth):
|
||||
auth = config.auth.secret
|
||||
else:
|
||||
auth = get_secret_obj(config.auth.secret.url, GithubAuth)
|
||||
|
||||
self.gh = login(username=auth.user, password=auth.personal_access_token)
|
||||
self.renderer = Render(container, filename, report)
|
||||
|
||||
def render(self, field: str) -> str:
|
||||
|
@ -11,6 +11,7 @@ from onefuzztypes.models import Report, TeamsTemplate
|
||||
from onefuzztypes.primitives import Container
|
||||
|
||||
from ..azure.containers import auth_download_url
|
||||
from ..secrets import get_secret_string_value
|
||||
from ..tasks.config import get_setup_container
|
||||
from ..tasks.main import Task
|
||||
|
||||
@ -46,7 +47,8 @@ def send_teams_webhook(
|
||||
if text:
|
||||
message["sections"].append({"text": text})
|
||||
|
||||
response = requests.post(config.url, json=message)
|
||||
config_url = get_secret_string_value(config.url)
|
||||
response = requests.post(config_url, json=message)
|
||||
if not response.ok:
|
||||
logging.error("webhook failed %s %s", response.status_code, response.content)
|
||||
|
||||
|
@ -14,6 +14,7 @@ from typing import (
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
@ -33,11 +34,12 @@ from onefuzztypes.enums import (
|
||||
UpdateType,
|
||||
VmState,
|
||||
)
|
||||
from onefuzztypes.models import Error
|
||||
from onefuzztypes.models import Error, SecretData
|
||||
from onefuzztypes.primitives import Container, PoolName, Region
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from ..onefuzzlib.secrets import save_to_keyvault
|
||||
from .azure.table import get_client
|
||||
from .telemetry import track_event_filtered
|
||||
from .updates import queue_update
|
||||
@ -268,18 +270,49 @@ class ORMMixin(ModelMixin):
|
||||
|
||||
return (partition_key, row_key)
|
||||
|
||||
@classmethod
|
||||
def hide_secrets(
|
||||
cls,
|
||||
model: BaseModel,
|
||||
hider: Callable[["SecretData"], None],
|
||||
visited: Set[int] = set(),
|
||||
) -> None:
|
||||
if id(model) in visited:
|
||||
return
|
||||
|
||||
visited.add(id(model))
|
||||
for field in model.__fields__:
|
||||
field_data = getattr(model, field)
|
||||
if isinstance(field_data, SecretData):
|
||||
hider(field_data)
|
||||
elif isinstance(field_data, List):
|
||||
if len(field_data) > 0:
|
||||
if not isinstance(field_data[0], BaseModel):
|
||||
continue
|
||||
for data in field_data:
|
||||
cls.hide_secrets(data, hider, visited)
|
||||
elif isinstance(field_data, dict):
|
||||
for key in field_data:
|
||||
if not isinstance(field_data[key], BaseModel):
|
||||
continue
|
||||
cls.hide_secrets(field_data[key], hider, visited)
|
||||
else:
|
||||
if isinstance(field_data, BaseModel):
|
||||
cls.hide_secrets(field_data, hider, visited)
|
||||
|
||||
def save(self, new: bool = False, require_etag: bool = False) -> Optional[Error]:
|
||||
self.__class__.hide_secrets(self, save_to_keyvault)
|
||||
# TODO: migrate to an inspect.signature() model
|
||||
raw = self.raw(by_alias=True, exclude_none=True, exclude=self.save_exclude())
|
||||
for key in raw:
|
||||
if not isinstance(raw[key], (str, int)):
|
||||
raw[key] = json.dumps(raw[key])
|
||||
|
||||
# for datetime fields that passed through filtering, use the real value,
|
||||
# rather than a serialized form
|
||||
for field in self.__fields__:
|
||||
if field not in raw:
|
||||
continue
|
||||
# for datetime fields that passed through filtering, use the real value,
|
||||
# rather than a serialized form
|
||||
if self.__fields__[field].type_ == datetime:
|
||||
raw[field] = getattr(self, field)
|
||||
|
||||
|
79
src/api-service/__app__/onefuzzlib/secrets.py
Normal file
79
src/api-service/__app__/onefuzzlib/secrets.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
|
||||
from typing import Tuple, Type, TypeVar, cast
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from azure.keyvault.secrets import KeyVaultSecret
|
||||
from onefuzztypes.models import SecretAddress, SecretData
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .azure.creds import get_instance_name, get_keyvault_client
|
||||
|
||||
A = TypeVar("A", bound=BaseModel)
|
||||
|
||||
|
||||
def save_to_keyvault(secret_data: SecretData) -> None:
|
||||
if isinstance(secret_data.secret, SecretAddress):
|
||||
return
|
||||
|
||||
secret_name = str(uuid4())
|
||||
if isinstance(secret_data.secret, str):
|
||||
secret_value = secret_data.secret
|
||||
elif isinstance(secret_data.secret, BaseModel):
|
||||
secret_value = secret_data.secret.json()
|
||||
else:
|
||||
raise Exception("invalid secret data")
|
||||
|
||||
kv = store_in_keyvault(get_keyvault_address(), secret_name, secret_value)
|
||||
secret_data.secret = SecretAddress(url=kv.id)
|
||||
|
||||
|
||||
def get_secret_string_value(self: SecretData[str]) -> str:
|
||||
if isinstance(self.secret, SecretAddress):
|
||||
secret = get_secret(self.secret.url)
|
||||
return cast(str, secret.value)
|
||||
else:
|
||||
return self.secret
|
||||
|
||||
|
||||
def get_keyvault_address() -> str:
|
||||
# https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name
|
||||
return f"https://{get_instance_name()}-vault.vault.azure.net"
|
||||
|
||||
|
||||
def store_in_keyvault(
|
||||
keyvault_url: str, secret_name: str, secret_value: str
|
||||
) -> KeyVaultSecret:
|
||||
keyvault_client = get_keyvault_client(keyvault_url)
|
||||
kvs: KeyVaultSecret = keyvault_client.set_secret(secret_name, secret_value)
|
||||
return kvs
|
||||
|
||||
|
||||
def parse_secret_url(secret_url: str) -> Tuple[str, str]:
|
||||
# format: https://{vault-name}.vault.azure.net/secrets/{secret-name}/{version}
|
||||
u = urlparse(secret_url)
|
||||
vault_url = f"{u.scheme}://{u.netloc}"
|
||||
secret_name = u.path.split("/")[2]
|
||||
return (vault_url, secret_name)
|
||||
|
||||
|
||||
def get_secret(secret_url: str) -> KeyVaultSecret:
|
||||
(vault_url, secret_name) = parse_secret_url(secret_url)
|
||||
keyvault_client = get_keyvault_client(vault_url)
|
||||
return keyvault_client.get_secret(secret_name)
|
||||
|
||||
|
||||
def get_secret_obj(secret_url: str, model: Type[A]) -> A:
|
||||
secret = get_secret(secret_url)
|
||||
return model.parse_raw(secret.value)
|
||||
|
||||
|
||||
def delete_secret(secret_url: str) -> None:
|
||||
(vault_url, secret_name) = parse_secret_url(secret_url)
|
||||
keyvault_client = get_keyvault_client(vault_url)
|
||||
keyvault_client.begin_delete_secret(secret_name).wait()
|
Reference in New Issue
Block a user