Connect Cloud Functions to Postgres(on-prem) with Tailscale and Gost

Serverless functions have become increasingly popular due to their scalability and ease of use. However, connecting these functions to on-premises resources, such as databases, can be challenging. In this post, I'll explore an solution to connect Google Cloud Functions to an on-premises PostgreSQL database using Tailscale for secure networking and Gost for port forwarding.

Problem

Cloud Functions are designed to be stateless and ephemeral, making it difficult to establish persistent connections to resources outside the cloud environment. This becomes particularly problematic when you need to interact with on-premises databases that aren't directly accessible from the cloud.

Solution

We'll use a combination of technologies to bridge this gap:

  1. Google Cloud Functions: Our serverless compute platform.
  2. Tailscale: A modern VPN built on WireGuard, providing secure networking.
  3. Gost: A versatile proxy server that will forward our database connections.

Let's dive into the implementation details.

The Cloud Function code

First, let's look at our basic Cloud Function code:

import functions_framework

@functions_framework.cloud_event
def cloud_event_function(cloud_event):
    pass

This is a minimal Cloud Function that uses the functions_framework decorator. In a real-world scenario, you'd add your database interaction logic here.

Building the Custom Container

To incorporate Tailscale and Gost, we need to build a custom container. Here's our Dockerfile:

FROM golang:1.22.6-bullseye AS builder
# Build gost.
WORKDIR /proxy
RUN git clone https://github.com/ginuerzh/gost.git \
    && cd gost/cmd/gost \
    && go build

FROM python:3.10
# Copy gost binary.
WORKDIR /proxy
COPY --from=builder /proxy/gost/cmd/gost/gost ./

WORKDIR /app
# Install dependencies.
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code.
COPY . .
RUN chmod +x start.sh

# Copy Tailscale binaries from the tailscale image on Docker Hub
COPY --from=docker.io/tailscale/tailscale:stable /usr/local/bin/tailscaled /app/tailscaled
COPY --from=docker.io/tailscale/tailscale:stable /usr/local/bin/tailscale /app/tailscale
RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale

# Run on container startup.
CMD ["/app/start.sh"]

This Dockerfile does several important things:

  1. It builds Gost from source in a separate stage.
  2. It sets up a Python environment for our Cloud Function.
  3. It copies the Tailscale binaries from the official Docker image.

Configuring the Startup Script

The heart of our solution lies in the startup script:

#!/bin/sh
echo Tailscale starting...
/app/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 &
/app/tailscale up --authkey=${TAILSCALE_OAUTH_CLIENT_SECRET} --hostname=${TAILSCALE_MACHINE_NAME} --advertise-tags=tag:tag_for_your_postgres_server
echo Tailscale started

echo Gost starting...
/proxy/gost -L tcp://:5432/your_postgres_server:5432 -F socks5://localhost:1055 &
echo Gost started

functions-framework --target cloud_event_function --signature-type cloudevent

This script:

  1. Starts Tailscale in userspace networking mode and connects to your Tailscale network.
  2. Launches Gost, configuring it to forward connections from port 5432 to my on-prem PostgreSQL server through the Tailscale SOCKS5 proxy.
  3. Starts the Cloud Function using the functions-framework.

How It All Works Together

When deployed, this setup creates a secure tunnel between your Cloud Function and your on-premises network:

  1. Tailscale establishes a secure connection to your private network.
  2. Gost listens on port 5432 (the default PostgreSQL port) and forwards connections through the Tailscale network.
  3. Your Cloud Function can now connect to localhost:5432, and Gost will securely proxy the connection to your on-premises PostgreSQL server.

Deployment and Configuration

To deploy this solution:

  1. Build and push your container image to Google Container Registry or Artifact Registry.
  2. Deploy your Cloud Function, specifying your custom container.
  3. Set the necessary environment variables:
    • TAILSCALE_OAUTH_CLIENT_SECRET: Your Tailscale auth key
    • TAILSCALE_MACHINE_NAME: A unique name for this Tailscale node

Conclusion

This approach allows you to securely connect your Cloud Functions to on-premises resources without exposing your database to the public internet. It combines the scalability and ease of use of serverless functions with the security and flexibility of a modern VPN solution.

While We're focused on PostgreSQL, this method can be adapted for other databases or services. The combination of Tailscale and Gost provides a powerful and flexible way to bridge the gap between cloud and on-premises resources.

Remember to always follow security best practices and thoroughly test your setup before using it in a production environment.