Skip to content

Instantly share code, notes, and snippets.

@pgreze
Last active October 16, 2020 08:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pgreze/c22615dfff5eee60ad50ea6a83d0919d to your computer and use it in GitHub Desktop.
Save pgreze/c22615dfff5eee60ad50ea6a83d0919d to your computer and use it in GitHub Desktop.
Google Cloud Function allowing to expose private resources if accessed from a secured network
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
node_modules
#!include:.gitignore
env
.vscode
__pycache__
*.pyc
# Should be provided during CI execution, so ignore local one.
service-account.json

VPN bypass

This GCP function is allowing to access direct access to a bucket resource if accessed inside authorized networks.

Main use case is to allow people using the company VPN or in the office to download an Android APK without having to log-in with their personal account.

Setup Service Account

Permissions:

  • Service Account Token Creator
  • Storage Object Viewer

Try the service account by signing a resource access. Example:

$ gsutil signurl -d 10m service-account.json gs://your-bucket/cradopaud.jpg
URL	HTTP Method	Expiration	Signed URL
gs://your-bucket/cradopaud.jpg	GET	2020-10-09 13:45:23	https://storage.googleapis.com/...

Setup local environment

https://cloud.google.com/python/setup

python3.8 -m venv env source env/bin/activate pip install flask google-cloud-storage pip freeze > requirements.txt

gcloud components update gcloud config set project your-project gcloud functions describe vpn_bypass

gcloud functions deploy vpn_bypass --runtime python38 --trigger-http --allow-unauthenticated --service-account vpn-bypass@your-project.iam.gserviceaccount.com --set-env-vars BUCKET_NAME=your-bucket

Test locally: pip install functions-framework functions-framework --target vpn_bypass --debug curl http://0.0.0.0:8080

https://stackoverflow.com/a/53700497/5489877 https://github.com/GoogleCloudPlatform/functions-framework-python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import os.path
import json
import ipaddress
import flask
import google.auth
from datetime import timedelta
from google.auth.transport import requests
from google.auth import compute_engine
from google.oauth2 import service_account
from google.cloud import storage
# Force bucket resolution for security purpose.
bucket = os.environ["BUCKET_NAME"]
def vpn_bypass(request):
"""Responds to any HTTP request.
Args:
request (flask.Request): HTTP request object.
https://werkzeug.palletsprojects.com/en/1.0.x/wrappers/
Returns:
The response text or any set of values that can be turned into a
Response object using
`make_response <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>`.
"""
ip_address = request.headers["X-Forwarded-For"] # Notice: not working with remote_addr
path = request.path[1:] # Drop first / separator
print(f"Check if {ip_address} can bypass access to {bucket} resource {path}")
if from_authorized_network(ip_address):
return flask.redirect(get_signed_url(bucket, path))
else:
return flask.redirect(f"https://storage.cloud.google.com/{bucket}/{path}")
# TODO: Specific to the context of this gist, update it with your own way to fetch the IP list.
def from_authorized_network(ip_address):
ip_address = ipaddress.ip_address(ip_address)
# Load all known IP ranges of authorized networks
ip_list = json.loads(open("ip_list.json").read())
location_to_ranges = {
item["location"]: [ # Flatten all ip_list items in one
ipi for ipl in item["ip_list"] for ipi in (ipl["staff"] if ipl["staff"] else [])
] for item in ip_list
}
# Iterate until finding a range including the provided ip
for location, ip_ranges in location_to_ranges.items():
for ip_range in ip_ranges:
if ip_address in ipaddress.ip_network(ip_range):
print(f"{ip_address} found in {location}'s {ip_range} network")
return True
return False
# Generate a signed url to access this resource
# https://cloud.google.com/storage/docs/access-control/signed-urls
# Inspired by https://gist.github.com/jezhumble/91051485db4462add82045ef9ac2a0ec
# See also https://cloud.google.com/storage/docs/access-control/signing-urls-manually
def get_signed_url(bucket, path, expiresMn = 10):
# It has to be a JSON account file, tests with google.auth.default credentials always failed.
credentials = service_account.Credentials.from_service_account_file(
"service-account.json",
scopes=["https://www.googleapis.com/auth/devstorage.read_only"],
)
# Consider a storage bucket in the same project than this function.
_, project = google.auth.default()
storage_client = storage.Client(project, credentials)
data_bucket = storage_client.bucket(bucket)
blob = data_bucket.blob(path)
signing_credentials = compute_engine.IDTokenCredentials(
requests.Request(),
"",
service_account_email=credentials.service_account_email,
)
signed_url = blob.generate_signed_url(
expiration=timedelta(minutes=expiresMn),
method="GET",
credentials=signing_credentials,
version="v4",
)
return signed_url
cachetools==4.1.1
certifi==2020.6.20
cffi==1.14.3
chardet==3.0.4
click==7.1.2
cloudevents==0.3.0
Flask==1.1.2
google-api-core==1.22.4
google-auth==1.22.1
google-cloud-core==1.4.3
google-cloud-storage==1.31.2
google-crc32c==1.0.0
google-resumable-media==1.1.0
googleapis-common-protos==1.52.0
gunicorn==20.0.4
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pathtools==0.1.2
protobuf==3.13.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.20
pytz==2020.1
requests==2.24.0
rsa==4.6
six==1.15.0
urllib3==1.25.10
watchdog==0.10.3
Werkzeug==1.0.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment