Skip to content

Revoke OpenVPN certs of deactivated Google Workspace users (development)#284

Merged
akuzminsky merged 1 commit into
mainfrom
openvpn-google-user-revocation
Jul 19, 2026
Merged

Revoke OpenVPN certs of deactivated Google Workspace users (development)#284
akuzminsky merged 1 commit into
mainfrom
openvpn-google-user-revocation

Conversation

@akuzminsky

Copy link
Copy Markdown
Member

What

A user suspended or deleted in Google Workspace can no longer mint a new certificate — the portal blocks their login — but any .ovpn they already downloaded keeps working until the CA revokes it. This closes that gap with a daily reconciliation: valid user CNs in the PKI index minus active directory users is the set to revoke.

Certificate CNs are Google email addresses, because the portal signs requests with EASYRSA_REQ_CN set to the authenticated user.

Development environment only, following the usual promotion path (development → sandbox → global).

Scope

The sync logic itself lives in infrahouse-toolkit as ih-openvpn sync-google-users (not yet released). This PR is the wrapper and its schedule. Until that toolkit release lands, the wrapper reports the missing subcommand as a misconfiguration.

Gating

Each external dependency is gated at the level it can actually be observed:

Dependency Knowable by Terraform? Gate
terraform-aws-openvpn yes compile time — new openvpn.google_directory_revocation fact
infrahouse-toolkit no — own release cycle runtime — probes for the subcommand, no version pinning
Workspace domain-wide delegation no — manual console step runtime — sync command's exit status

Hosts with the feature off get nothing at all: no script, no cron. The fact is absent on instances built by a module version predating the feature, which reads as false.

The third can never be a fact: authorizing the service account's client id is a paste into the Workspace admin console with no hashicorp/google resource behind it, so the feature can be enabled for days before it is done.

Unmet dependencies are loud

Because Puppet installs the wrapper only where the feature was explicitly enabled, its presence means someone asked for it. An unmet dependency is therefore a failure, not a skip — a silent skip would leave an opted-in host never revoking anything with nothing anywhere to say so.

Only genuinely self-resolving conditions stay quiet: another ASG instance holding the cluster-wide lock, and a PKI that has not been generated yet.

Safety

The OpenVPN server's own certificate must never become a candidate. On a fresh deployment CN=server is the only valid certificate, so a naive set-diff computes it as belonging to no active user and revokes it — killing the VPN before any user has enrolled. Two independent guards in the toolkit: CNs must contain @, and an empty directory response is refused rather than treated as "everyone was deactivated".

The lock lives on the EFS-backed config directory rather than /var/run, so it is cluster-wide — every instance mounts the same PKI and concurrent easyrsa revoke runs would race on index.txt. The cron minute is derived from the hostname so the ASG does not query the Directory API in lockstep.

Testing

Verified end to end on a live development instance: the wrapper as rendered by Puppet, driving the real toolkit, authenticated keylessly through AWS → GCP → domain-wide delegation and read the real directory.

openvpn-google-sync: sync completed
openvpn-google-sync: Checked 0 certificate(s) against 3 active user(s); nothing to revoke.

Every dependency state was exercised against a sandboxed copy on a real Ubuntu host — missing credentials, stale toolkit, unauthorized delegation, real failure, success, uninitialized PKI, lock contention — confirming which exit non-zero and which stay quiet. puppet-lint --fail-on-warnings clean on all four module paths.

Not covered

  • The revocation path has been reached but never executed — every run so far had zero candidates. Exercising it needs a certificate whose CN is an email absent from the directory.
  • revoke_client uses EASYRSA_PASSIN=file:, while regenerate-crl.sh.erb documents that file: fails under easy-rsa batch mode with OpenSSL 3.x and uses pass: instead. Unresolved — raw OpenSSL handles both, so if the bug is real it lives in easy-rsa's batch handling. Worth settling before relying on this path.

Depends on

  • infrahouse-toolkit: ih-openvpn sync-google-users (unreleased)
  • terraform-aws-openvpn: emits the openvpn.google_directory_revocation fact (unreleased)

🤖 Generated with Claude Code

A user suspended or deleted in Google Workspace can no longer mint a new
certificate -- the portal blocks their login -- but any .ovpn they already
downloaded keeps working until the CA revokes it. This closes that gap with a
daily reconciliation: valid user CNs in the PKI index minus active directory
users is the set to revoke. Certificate CNs are Google email addresses because
the portal signs requests with EASYRSA_REQ_CN set to the authenticated user.

Development environment only, following the usual promotion path to sandbox and
then global.

The sync logic itself lives in infrahouse-toolkit as `ih-openvpn
sync-google-users` (not yet released); this change is the wrapper and its
schedule. Each of the three external dependencies is gated at the level it can
actually be observed:

  1. terraform-aws-openvpn -- COMPILE time, on the new
     openvpn.google_directory_revocation fact. Hosts with the feature off get
     nothing at all: no script, no cron. The fact is absent on instances built
     by a module version predating the feature, which reads as false.
  2. infrahouse-toolkit -- RUNTIME, by probing for the subcommand rather than
     pinning a version, since the toolkit ships on its own release cycle.
  3. Google Workspace domain-wide delegation -- RUNTIME, via the sync command's
     exit status. This one can never be a fact: authorizing the service
     account's client id is a manual step in the admin console with no
     Terraform resource behind it, so the feature can be enabled for days
     before it is done.

Because Puppet installs the wrapper only where the feature was explicitly
enabled, an unmet dependency is reported as a failure rather than skipped
quietly. A silent skip would leave a host that was opted in never revoking
anything with nothing anywhere to say so. Only genuinely self-resolving
conditions stay quiet: another ASG instance holding the cluster-wide lock, and
a PKI that has not been generated yet.

Safety: the OpenVPN server's own certificate must never become a candidate. On
a fresh deployment CN=server is the only valid certificate, so a naive set-diff
would compute it as belonging to no active user and revoke it, killing the VPN
before any user has enrolled. The toolkit filters to CNs containing @ and
refuses to act on an empty directory response.

The lock lives on the EFS-backed config directory rather than /var/run so it is
cluster-wide -- every instance mounts the same PKI and concurrent easyrsa
revoke runs would race on index.txt. The cron minute is derived from the
hostname so the ASG does not query the Directory API in lockstep.

Verified end to end on a live development instance: the wrapper rendered by
Puppet, driving the real toolkit, authenticated keylessly through AWS -> GCP ->
domain-wide delegation and read the real directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@akuzminsky
akuzminsky merged commit d967e4f into main Jul 19, 2026
2 checks passed
@akuzminsky
akuzminsky deleted the openvpn-google-user-revocation branch July 19, 2026 23:22
akuzminsky added a commit that referenced this pull request Jul 21, 2026
Final promotion of the OpenVPN certificate-revocation feature: development ->
sandbox -> global, completing the development -> sandbox -> global path (cf.
#284, #285). Copies the two files verbatim from environments/development into the
top-level modules/ (which serves every environment, i.e. production) and adds the
same fact-gated include to the global openvpn_server.pp.

Inert until activated: the class is declared only where the
openvpn.google_directory_revocation fact is true, which terraform-aws-openvpn
(>= 7.0.0) sets only when enable_google_directory_revocation = true. On every
existing node the fact is absent, so this installs no script and no cron and
changes nothing until the feature is deliberately switched on per consumer.
Validated on a live sandbox OpenVPN node running the promoted code: catalog
compiled and no sync cron was installed while the fact was absent. Runtime
dependencies Terraform cannot see (toolkit >= 2.61.0 for `ih-openvpn
sync-google-users`, Google Workspace domain-wide delegation) are gated inside
the wrapper.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants