Skip to content

Google Workspace

Cloud-to-cloud backup and restore of Google Workspace accounts. Pure Rust — one FD plugin (gworkspace-fd.so) plus one backend binary, no JVM, no Python, no agents inside Google.

WorkloadBackupIncrementalRestore
Gmail.eml full-fidelity (RFC822, labels preserved)history APIre-insert with labels recreated
Google Drivebinaries native + Docs/Sheets/Slides exported (docx/xlsx/pptx, configurable)changes APIupload under NGBackup Restore <date>/
Google CalendarJSON per eventfull walkevents.import
Google ContactsJSON per contactfull walkpeople:createContact

All workloads support restore to origin and cross-user restore (restore_user=). Filesystem download of virtual files (where=/path) is currently limited (see Restore below). Shared Drives, Gmail settings and deletion tombstones are on the roadmap.

  • Service account + Domain-Wide Delegation (OAuth 2.0, RS256 JWT) — one key protects the whole domain; per-user impersonation, no user passwords.
  • New users included automatically: domain jobs enumerate active users via the Admin SDK Directory API on every run.
  • Takeout-compatible Gmail format: each message is a standalone .eml with an X-Gmail-Labels: header — the self-contained format opens in any mail client after a restore-to-origin export.
  • Quota-aware: truncated exponential backoff on 429 / 403 rate-limit / 5xx honoring Retry-After (Gmail quota model: 6,000 units/min/user).
  • Secrets hygiene: inline keys are rejected at parse time; the service-account key lives in a 0600 file (sa_key_file=).
  1. Google Cloud Console: create a project (or reuse the org’s backup project), enable the Gmail API, Google Drive API, Google Calendar API, People API and Admin SDK API, create a service account and download its JSON key.
  2. Copy the service account’s numeric OAuth2 Client ID (on the SA details page).
  3. Workspace Admin console (admin.google.com): Security → API Controls → Domain-wide Delegation → add the numeric client ID with exactly these scopes (propagation can take up to 24 h, usually minutes):
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/contacts.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/contacts

Install the ngbackup-gworkspace-plugin package (deb/rpm) on the client host that will run the jobs, then place the key and configuration:

Terminal window
install -o bacula -g bacula -m 0600 sa-key.json /opt/ngbackup/etc/gworkspace/sa-key.json
cp gworkspace.conf.example /opt/ngbackup/etc/gworkspace/gworkspace.conf # edit domain/admin_user

Pre-flight check (exit code 0 = all green):

Terminal window
sudo -u bacula gworkspace-backend mode=verify \
config_file=/opt/ngbackup/etc/gworkspace/gworkspace.conf user=someone@yourdomain

key = value format; keys on the Plugin = line override the file.

KeyMeaning
domainWorkspace primary domain (virtual paths and Directory API queries)
sa_key_fileService-account JSON key with DWD enabled — mode 0600
admin_userAdmin impersonated for Directory API user enumeration (domain-wide jobs and list pluginobjects); not needed when jobs pass explicit user=
working_dirCheckpoint directory (Gmail historyId / Drive pageToken), default /opt/ngbackup/working
log_levelOptional; debug for verbose runs
page_sizeOptional; API page size (e.g. 500)
export_docsDocs export format: docx | odt | pdf | txt | html | md
export_sheetsSheets export format: xlsx | ods | pdf | csv | tsv
export_slidesSlides export format: pptx | odp | pdf | txt
max_file_mbPer-file Drive cap in MiB, 0 = unlimited
include_trashBack up trashed items (no by default)

Prefix gworkspace: routes to the plugin; everything after it is space-separated key=value pairs. mode= values: account (all four services) | gmail | drive | calendar | contacts | verify. Domain jobs (mode=account without user=) re-enumerate active users on every run — new employees are protected automatically.

Enumerate domain users from the console with ready-to-paste Plugin = lines:

