---
title: "Gmail Pub/Sub on a VPS: Fix the Org Policy Blocker That Stops Every Google Workspace Setup"
url: "https://managemyclaw.com/blog/gmail-pubsub-org-policy-vps-guide/"
date: "2026-03-29T11:36:33-04:00"
modified: "2026-03-29T12:00:55-04:00"
author:
  name: "Rakesh Patel"
  url: "https://www.rakeshpatel.co"
categories:
  - "OpenClaw Guides"
tags:
  - "gmail-pubsub"
  - "google-cloud"
  - "oauth"
  - "openclaw-setup"
  - "vps-deployment"
word_count: 3156
reading_time: "16 min read"
summary: "Every Google Workspace org policy blocks the same service account. You run the gcloud pubsub topics add-iam-policy-binding command, it fails with iam.allowedPolicyMemberDomains, and you spend the n..."
description: "Fix the Gmail Pub/Sub org policy blocker on your VPS. Step-by-step guide to real-time push notifications with Google Cloud for OpenClaw agents."
keywords: "gmail pubsub org policy, gmail-pubsub, google-cloud, oauth, openclaw-setup, vps-deployment"
language: "en"
schema_type: "Article"
related_posts:
  - title: "OpenClaw Gog OAuth Setup: Every Error and Fix"
    url: "https://managemyclaw.com/blog/openclaw-gog-oauth-setup-errors/"
  - title: "How We Deploy OpenClaw for Google Workspace: A 12-Step Production Walkthrough"
    url: "https://managemyclaw.com/blog/openclaw-google-workspace-deployment/"
---

# Gmail Pub/Sub on a VPS: Fix the Org Policy Blocker That Stops Every Google Workspace Setup

_Published: March 29, 2026_  
_Author: Rakesh Patel_  

![Gmail Pub/Sub setup with OpenClaw on VPS](https://managemyclaw.com/wp-content/uploads/2026/03/CS06-blog-gmail-pubsub-guide-hero-1024x538.jpg)

**Every Google Workspace org policy blocks the same service account.** You run the `gcloud pubsub topics add-iam-policy-binding` command, it fails with `iam.allowedPolicyMemberDomains`, and you spend the next three hours searching the wrong admin console. We have deployed Gmail Pub/Sub on bare-metal VPS servers for multiple [OpenClaw Google Workspace installations](/blog/openclaw-google-workspace-deployment/) through ManageMyClaw, and the org policy blocker is the single issue that stops every first-time setup cold.

Gmail Pub/Sub delivers email notifications in 30 seconds flat. The org policy error that blocks it takes 3 hours to diagnose — not because the fix is hard, but because Google buries it in the wrong console.

OpenClaw is an open-source AI agent framework that automates Gmail, Calendar, and other Google Workspace services on your own VPS. Gog is the CLI tool that handles OpenClaw’s Google integration — OAuth, Gmail watch, webhook serving, and Pub/Sub plumbing. ManageMyClaw deploys and maintains these systems so you don’t debug org policies at 2 AM. *Three products, one pipeline, and a single IAM binding standing between you and real-time email processing.*

This guide walks through the **complete Gmail Pub/Sub setup on a bare-metal VPS**, from project creation to end-to-end verification. The org policy fix is in Step 3a — but if you skip ahead, you’ll miss the context that makes the fix make sense.

Architecture

## How Gmail Pub/Sub Delivers Real-Time Notifications

**The pipeline has seven handoffs, and every one must work for a single notification to reach your OpenClaw agent.** A new email arrives at your Google Workspace inbox. Gmail’s Watch API detects the change and publishes a notification to your Google Cloud Pub/Sub topic. Pub/Sub pushes a POST request to your HTTPS webhook endpoint. Nginx terminates SSL and proxies the request to `gog serve` running on localhost. Gog fetches the full email content via the Gmail API. Your OpenClaw agent processes the email — categorizes it, drafts a reply, fires a notification.

~30sEnd-to-end latency from inbox arrival to agent processing| Component | Purpose |
|---|---|
| **Google Cloud Project** | Houses Pub/Sub topic, OAuth credentials, API access |
| **Gmail API** | Email access and watch registration |
| **Cloud Pub/Sub API** | Message delivery from Gmail to your server |
| **OAuth 2.0 Client (Desktop)** | Headless auth for your VPS-based OpenClaw agent |
| **HTTPS Webhook Endpoint** | Receives Pub/Sub push notifications via nginx |
| **Gog CLI** | Processes notifications, fetches email, forwards to OpenClaw |
| **SSL Certificate** | Required — Pub/Sub only pushes to HTTPS endpoints |

*Seven components to receive a push notification that polling handles with a single cron line. The trade-off is latency: 30 seconds vs. up to 5 minutes.*

Prerequisites

## What You Need Before Starting

Prerequisites Checklist

- **Google Workspace account** (e.g., `support@yourdomain.com`) — personal Gmail accounts work but lack org policy controls
- **A VPS with a public IP** and a domain pointing to it — bare-metal or cloud VM with systemd
- **Nginx installed with SSL** via Let’s Encrypt — Pub/Sub will not deliver to HTTP endpoints
- **`gcloud` CLI installed** on your local machine — for GCP project management and org policy overrides
- **Gog CLI installed on the VPS** — [OAuth must be configured first](/blog/openclaw-gog-oauth-setup-errors/)
- **OpenClaw agent running** on the VPS with a hook endpoint for incoming email events

Step 1

## Create a Google Cloud Project and Enable APIs

**Start at the [Google Cloud Console](https://console.cloud.google.com).** Create a new project (e.g., `my-email-notifications`) and note the Project ID — you’ll reference it in every command from here forward.

$ gcloud services enable gmail.googleapis.com pubsub.googleapis.com  –project=my-email-notifications✓ Gmail API enabled✓ Cloud Pub/Sub API enabledIf your OpenClaw agent also handles Calendar, Drive, or Contacts, enable those APIs now too:

$ gcloud services enable calendar-json.googleapis.com  drive.googleapis.com people.googleapis.com  tasks.googleapis.com –project=my-email-notifications✓ Additional APIs enabledAPIs vs. OAuth scopes**Enabling an API and granting an OAuth scope are two different things.** Having the Gmail scope in your OAuth consent screen does not mean the Gmail API is active in your GCP project. You need both. This distinction trips up even experienced developers.

Step 2

## Set Up OAuth 2.0 Credentials

**Create an OAuth consent screen set to “Internal”** — this skips Google’s app verification process entirely, which only applies to external apps. Add all required scopes for Gmail, Calendar, Drive, and any other services your OpenClaw agent will use.

1. **Go to APIs & Services > OAuth consent screen.** Select Internal. Set app name, support email, and developer contact.
2. **Go to APIs & Services > Credentials.** Click Create Credentials > OAuth client ID. Type: Desktop app. Name it something like `OpenClaw VPS Watcher`.
3. **Download the `client_secret.json` file** and upload it to your VPS:

$ scp client_secret.json user@your-vps:/opt/openclaw/google-client-secret.json✓ Credentials uploaded*Desktop app type, not web app. This matters for the headless OAuth flow you’ll run on the VPS in Step 6.*

Step 3

## Create the Pub/Sub Topic and Grant Gmail Permission

**This is where most setups break.** Creating the topic is straightforward. Granting Gmail permission to publish to it is not.

$ gcloud pubsub topics create gmail-inbox-watch  –project=my-email-notifications✓ Topic createdNow grant Gmail’s internal service account permission to publish messages to your topic:

$ gcloud pubsub topics add-iam-policy-binding  projects/my-email-notifications/topics/gmail-inbox-watch  –member=’serviceAccount:gmail-api-push@system.gserviceaccount.com’  –role=’roles/pubsub.publisher’**If this command succeeds, skip to Step 4.** If it fails with the `iam.allowedPolicyMemberDomains` error, you’ve hit the org policy blocker. Keep reading.

Step 3a

## Fixing the Org Policy Blocker — The Error That Stops Every Setup

The Error OutputFAILED_PRECONDITIONFAILED_PRECONDITION: One or more users named in the policydo not belong to a permitted customer. – description: User gmail-api-push@system.gserviceaccount.com is not in permitted organization. – type: constraints/iam.allowedPolicyMemberDomains**What’s happening:** Google Workspace organizations have a default policy called [Domain Restricted Sharing](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains) (`iam.allowedPolicyMemberDomains`). This policy prevents adding IAM members from outside your organization. The problem is that `gmail-api-push@system.gserviceaccount.com` is a **Google-owned system service account**. It’s not part of your org, so the policy blocks it — even though Gmail Pub/Sub requires it.

The #1 mistake: wrong console**This is configured in the Google Cloud Console** (`console.cloud.google.com`), NOT the Google Workspace Admin Console (`admin.google.com`). The Workspace Admin Console has a “Google Cloud session control” page that looks related but controls a completely different setting. This distinction costs people hours.

### Option A: Override via gcloud CLI (Recommended)

First, enable the [Organization Policy API](https://cloud.google.com/resource-manager/docs/organization-policy/overview):

$ gcloud services enable orgpolicy.googleapis.com  –project=my-email-notifications✓ Organization Policy API enabledThen override the policy at the project level:

$ gcloud org-policies set-policy /dev/stdin  –project=my-email-notifications <<‘EOF’name: projects/my-email-notifications/policies/iam.allowedPolicyMemberDomainsspec: rules: – allowAll: trueEOF✓ Policy overridden at project level

### Option B: Override via GCP Console UI

1. **Navigate to:** `console.cloud.google.com/iam-admin/orgpolicies/iam.allowedPolicyMemberDomains?project=my-email-notifications`
2. **Click “MANAGE POLICY”** and select “Override parent’s policy”
3. **Add a rule** set to “Allow All” and click Save

Required IAM roleYour account needs the **Organization Policy Administrator** role at the organization level. If you don’t have it, an existing admin must grant it:

$ gcloud organizations listDISPLAY_NAME ID DIRECTORY_CUSTOMER_IDyourdomain 123456789 C0xxxxxxx$ gcloud organizations add-iam-policy-binding 123456789  –member=’user:admin@yourdomain.com’  –role=’roles/orgpolicy.policyAdmin’✓ Role granted

### Now Retry the Permission Grant

$ gcloud pubsub topics add-iam-policy-binding  projects/my-email-notifications/topics/gmail-inbox-watch  –member=’serviceAccount:gmail-api-push@system.gserviceaccount.com’  –role=’roles/pubsub.publisher’✓ Updated IAM policy for topic [gmail-inbox-watch]bindings:– members: – serviceAccount:gmail-api-push@system.gserviceaccount.com role: roles/pubsub.publisherSecurity: re-tighten after granting**After the IAM binding succeeds, re-tighten the org policy immediately.** Existing bindings are NOT retroactively removed — the policy only checks at binding creation time. Run: `gcloud org-policies reset iam.allowedPolicyMemberDomains --project=my-email-notifications`

Step 4

## Create the Push Subscription

$ gcloud pubsub subscriptions create gmail-inbox-push  –topic=projects/my-email-notifications/topics/gmail-inbox-watch  –push-endpoint=”https://webhook.yourdomain.com/gmail-pubsub”  –ack-deadline=30  –project=my-email-notifications✓ Subscription created**Four requirements for the push endpoint:** must be HTTPS (not HTTP), must have a valid SSL certificate (Let’s Encrypt works), must be publicly accessible from the internet, and must respond with a 2xx status code to acknowledge messages.

Step 5

## Configure Nginx as a Reverse Proxy

**Gog runs on localhost (port 8788 by default). Nginx terminates SSL and proxies Pub/Sub push notifications to it.**



# /etc/nginx/sites-available/webhookserver { listen 80; server_name webhook.yourdomain.com; return 301 https://$host$request_uri;}server { listen 443 ssl http2; server_name webhook.yourdomain.com; ssl_certificate /etc/letsencrypt/live/webhook.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/webhook.yourdomain.com/privkey.pem; add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; location /gmail-pubsub { proxy_pass http://127.0.0.1:8788; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; client_max_body_size 1m; } location /health { return 200 ‘OK’; add_header Content-Type text/plain; } location / { return 403 ‘Forbidden’; }}Get the SSL certificate and enable the site:

$ certbot certonly –webroot -w /var/www/html  -d webhook.yourdomain.com  –non-interactive –agree-tos –email admin@yourdomain.com✓ Certificate obtained$ ln -sf /etc/nginx/sites-available/webhook /etc/nginx/sites-enabled/$ nginx -t && systemctl reload nginx✓ Nginx reloadedStep 6

## Authenticate OAuth on the VPS (Headless Flow)

**Your VPS has no browser. Gog uses a 2-step remote auth flow** — generate the URL on the VPS, authorize in your laptop’s browser, paste the callback URL back.

### Generate the Auth URL (on VPS)

$ gog login user@yourdomain.com  –services=all –remote –step=1  –force-consent –no-inputauth_url: https://accounts.google.com/o/oauth2/v2/auth?client_id=…

### Authorize in Browser (on your laptop)

Open the auth URL in your browser. Sign in with the correct Google account. Grant all requested permissions. The browser redirects to `http://127.0.0.1:XXXXX/oauth2/callback?...` and shows a “can’t connect” error. **This is expected behavior.** Copy the full URL from the address bar.

### Exchange the Code (on VPS)

$ gog login user@yourdomain.com  –services=all –remote –step=2  –force-consent –no-input  –auth-url ‘http://127.0.0.1:XXXXX/oauth2/callback?state=…&code=…&scope=…’✓ OAuth tokens savedCommon OAuth mistakes**URL breaks across lines when copying from SSH** — widen your terminal window first. Auth codes are time-sensitive — complete the exchange within 5 minutes. And always use `--force-consent` when re-authenticating to ensure a fresh refresh token.

*The “can’t connect” page in your browser is the part that panics every first-timer. It’s normal. The auth code is embedded in the URL parameters — you don’t need the page to actually load.*

Step 7

## Start the Gmail Watch

**Register a watch on the inbox.** This tells Gmail to publish notifications to your Pub/Sub topic whenever a new email arrives.

$ gog gmail watch start  –label INBOX  –topic projects/my-email-notifications/topics/gmail-inbox-watch  –account user@yourdomain.com  –no-inputaccount user@yourdomain.comtopic projects/my-email-notifications/topics/gmail-inbox-watchlabels INBOXhistory_id 1056216expiration 2026-04-05T14:21:02Z7 daysGmail watch expiration — no notification, no grace period, silent failure*That expiration date is not a suggestion. When it passes, Gmail silently stops publishing. No error in your logs. No alert. Your agent just stops receiving email notifications and nobody knows until someone asks why the urgent email from 3 hours ago wasn’t processed.*

Step 8

## Set Up the Webhook Server as a Systemd Service

**Run `gog gmail watch serve` as a systemd service** so it survives reboots and auto-restarts on failure.



# /etc/systemd/system/gmail-webhook.service[Unit]Description=Gmail Webhook ServerAfter=network.target[Service]Type=simpleUser=rootEnvironment=GOG_KEYRING_PASSWORD=your-password-hereEnvironment=GOG_ACCOUNT=user@yourdomain.comEnvironment=GOG_KEYRING_BACKEND=fileExecStart=/usr/local/bin/gog gmail watch serve  –bind 127.0.0.1 –port 8788  –path /gmail-pubsub  –include-body –max-bytes 20000  –hook-url http://127.0.0.1:YOUR_OPENCLAW_PORT/hooks/gmail  –hook-token YOUR_HOOK_TOKEN  –no-inputRestart=alwaysRestartSec=10[Install]WantedBy=multi-user.targetOrder matters: watch before serve**Start the Gmail watch (Step 7) BEFORE starting this service.** The service will crash with `watch state not found` if the watch hasn’t been registered yet. This creates a restart loop that systemd will eventually give up on.

$ systemctl enable gmail-webhook.service$ systemctl start gmail-webhook.service$ systemctl status gmail-webhook.service✓ Active (running)$ ss -tlnp | grep 8788LISTEN 0 128 127.0.0.1:8788 0.0.0.0:*Step 9

## Set Up Watch Renewal Cron

**Gmail watch expires every 7 days with zero warning.** Create a daily renewal script that runs before the watch can expire. For more on [cron-based email automation with OpenClaw](/blog/openclaw-cron-email-triage-calendar/), see our dedicated guide.

$ cat > /usr/local/bin/gmail-watch-renew.sh << ‘EOF’#!/bin/bashexport GOG_KEYRING_PASSWORD=your-password-hereexport GOG_ACCOUNT=user@yourdomain.comexport GOG_KEYRING_BACKEND=file/usr/local/bin/gog gmail watch start  –label INBOX  –topic projects/my-email-notifications/topics/gmail-inbox-watch  >> /var/log/gmail-watch-renew.log 2>&1EOF$ chmod +x /usr/local/bin/gmail-watch-renew.sh✓ Renewal script created$ (crontab -l 2>/dev/null; echo ‘0 3 * * * /usr/local/bin/gmail-watch-renew.sh’) | crontab –✓ Daily cron at 3 AM UTCStep 10

## Verify the Full Pipeline

**Check each component individually before running the end-to-end test.**



# 1. Webhook endpoint reachable?$ curl -sI https://webhook.yourdomain.com/healthHTTP/2 200

# 2. Webhook server listening?$ ss -tlnp | grep 8788LISTEN 0 128 127.0.0.1:8788 0.0.0.0:*

# 3. Gmail watch active?$ gog gmail watch status –account user@yourdomain.comexpiration: 2026-04-05T14:21:02Z (6 days remaining)

# 4. Pub/Sub subscription active?$ gcloud pubsub subscriptions describe gmail-inbox-push  –project=my-email-notificationspushConfig: endpoint: https://webhook.yourdomain.com/gmail-pubsub**End-to-end test:** Send a test email to `user@yourdomain.com` from a different account. Watch your OpenClaw agent’s logs. You should see the notification arrive within approximately 30 seconds.

Best Practices

## Do’s and Don’ts

Do**Use `--services=all` when authenticating** to avoid re-doing OAuth later. **Set up watch renewal cron** — Gmail watch expires every 7 days. **Start Gmail watch BEFORE starting the webhook service.** Use a dedicated webhook subdomain. Use systemd for auto-restart. Keep your OAuth consent screen as “Internal.” Use `--force-consent` when re-authenticating. **Re-tighten the org policy after granting the Pub/Sub permission.**

Don’t- **Fix `iam.allowedPolicyMemberDomains` in the Workspace Admin Console** — it’s in the GCP Console
- **Use HTTP for the push endpoint** — Pub/Sub requires HTTPS
- **Forget to enable APIs in the GCP project** — OAuth scopes alone aren’t enough
- **Use `gcloud auth login` interactively on a headless VPS** — use `--no-browser`
- **Rely solely on [polling when Pub/Sub is available](/blog/openclaw-gmail-pubsub-vs-polling/)** — polling wastes API calls and adds latency
- **Hardcode credentials in application code** — use environment variables or keyring

Fallback

## When You Cannot Change the Org Policy

If corporate IT controls the org policy and won’t budge, **use [heartbeat polling](/blog/openclaw-gmail-pubsub-vs-polling/) as a fallback**. It works. It adds 0-5 minutes of latency. It uses more API quota. But it doesn’t require any GCP configuration beyond OAuth.



# Cron job: check inbox every 5 minutes$ */5 * * * * /usr/local/bin/gog gmail search “in:inbox” –max 10 –json  >> /var/log/inbox-check.log 2>&1*Not elegant. Not real-time. But it works every time, on every Google Workspace setup, without touching a single org policy.*

Bottom Line

## The Bottom Line

Gmail Pub/Sub is the correct architecture for real-time email processing on a VPS. The `iam.allowedPolicyMemberDomains` org policy blocker is the single obstacle that stops every Google Workspace setup, and it’s fixable in under 5 minutes once you know the fix is in the **GCP Console, not the Workspace Admin Console**. The confusion between the two consoles is what turns a 5-minute fix into a 3-hour debugging session.

The setup is not trivial — a Google Cloud project, a Pub/Sub topic, an IAM binding, a push subscription, nginx with SSL, a systemd service, and a [daily cron job](/blog/openclaw-cron-email-triage-calendar/) that must never fail. But every component serves a purpose, and once the pipeline is running, your OpenClaw agent processes email in 30 seconds instead of 5 minutes. For [security-conscious deployments](/blog/openclaw-security/), the reduced API surface of event-driven processing is a meaningful improvement over constant polling.

If you can change the org policy: **use Pub/Sub with heartbeat polling as a fallback**. If you can’t: **polling alone is perfectly fine for most email triage use cases**. Either way, your OpenClaw agent processes email. The architecture is a latency decision, not a capability decision.

FAQ

## Frequently Asked Questions

Why does the iam.allowedPolicyMemberDomains error happen?

Google Workspace organizations have a default policy called Domain Restricted Sharing that prevents adding IAM members from outside your organization. The `gmail-api-push@system.gserviceaccount.com` service account is owned by Google, not your org, so the policy blocks it — even though Gmail Pub/Sub requires this exact binding to function. The fix is overriding the policy at the project level in the **GCP Console** (not the Workspace Admin Console), granting the binding, then re-tightening the policy.

Can I fix this in the Google Workspace Admin Console?

No. The `iam.allowedPolicyMemberDomains` constraint is a [Google Cloud Organization Policy](https://cloud.google.com/resource-manager/docs/organization-policy/overview), managed exclusively through the GCP Console at `console.cloud.google.com`. The Workspace Admin Console at `admin.google.com` has a “Google Cloud session control” page that looks related but controls an entirely different setting. This is the #1 source of wasted debugging time.

Is it safe to set allowAll on the org policy?

Yes, temporarily. Set the override at the **project level** (not the organization level), grant the `gmail-api-push` service account its Publisher role, then immediately re-tighten the policy with `gcloud org-policies reset`. Existing IAM bindings are not retroactively removed — the policy only checks at binding creation time. The window of exposure is seconds, not permanent.

What if I can’t change the org policy?

Use [heartbeat polling as a fallback](/blog/openclaw-gmail-pubsub-vs-polling/). Set up a cron job that runs `gog gmail search` every 5 minutes. You lose real-time latency (0-5 minute delay instead of 30 seconds) and use more API quota, but it works on every Google Workspace setup without any GCP configuration beyond OAuth. Many [OpenClaw deployments](/how-it-works/) run successfully on polling alone.

Skip the Setup HeadachesManageMyClaw handles Gmail Pub/Sub, OAuth, and every VPS integration.[See Pricing](/pricing/)Not affiliated with or endorsed by the OpenClaw open-source project.


---

_View the original post at: [https://managemyclaw.com/blog/gmail-pubsub-org-policy-vps-guide/](https://managemyclaw.com/blog/gmail-pubsub-org-policy-vps-guide/)_  
_Served as markdown by [Third Audience](https://github.com/third-audience) v3.5.3_  
_Generated: 2026-03-29 16:00:55 UTC_  
