Skip to content

feat(bigtable): verify daemon identity before routing accelerated RPCs - #18606

Open
mutianf wants to merge 4 commits into
googleapis:bigtable-accelfrom
mutianf:accel4
Open

mutianf wants to merge 4 commits into
googleapis:bigtable-accelfrom
mutianf:accel4

Conversation

@mutianf

@mutianf mutianf commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> 🦕

@mutianf
mutianf requested a review from a team as a code owner October 8, 2026 16:29

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements identity and scope verification for the Bigtable accelerator daemon before routing RPCs, ensuring that the daemon and client share the same credentials and scopes. If a mismatch is detected, the client refuses to route RPCs, and if verification is inconclusive, it falls back to the native client. A review comment points out a critical issue where failures during server.read_identity() (such as reading from older daemons or JSON parsing errors) are not caught, which would crash client initialization instead of falling back gracefully. A suggestion was provided to catch these exceptions and raise _AcceleratorUnverified.

Comment thread packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/client.py Outdated
…tion

- Wrap OSError/ValueError from read_identity() as RuntimeError so callers
  always see a consistent type (not a raw json.JSONDecodeError on truncated
  identity.json)
- Add _AcceleratorIdentityError(RuntimeError) for confirmed identity/scope
  flips so _maybe_start_accelerator can emit a specific security-oriented
  warning instead of the generic "Failed to start daemon" message
- Fix _resolve_principal() docstring: it does make a link-local
  metadata-server call for Compute Engine credentials

Change-Id: I163a3fbb5342e339896b107c67755ee2838a2003
Wrap server.read_identity() in _verify_daemon_identity with try/except so
that any failure (older daemon without identity.json, corrupt/truncated file,
permission error) raises _AcceleratorUnverified and triggers the native-client
fallback instead of propagating as a raw exception through _start_accelerator.

Also fixes AcceleratorDaemon.__init__ to accept an optional binary_path
override (primarily for testing), removing pre-existing test failures where
_make_daemon was passing the kwarg to a constructor that didn't accept it.
Removes the stale git conflict marker from _sync_autogen/client.py.

Change-Id: Ib740df045487d07b3a0bda5c1fa3073301776246
self,
cli_flags: Sequence[str] = (),
*,
binary_path: str | None = None,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the binary path override.

…cation

- Remove binary_path constructor override from AcceleratorDaemon; fix
  _make_daemon test helper to patch _resolve_binary_path instead
- Merge _AcceleratorUnverified and _AcceleratorIdentityError into a single
  _AcceleratorIdentityError(RuntimeError) with a descriptive message at each
  raise site; simplify _start_accelerator to a single except BaseException
  handler and let _maybe_start_accelerator route all identity errors

Change-Id: Ieadd5ecd597fbccaf272896fb83533d632e89fd7
@parthea
parthea requested a review from sushanb October 8, 2026 18:19

@sushanb sushanb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few follow-ups from the earlier review — timeout, logging, cache init, and a couple of nits.

# Compute Engine credentials only populate the email after a refresh
# against the (link-local, non-egress) metadata server.
try:
creds.refresh(google_auth_requests.Request())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

google_auth_requests.Request() is constructed with no timeout, and compute_engine.Credentials.refresh doesn't impose one of its own on the metadata call. On a host where 169.254.169.254 is blackholed (hardened VM, broken route, DNS-shadowed metadata.google.internal) this blocks on connect for the OS default — minutes — and because _start_accelerator runs synchronously under _maybe_start_accelerator, client construction hangs for that long.

Please thread a short, bounded timeout here, ideally on the same order as the daemon's own _DEFAULT_STARTUP_TIMEOUT = 10.0:

from google.auth.transport import requests as google_auth_requests

_PRINCIPAL_RESOLVE_TIMEOUT = 2.0  # seconds; metadata server is link-local

...
try:
    creds.refresh(google_auth_requests.Request(timeout=_PRINCIPAL_RESOLVE_TIMEOUT))
except Exception as exc:
    _LOGGER.debug("Compute Engine credential refresh failed: %s", exc)

That way a non-GCE environment that happens to carry CE credentials degrades to principal = None → fall back to native, instead of a multi-minute BigtableDataClient(...) hang.

try:
creds.refresh(google_auth_requests.Request())
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent except Exception: pass here hides the reason for refresh failures. If the metadata call fails for a reason other than "no server" (403, token endpoint 5xx, DNS flake), the user ends up with principal = None and sees

Could not verify daemon identity: principal is unknown on this client side

with no breadcrumb back to the real cause. A single debug line costs nothing and makes support bugs tractable:

except Exception as exc:
    _LOGGER.debug("Compute Engine credential refresh failed: %s", exc)

(Can be folded into the same change as the timeout above.)

if explicit:
raise
warnings.warn(
f"Accelerator disabled: {exc}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth leaving a short comment here spelling out the fail-closed-on-ADC behavior change, since this is the point where it becomes visible to users:

# Any dev running under `gcloud auth application-default login` has no
# service_account_email, so _resolve_principal() returns None and we land
# here with "principal is unknown on this client side". Fail-closed to the
# native client is intentional — user ADC is almost never what the daemon
# resolved — but it does mean every user-ADC caller sees this warning once
# per client construction. Call it out in the release notes.
warnings.warn(
    f"Accelerator disabled: {exc}",
    RuntimeWarning,
    stacklevel=2,
)


Cached: resolution runs at most once per client.
"""
cached = getattr(self, "_cached_principal", _UNSET)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: _cached_principal is initialized lazily via getattr(..., _UNSET) on first call, which means vars(self) won't show the attribute until then and two concurrent coroutines that both reach a cold _resolve_principal will each run the (potentially network-touching) CE refresh before the second overwrites the first. Low impact — the refresh is idempotent — but a one-liner in _init_accelerator_config makes the invariant explicit and removes the race:

# In _init_accelerator_config, right after self._accelerator_scopes = ...:
self._cached_principal: str | None | object = _UNSET

Then _resolve_principal can drop the getattr and just read self._cached_principal.

pass
email = getattr(creds, "service_account_email", None)

principal: str | None = email or None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the three-line principal = email or None / if principal == "default": principal = None reads a bit indirectly — the second check only exists because the first collapsed the CE sentinel into the result. One expression says it:

principal: str | None = email if (email and email != "default") else None

Same behavior, one branch instead of two.

@parthea parthea assigned mutianf and unassigned sushanb Oct 9, 2026

This branch has not been deployed

No deployments
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