def get_env(self) -> Dict[str, str]: revision = os.environ["K_REVISION"] project = self._get_from_metadata("project/project-id") zone = self._get_from_metadata("instance/zone") region = Path(zone).name[:-2] name = f"projects/{project}/locations/{region}/revisions/{revision}" cloudrun = self._build("run", "v1alpha1") try: service = cloudrun.projects().locations().revisions().get(name=name).execute() except HttpError as e: if e.resp.status == http.client.NOT_FOUND.value: raise AutoException("cloud run service not found") elif e.resp.status == http.client.UNAUTHORIZED.value: raise AutoException("permission denied to fetch cloud run info") raise AutoException(f"unable to fetch cloud run service: {e.content}") if "spec" not in service or "containers" not in service["spec"] or not service["spec"]["containers"]: raise AutoException(f"unable to get env from cloud run") env = service["spec"]["containers"][0]["env"] return {v["name"]: v["value"] for v in env}
def _decrypt_gcm(dek: bytes, ciphertext: bytes) -> bytes: # TODO use some constant from crypto library instead of hardcoding here nonce_size = 12 tag_size = 16 if len(ciphertext) < nonce_size + tag_size: raise AutoException("malformed ciphertext") nonce, ciphertext, tag = ciphertext[:nonce_size], ciphertext[ nonce_size:-tag_size], ciphertext[-tag_size:] cipher = AES.new(dek, AES.MODE_GCM, nonce=nonce) try: return cipher.decrypt_and_verify(ciphertext, tag) except ValueError: raise AutoException("failed to decrypt ciphertext")
def resolve(self, value: str) -> bytes: """ Resolves a berglas reference value to the plain text secret """ if not is_reference(value): raise AutoException("not a berglas reference") parsed = urlparse(value) bucket = parsed.netloc obj = parsed.path.lstrip("/") destination = parse_qs(parsed.query).get("destination", [""])[0] generation = int(parsed.fragment or 0) tmpfile = None if destination in ["tmpfile", "tempfile"]: tmpfile = tempfile.NamedTemporaryFile(prefix="berglas-", delete=False) elif destination: tmpfile = open(destination, "wb") plaintext = self._access(bucket, obj, generation=generation) if tmpfile: os.chmod(tmpfile.name, stat.S_IRUSR | stat.S_IWUSR) tmpfile.write(plaintext) plaintext = tmpfile.name.encode() tmpfile.close() return plaintext
def get_env(self) -> Dict[str, str]: name = ( f"projects/{os.environ['X_GOOGLE_GCP_PROJECT']}/locations/{os.environ['X_GOOGLE_FUNCTION_REGION']}/" f"functions/{os.environ['X_GOOGLE_FUNCTION_NAME']}" ) cloudfunctions_v1 = self._build("cloudfunctions", "v1") try: function = cloudfunctions_v1.projects().locations().functions().get(name=name).execute() except HttpError as e: if e.resp.status == http.client.NOT_FOUND.value: raise AutoException("function not found") elif e.resp.status == http.client.UNAUTHORIZED.value: raise AutoException("permission denied to fetch function info") raise AutoException(f"unable to fetch function: {e}") return function["environmentVariables"]
def _get_from_metadata(path: str) -> str: path = f"http://metadata.google.internal/computeMetadata/v1/{path}" http_client = discovery.build_http() set_user_agent(http_client, USER_AGENT) response, content = http_client.request(path, headers={"Metadata-Flavor": "Google", "User-Agent": USER_AGENT}) if response.status != http.client.OK.value: raise AutoException(f"failed to get metadata for: {path}: {content}") return content.decode()
def detect_runtime_environment() -> RuntimeEnv: """ Finds the most likely runtime environment """ if os.environ.get("X_GOOGLE_FUNCTION_NAME"): return CloudFunctionEnv() if os.environ.get("K_REVISION"): return CloudRunEnv() if os.environ.get("GAE_SERVICE"): return AppEngineEnv() raise AutoException("unknown runtime")
def get_env(self) -> Dict[str, str]: version = os.environ["GAE_VERSION"] service = os.environ["GAE_SERVICE"] project = self._get_from_metadata("project/project-id") gae = self._build("appengine", "v1") try: service_info = ( gae.apps() .services() .versions() .get(appsId=project, servicesId=service, versionsId=version, view="FULL") .execute() ) except HttpError as e: if e.resp.status == http.client.NOT_FOUND.value: raise AutoException("app engine service not found") elif e.resp.status == http.client.UNAUTHORIZED.value: raise AutoException("permission denied to fetch app engine info") raise AutoException(f"unable to fetch app engine service: {e.content}") return service_info["envVariables"]
def _access(self, bucket_name: str, path: str, generation: Optional[int] = None) -> bytes: """ Get plaintext value of the secret stored at path in bucket """ blob = self.storage_client.bucket(bucket_name).get_blob( path, client=self.storage_client, generation=generation) if not blob: raise AutoException("secret object not found") if not blob.metadata or not blob.metadata.get(METADATA_KMS_KEY): raise AutoException("missing kms key in secret metadata") key = blob.metadata[METADATA_KMS_KEY] data = blob.download_as_string() parts = data.split(b":", maxsplit=1) if len(parts) < 2: raise AutoException("invalid ciphertext: not enough parts") try: enc_dek = base64.decodebytes(parts[0]) except binascii.Error: raise AutoException("invalid ciphertext: failed to parse dek") try: ciphertext = base64.decodebytes(parts[1]) except binascii.Error: raise AutoException( "invalid ciphertext: failed to parse ciphertext") try: response = self.kms_client.decrypt(key, enc_dek, path.encode("UTF8")) dek = response.plaintext except (GoogleAPICallError, RetryError, ValueError): raise AutoException("failed to decrypt dek") return _decrypt_gcm(dek, ciphertext)