list pluginobjects client=<fd> plugin="gworkspace: config_file=/opt/ngbackup/etc/gworkspace/gworkspace.conf"
# Whole-domain, all services (Gmail + Drive + Calendar + Contacts).
FileSet {
Name = "FS-GWorkspace-Domain"
Include {
Options { Signature = MD5; Compression = GZIP }
Plugin = "gworkspace: mode=account \
config_file=/opt/ngbackup/etc/gworkspace/gworkspace.conf"
}
}
# Gmail only, two explicit users, excluding a label.
FileSet {
Name = "FS-GWorkspace-Gmail-Pilot"
Include {
Options { Signature = MD5; Compression = GZIP }
Plugin = "gworkspace: mode=gmail \
config_file=/opt/ngbackup/etc/gworkspace/gworkspace.conf \
labels_exclude=Newsletters"
}
}
# Drive only, PDFs kept native, Google Docs exported as ODT.
FileSet {
Name = "FS-GWorkspace-Drive"
Include {
Options { Signature = MD5; Compression = GZIP }
Plugin = "gworkspace: mode=drive \
config_file=/opt/ngbackup/etc/gworkspace/gworkspace.conf \
export_docs=odt"
}
}
# 2x/day: Full weekly + Incremental at 07:00 and 19:00.
# Gmail incrementals use history.list, Drive uses changes.list.
Schedule {
Name = "Sched-GWorkspace-2xDay"
Run = Level=Full sun at 01:00
Run = Level=Incremental mon-sat at 07:00
Run = Level=Incremental mon-sat at 19:00
}
Job {
Name = "BackupGoogleWorkspace"
Type = Backup
Level = Incremental
Client = ngbackup-fd # FD where gworkspace-fd.so is installed
FileSet = "FS-GWorkspace-Domain"
Schedule = "Sched-GWorkspace-2xDay"
Storage = File1
Pool = Default
Messages = Standard
Accurate = no
}

restore in bconsole → mark items → where=/. Gmail messages are re-inserted with their labels (missing custom labels are recreated); Drive files land under NGBackup Restore <date>/ (never overwrites live files); Calendar events are re-imported; Contacts recreated.

To another user (departed employee, migration)

Section titled “To another user (departed employee, migration)”

Add restore_user=target@domain to the plugin options string at restore time. All selected objects land in the target account (Drive under the restore folder, Gmail with labels, Calendar into primary).

Download to filesystem (where=/path) — limitation

Section titled “Download to filesystem (where=/path) — limitation”

Restoring the plugin’s virtual files to an alternate filesystem location currently produces 0-byte files: the File Daemon routes alternate-location restores to its core extractor, which does not invoke the plugin and cannot materialise a plugin data stream as plain file content. This is a shared limitation across NGBackup’s virtual-file plugins, not specific to Google Workspace. To obtain files on disk, restore to origin (into the original or a scratch account) and export from Google. Routing where= restores through the plugin is on the roadmap.

See also: Restore, Instant Recovery & V2V.

Logs: /opt/ngbackup/working/gworkspace-backend.log. Checkpoints: /opt/ngbackup/working/gworkspace/<domain>/state.json — deleting the file forces a full resync on the next Incremental (safe; never deletes backed-up data).

SymptomLikely causeAction
token exchange HTTP 401 invalid_grantDWD not authorized / wrong client ID / clock skewRe-check the delegation step; timedatectl NTP on
token exchange HTTP 400 + unauthorized_clientScope list in Admin console ≠ scopes requestedPaste the exact 9-scope list
Directory users.list HTTP 403admin_user= is not a Workspace adminUse a super-admin (or delegated admin with Users→Read)
Gmail 403 rateLimitExceeded bursts>6,000 quota units/min/userThe plugin backs off automatically; for 1,500+ accounts split jobs by user ranges
history.list 404 on every IncrementalhistoryId expired (>1 week gap)Expected — the run auto-falls back to a full walk and re-checkpoints
Drive file skipped exceeds the 10 MB export capGoogle-native file too large for files.exportExport it manually or accept the skip (fix on the roadmap)
Job fails backend completed without producing any virtual filesBad credentials / empty selectionRun mode=verify; check FileSet selectors
inline secret ... is forbidden (ADR-008)sa_key=/password= in Plugin= lineUse sa_key_file= (0600)