API Gateway Backend Security and Rate Limiting This skill configures client authentication, gateway-to-backend authentication, and per-project rate limits (quotas) for Google Cloud API Gateway across OpenAPI 2.0 ( ) and OpenAPI 3.0.x ( ), including gateways with Model Context Protocol (MCP) enabled (Public Preview). Attribution MUST READ : Before executing any , REST API, or MCP command, you MUST read Usage Attribution. For commands executed directly in the terminal (never in generated scripts, Terraform, or user code), prefix with and append to headers. Prerequisites 1. Install the Google Cl…

\n)\n\n# The maximum time, in seconds, to wait for each gcloud command.\n_GCLOUD_TIMEOUT_SECONDS = 60\n\n\ndef _load_spec(content: str, source_name: str) -> dict[str, Any]:\n \"\"\"Parses a YAML or JSON OpenAPI specification string into a dictionary.\"\"\"\n try:\n data = json.loads(content)\n if isinstance(data, dict):\n return data\n except json.JSONDecodeError:\n pass\n\n if not _HAS_YAML:\n raise RuntimeError(\n 'PyYAML is required to parse YAML specifications. Install it with: '\n 'pip install \"PyYAML>=6.0,\u003c7.0\"'\n )\n try:\n data = yaml.safe_load(content)\n except yaml.YAMLError as exc:\n raise ValueError(\n f'Specification {source_name} is not valid JSON or YAML: {exc}'\n ) from exc\n if not isinstance(data, dict):\n raise ValueError(f'Specification {source_name} did not parse as a mapping.')\n return data\n\n\ndef _read_spec(path: str) -> dict[str, Any]:\n \"\"\"Reads and parses the OpenAPI specification file at path.\"\"\"\n with open(path, 'r', encoding='utf-8') as f:\n return _load_spec(f.read(), path)\n\n\ndef extract_quota_metrics(spec: dict[str, Any]) -> set[str]:\n \"\"\"Extracts declared quota metric names from an OpenAPI 2.0 or 3.x dict.\"\"\"\n metrics: set[str] = set()\n\n # OpenAPI 2.0: x-google-management.metrics is a list of {name: ...} objects.\n oas2_management = spec.get('x-google-management')\n if isinstance(oas2_management, dict):\n oas2_metrics = oas2_management.get('metrics', [])\n if isinstance(oas2_metrics, list):\n for item in oas2_metrics:\n if isinstance(item, dict) and item.get('name') is not None:\n metrics.add(str(item['name']))\n\n # OpenAPI 3.x: x-google-api-management.metrics is a map keyed by metric name.\n oas3_management = spec.get('x-google-api-management')\n if isinstance(oas3_management, dict):\n oas3_metrics = oas3_management.get('metrics', {})\n if isinstance(oas3_metrics, dict):\n for key in oas3_metrics.keys():\n if key is not None:\n metrics.add(str(key))\n\n return metrics\n\n\ndef extract_limited_metrics(spec: dict[str, Any]) -> set[str]:\n \"\"\"Extracts metric names that have a limit in quota.limits.\"\"\"\n limited: set[str] = set()\n\n oas2_management = spec.get('x-google-management')\n if isinstance(oas2_management, dict):\n quota = oas2_management.get('quota', {})\n if isinstance(quota, dict):\n limits = quota.get('limits', [])\n if isinstance(limits, list):\n for item in limits:\n if isinstance(item, dict) and item.get('metric') is not None:\n limited.add(str(item['metric']))\n\n oas3_management = spec.get('x-google-api-management')\n if isinstance(oas3_management, dict):\n quota = oas3_management.get('quota', {})\n if isinstance(quota, dict):\n limits = quota.get('limits', {})\n if isinstance(limits, dict):\n for item in limits.values():\n if isinstance(item, dict) and item.get('metric') is not None:\n limited.add(str(item['metric']))\n\n return limited\n\n\ndef _quota_extension_metrics(quota_extension: Any) -> set[str]:\n \"\"\"Returns the metric names that one x-google-quota extension charges.\"\"\"\n if not isinstance(quota_extension, dict):\n return set()\n costs = quota_extension.get('metricCosts')\n if isinstance(costs, dict):\n return {str(metric_name) for metric_name in costs}\n return {str(metric_name) for metric_name in quota_extension}\n\n\ndef extract_referenced_quota_metrics(spec: dict[str, Any]) -> set[str]:\n \"\"\"Extracts metric names that x-google-quota references at any level.\"\"\"\n referenced = _quota_extension_metrics(spec.get('x-google-quota'))\n paths = spec.get('paths', {})\n if isinstance(paths, dict):\n for path_item in paths.values():\n if isinstance(path_item, dict):\n referenced |= _quota_extension_metrics(path_item.get('x-google-quota'))\n for operation in path_item.values():\n if isinstance(operation, dict):\n referenced |= _quota_extension_metrics(\n operation.get('x-google-quota')\n )\n\n return referenced\n\n\ndef find_ignored_quota_extensions(spec: dict[str, Any]) -> list[str]:\n \"\"\"Returns the locations of x-google-quota extensions outside an operation.\n\n API Gateway reads x-google-quota only on operations. It ignores the extension\n at the root and path levels without an error, so the metrics that it charges\n are never counted.\n\n Args:\n spec: The parsed OpenAPI 2.0 or 3.x specification.\n\n Returns:\n The location of each ignored extension, such as \"x-google-quota\" or\n \"paths./v1/orders.x-google-quota\".\n \"\"\"\n locations: list[str] = []\n if 'x-google-quota' in spec:\n locations.append('x-google-quota')\n paths = spec.get('paths', {})\n if isinstance(paths, dict):\n for path, path_item in paths.items():\n if isinstance(path_item, dict) and 'x-google-quota' in path_item:\n locations.append(f'paths.{path}.x-google-quota')\n return locations\n\n\ndef _decode_contents(b64_contents: str) -> str:\n \"\"\"Decodes base64 or base64url API config contents.\"\"\"\n normalized = b64_contents.replace('-', '+').replace('_', '/')\n normalized += '=' * (-len(normalized) % 4)\n return base64.b64decode(normalized).decode('utf-8-sig')\n\n\ndef _fetch_live_config_metrics(\n project: str, api: str\n) -> tuple[dict[str, list[str]], dict[str, list[str]], list[str]]:\n \"\"\"Reads the quota metrics of each active API config of an API.\n\n Args:\n project: The Google Cloud project ID of the gateways.\n api: The API Gateway API ID.\n\n Returns:\n The metrics of each active API config, the limited metrics of each active\n API config, and the IDs of the active configs that have no OpenAPI\n documents (for example, gRPC configs), which the script cannot inspect.\n\n Raises:\n RuntimeError: If a gcloud command fails, or if no active API config has\n OpenAPI documents.\n \"\"\"\n gateways_command = [\n 'gcloud',\n 'api-gateway',\n 'gateways',\n 'list',\n f'--project={project}',\n '--format=json',\n ]\n gateways_result = subprocess.run(\n gateways_command,\n capture_output=True,\n text=True,\n check=False,\n stdin=subprocess.DEVNULL,\n timeout=_GCLOUD_TIMEOUT_SECONDS,\n )\n if gateways_result.returncode != 0:\n raise RuntimeError(\n f'Failed to list gateways via gcloud: {gateways_result.stderr.strip()}'\n )\n\n gateways = json.loads(gateways_result.stdout or '[]')\n config_ids: set[str] = set()\n for gateway in gateways:\n match = _API_CONFIG_NAME_RE.search(gateway.get('apiConfig', ''))\n if match and match.group('api') == api:\n config_ids.add(match.group('config'))\n\n metrics_by_source: dict[str, list[str]] = {}\n limited_by_source: dict[str, list[str]] = {}\n skipped_configs: list[str] = []\n for config_id in sorted(config_ids):\n config_command = [\n 'gcloud',\n 'api-gateway',\n 'api-configs',\n 'describe',\n config_id,\n f'--api={api}',\n f'--project={project}',\n '--view=FULL',\n '--format=json',\n ]\n config_result = subprocess.run(\n config_command,\n capture_output=True,\n text=True,\n check=False,\n stdin=subprocess.DEVNULL,\n timeout=_GCLOUD_TIMEOUT_SECONDS,\n )\n if config_result.returncode != 0:\n raise RuntimeError(\n f'Failed to describe API config {config_id}:'\n f' {config_result.stderr.strip()}'\n )\n config_data = json.loads(config_result.stdout or '{}')\n openapi_documents = config_data.get('openapiDocuments', [])\n if not openapi_documents:\n # One gRPC config must not block the check of the other configs.\n skipped_configs.append(config_id)\n continue\n config_metrics: set[str] = set()\n config_limited: set[str] = set()\n for document_entry in openapi_documents:\n document = document_entry.get('document', {})\n b64_contents = document.get('contents', '')\n if b64_contents:\n raw_text = _decode_contents(b64_contents)\n parsed = _load_spec(\n raw_text, f'{config_id}:{document.get(\"path\", \"spec\")}'\n )\n config_metrics.update(extract_quota_metrics(parsed))\n config_metrics.update(extract_referenced_quota_metrics(parsed))\n config_limited.update(extract_limited_metrics(parsed))\n metrics_by_source[f'api-config:{config_id}'] = sorted(config_metrics)\n limited_by_source[f'api-config:{config_id}'] = sorted(config_limited)\n\n if skipped_configs and not metrics_by_source:\n raise RuntimeError(\n f'No active API config of API {api} has OpenAPI documents (for'\n f' example, gRPC configs: {\", \".join(skipped_configs)}), so the script'\n ' cannot inspect them. Check their quota metrics and limits manually'\n ' before rollout.'\n )\n return metrics_by_source, limited_by_source, skipped_configs\n\n\ndef main(argv: list[str] | None = None) -> int:\n parser = argparse.ArgumentParser(\n description=(\n 'Verify that a candidate OpenAPI specification retains all quota '\n 'metrics and limits required by baseline specs or active gateways.'\n )\n )\n parser.add_argument(\n '--spec',\n required=True,\n help='Path to the candidate OpenAPI 2.0 or 3.x specification file.',\n )\n parser.add_argument(\n '--baseline-spec',\n action='append',\n default=[],\n help=(\n 'Path to an existing OpenAPI spec currently served by a gateway. '\n 'Can be passed multiple times for offline comparison.'\n ),\n )\n parser.add_argument(\n '--project',\n help='Google Cloud project ID to inspect active gateways via gcloud.',\n )\n parser.add_argument(\n '--api',\n help='API Gateway API ID to inspect active gateways via gcloud.',\n )\n parser.add_argument(\n '--dry-run',\n action='store_true',\n help='Validate local --baseline-spec files only without calling gcloud.',\n )\n\n args = parser.parse_args(argv)\n\n if args.dry_run and (args.project or args.api):\n print(\n 'Error: Do not combine --dry-run with --project or --api. Use '\n '--project and --api for live gateway checks, or --baseline-spec with '\n '--dry-run for offline file comparisons.',\n file=sys.stderr,\n )\n return 2\n\n if (args.project or args.api) and not (args.project and args.api):\n print(\n 'Error: Both --project and --api must be provided together when '\n 'querying live gateways.',\n file=sys.stderr,\n )\n return 2\n\n if not args.baseline_spec and not (args.project and args.api):\n print(\n 'Error: Provide at least one baseline source via --project and --api '\n '(live gateway check) or --baseline-spec (offline spec check).',\n file=sys.stderr,\n )\n return 2\n\n try:\n candidate_spec = _read_spec(args.spec)\n baseline_specs = {path: _read_spec(path) for path in args.baseline_spec}\n live_sources, live_limited, skipped_configs = (\n _fetch_live_config_metrics(args.project, args.api)\n if args.project and args.api\n else ({}, {}, [])\n )\n except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:\n print(f'Error: {exc}', file=sys.stderr)\n return 2\n\n for config_id in skipped_configs:\n print(\n f'Warning: Skipped active API config {config_id} because it has no'\n ' OpenAPI documents (for example, a gRPC config). Check its quota'\n ' metrics and limits manually.',\n file=sys.stderr,\n )\n\n candidate_metrics = extract_quota_metrics(candidate_spec)\n candidate_limited = extract_limited_metrics(candidate_spec)\n candidate_references = extract_referenced_quota_metrics(candidate_spec)\n ignored_quota_extensions = find_ignored_quota_extensions(candidate_spec)\n\n baseline_sources: dict[str, list[str]] = {}\n baseline_limited: dict[str, list[str]] = {}\n for base_path, base_spec in baseline_specs.items():\n base_names = extract_quota_metrics(\n base_spec\n ) | extract_referenced_quota_metrics(base_spec)\n baseline_sources[base_path] = sorted(base_names)\n baseline_limited[base_path] = sorted(extract_limited_metrics(base_spec))\n baseline_sources.update(live_sources)\n baseline_limited.update(live_limited)\n\n missing_by_source: dict[str, list[str]] = {}\n all_missing: set[str] = set()\n missing_limits: set[str] = set()\n undeclared_candidate_references = sorted(\n candidate_references - candidate_metrics\n )\n\n for source, old_metrics in baseline_sources.items():\n missing = sorted(set(old_metrics) - candidate_metrics)\n if missing:\n missing_by_source[source] = missing\n all_missing.update(missing)\n # Flag only limits that a baseline has and the candidate drops. A metric\n # that never had a limit does not need one now.\n for old_limited in baseline_limited.values():\n missing_limits.update(set(old_limited) - candidate_limited)\n\n has_failure = bool(\n all_missing\n or missing_limits\n or undeclared_candidate_references\n or ignored_quota_extensions\n )\n status = 'FAIL' if has_failure else 'PASS'\n report = {\n 'status': status,\n 'candidate_spec': args.spec,\n 'candidate_metrics': sorted(candidate_metrics),\n 'candidate_limited_metrics': sorted(candidate_limited),\n 'undeclared_candidate_references': undeclared_candidate_references,\n 'ignored_quota_extensions': ignored_quota_extensions,\n 'skipped_configs': skipped_configs,\n 'baseline_sources': baseline_sources,\n 'baseline_limited_metrics': baseline_limited,\n 'missing_metrics': sorted(all_missing),\n 'missing_limits_for_metrics': sorted(missing_limits),\n 'missing_by_source': missing_by_source,\n }\n print(json.dumps(report, indent=2))\n\n if all_missing or undeclared_candidate_references:\n print(\n 'Quota metric validation failed. Creating this API config will '\n 'overwrite API-wide quota definitions and can cause HTTP 500 \"Failed '\n 'to call Service Control Quota\" on active gateways.',\n file=sys.stderr,\n )\n if missing_limits:\n print(\n 'Quota limit validation failed. Creating this API config removes the '\n 'limits for the metrics in \"missing_limits_for_metrics\" from every '\n 'gateway of the API, so those metrics are no longer rate limited.',\n file=sys.stderr,\n )\n if ignored_quota_extensions:\n print(\n 'Quota extension validation failed. API Gateway reads x-google-quota '\n 'only on operations and ignores the extensions in '\n '\"ignored_quota_extensions\", so the metrics that they charge are never '\n 'counted. Move each x-google-quota into the operations it applies to.',\n file=sys.stderr,\n )\n return 1 if has_failure else 0\n\n\nif __name__ == '__main__':\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16093,"content_sha256":"7cf94143bdc661e85a686dc7ba6276829b671aecdf83783127aec87e660d4073"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"API Gateway Backend Security and Rate Limiting","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill configures client authentication, gateway-to-backend authentication, and per-project rate limits (quotas) for Google Cloud API Gateway across OpenAPI 2.0 (","type":"text"},{"text":"swagger: \"2.0\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") and OpenAPI 3.0.x (","type":"text"},{"text":"openapi: 3.0.4","type":"text","marks":[{"type":"code_inline"}]},{"text":"), including gateways with Model Context Protocol (MCP) enabled (Public Preview).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Attribution","type":"text"}]},{"type":"paragraph","content":[{"text":"MUST READ","type":"text","marks":[{"type":"strong"}]},{"text":": Before executing any ","type":"text"},{"text":"gcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":", REST API, or MCP command, you MUST read ","type":"text"},{"text":"Usage Attribution","type":"text","marks":[{"type":"link","attrs":{"href":"references/attribution.md","title":null}}]},{"text":". For commands executed directly in the terminal (never in generated scripts, Terraform, or user code), prefix ","type":"text"},{"text":"gcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"CLOUDSDK_METRICS_REQUEST_ATTRIBUTION=\"agent-skills/1.0 (skill:api-gateway-backend-security-and-rate-limiting)\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and append ","type":"text"},{"text":"agent-skills/1.0 (skill:api-gateway-backend-security-and-rate-limiting)","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"User-Agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" headers.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install the Google Cloud CLI and Python 3.10+ YAML parser:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install or update the ","type":"text"},{"text":"Google Cloud SDK","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.cloud.google.com/sdk/docs/install-sdk.md.txt","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install ","type":"text"},{"text":"PyYAML","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the quota metric validation script:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install \"PyYAML>=6.0,\u003c7.0\"","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Authenticate your environment:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local CLI session: ","type":"text"},{"text":"gcloud auth login","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Application Default Credentials (SDKs): ","type":"text"},{"text":"gcloud auth application-default login","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Service account key file (if required by CI): set ","type":"text"},{"text":"GOOGLE_APPLICATION_CREDENTIALS={key_file_path}","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Select or create a project, verify billing, and pick a supported gateway location:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Select an existing project (","type":"text"},{"text":"gcloud config set project {project_id}","type":"text","marks":[{"type":"code_inline"}]},{"text":") or create one (","type":"text"},{"text":"gcloud projects create {project_id}","type":"text","marks":[{"type":"code_inline"}]},{"text":" as an escape hatch).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify that an active billing account is linked to ","type":"text"},{"text":"{project_id}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (required even when staying within the ","type":"text"},{"text":"API Gateway pricing free tier","type":"text","marks":[{"type":"link","attrs":{"href":"https://cloud.google.com/api-gateway/pricing","title":null}}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"us-central1","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the default ","type":"text"},{"text":"--location","type":"text","marks":[{"type":"code_inline"}]},{"text":" for API Gateway commands. For the other supported regions, see ","type":"text"},{"text":"API Gateway deployment model","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.cloud.google.com/api-gateway/docs/deployment-model.md.txt","title":null}}]},{"text":".","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable required Google Cloud APIs:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gcloud services enable \\\n apigateway.googleapis.com \\\n servicemanagement.googleapis.com \\\n servicecontrol.googleapis.com \\\n iam.googleapis.com \\\n apikeys.googleapis.com \\\n --project={project_id}","type":"text"}]},{"type":"paragraph","content":[{"text":"API Gateway does not need the Cloud Run API. If the backend runs on Cloud Run, that API is already enabled in the backend's project.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify required IAM roles:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"To manage API Gateway resources and enable services: ","type":"text"},{"text":"roles/apigateway.admin","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"roles/serviceusage.serviceUsageAdmin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"To create the gateway service account and attach it to an API config: ","type":"text"},{"text":"roles/iam.serviceAccountAdmin","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"roles/iam.serviceAccountUser","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"To manage API keys in consumer projects: ","type":"text"},{"text":"roles/serviceusage.apiKeysAdmin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For the gateway service account to invoke the backend: ","type":"text"},{"text":"roles/run.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Cloud Run or 2nd gen functions) or ","type":"text"},{"text":"roles/cloudfunctions.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":" (1st gen functions)","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Decision Matrix: Client Authentication, Backend Security, and Quotas","type":"text"}]},{"type":"paragraph","content":[{"text":"Select the layer that matches the security requirement:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Goal","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mechanism to Configure","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key Constraint","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Detailed Reference","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Identify the calling consumer project for quotas and billing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API key (","type":"text"},{"text":"x-api-key","type":"text","marks":[{"type":"code_inline"}]},{"text":" header convention; ","type":"text"},{"text":"key","type":"text","marks":[{"type":"code_inline"}]},{"text":" query parameter escape hatch)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NEVER rely on an API key alone to authenticate end-user identity. ","type":"text"},{"text":"Rationale:","type":"text","marks":[{"type":"strong"}]},{"text":" API keys identify a Google Cloud project rather than a user, and clients can extract them.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Client Authentication","type":"text","marks":[{"type":"link","attrs":{"href":"references/client-authentication.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Authenticate end users or calling services","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JWT (","type":"text"},{"text":"oauth2","type":"text","marks":[{"type":"code_inline"}]},{"text":" scheme with ","type":"text"},{"text":"x-google-issuer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"x-google-jwks_uri","type":"text","marks":[{"type":"code_inline"}]},{"text":" in OAS 2.0, or ","type":"text"},{"text":"x-google-auth","type":"text","marks":[{"type":"code_inline"}]},{"text":" in OAS 3.x)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Operation-level ","type":"text"},{"text":"security","type":"text","marks":[{"type":"code_inline"}]},{"text":" overrides root ","type":"text"},{"text":"security","type":"text","marks":[{"type":"code_inline"}]},{"text":". Include ","type":"text"},{"text":"api_key: []","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the JWT scheme in the same security requirement object when an endpoint needs both user identity and quotas.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Client Authentication","type":"text","marks":[{"type":"link","attrs":{"href":"references/client-authentication.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Block direct calls to the backend service","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dedicated gateway service account (","type":"text"},{"text":"--backend-auth-service-account","type":"text","marks":[{"type":"code_inline"}]},{"text":") + ","type":"text"},{"text":"roles/run.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":" + backend ","type":"text"},{"text":"--no-allow-unauthenticated","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Roll out the gateway config with ","type":"text"},{"text":"--backend-auth-service-account","type":"text","marks":[{"type":"code_inline"}]},{"text":" BEFORE setting ","type":"text"},{"text":"--no-allow-unauthenticated","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a live backend to avoid ","type":"text"},{"text":"403","type":"text","marks":[{"type":"code_inline"}]},{"text":" downtime.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Backend Security","type":"text","marks":[{"type":"link","attrs":{"href":"references/backend-security.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limit requests per consumer project (","type":"text"},{"text":"1/min/{project}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"x-google-management","type":"text","marks":[{"type":"code_inline"}]},{"text":" (OAS 2.0) or ","type":"text"},{"text":"x-google-api-management","type":"text","marks":[{"type":"code_inline"}]},{"text":" (OAS 3.x) + ","type":"text"},{"text":"x-google-quota","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"NEVER rename or remove a quota metric when creating a new API config while any gateway of the API runs an older config referencing that metric. ","type":"text"},{"text":"Rationale:","type":"text","marks":[{"type":"strong"}]},{"text":" Creating the config immediately overwrites API-wide quotas and causes ","type":"text"},{"text":"HTTP 500 Failed to call Service Control Quota","type":"text","marks":[{"type":"code_inline"}]},{"text":" on older gateways.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quotas and Rate Limiting","type":"text","marks":[{"type":"link","attrs":{"href":"references/quotas-and-rate-limiting.md","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Secure MCP ","type":"text"},{"text":"tools/list","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"tools/call","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"/mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Public Preview)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"x-google-api-management.mcp.tools-list.security","type":"text","marks":[{"type":"code_inline"}]},{"text":" (exactly one JWT or API key scheme) + operation ","type":"text"},{"text":"security","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"x-google-mcp-tool: false","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"x-google-mcp-tool: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" hides an operation from MCP only; the underlying REST route remains live unless secured or removed.","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP Traffic Security","type":"text","marks":[{"type":"link","attrs":{"href":"references/mcp-traffic-security.md","title":null}}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Execution Workflow and Validation Loop","type":"text"}]},{"type":"paragraph","content":[{"text":"Follow this zero-downtime sequence when authoring or updating an API Gateway security or quota configuration:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create the gateway service account and grant backend invoker permissions:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow Steps 1–3 of ","type":"text"},{"text":"Backend Security","type":"text","marks":[{"type":"link","attrs":{"href":"references/backend-security.md","title":null}}]},{"text":" to create ","type":"text"},{"text":"{gateway_sa_name}","type":"text","marks":[{"type":"code_inline"}]},{"text":" and request human approval before granting ","type":"text"},{"text":"roles/run.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"{backend_service}","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update backend handler code to decode verified user claims from ","type":"text"},{"text":"X-Apigateway-Api-Userinfo","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"base64url","type":"text","marks":[{"type":"code_inline"}]},{"text":"-encoded JSON) instead of ","type":"text"},{"text":"Authorization","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Author the OpenAPI authentication and quota definitions:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the OpenAPI 2.0 or OpenAPI 3.0.x templates and provider tables in ","type":"text"},{"text":"Client Authentication","type":"text","marks":[{"type":"link","attrs":{"href":"references/client-authentication.md","title":null}}]},{"text":" and ","type":"text"},{"text":"Quotas and Rate Limiting","type":"text","marks":[{"type":"link","attrs":{"href":"references/quotas-and-rate-limiting.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the specification enables MCP (","type":"text"},{"text":"x-google-api-management.mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":"), configure exactly one JWT or API key scheme on ","type":"text"},{"text":"tools-list","type":"text","marks":[{"type":"code_inline"}]},{"text":" (also list a JWT scheme in the root ","type":"text"},{"text":"security","type":"text","marks":[{"type":"code_inline"}]},{"text":" list), and review operation opt-outs using ","type":"text"},{"text":"MCP Traffic Security","type":"text","marks":[{"type":"link","attrs":{"href":"references/mcp-traffic-security.md","title":null}}]},{"text":".","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the quota metric validation loop BEFORE running ","type":"text","marks":[{"type":"strong"}]},{"text":"api-configs create","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":":","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If neither the active specs nor ","type":"text"},{"text":"{candidate_openapi_spec}","type":"text","marks":[{"type":"code_inline"}]},{"text":" define quota metrics (","type":"text"},{"text":"x-google-management.metrics","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"x-google-api-management.metrics","type":"text","marks":[{"type":"code_inline"}]},{"text":"), skip this step.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request human approval before creating an API config that defines or modifies quotas, because ","type":"text"},{"text":"api-configs create","type":"text","marks":[{"type":"code_inline"}]},{"text":" immediately replaces quota definitions across all gateways of the API.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"By default, run ","type":"text"},{"text":"scripts/check_quota_metrics.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" against all active gateways deployed for ","type":"text"},{"text":"{api_id}","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"{project_id}","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/check_quota_metrics.py \\\n --spec={candidate_openapi_spec} \\\n --project={project_id} \\\n --api={api_id}","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Escape hatch for offline environments without ","type":"text"},{"text":"gcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":" access (pass ","type":"text"},{"text":"--baseline-spec","type":"text","marks":[{"type":"code_inline"}]},{"text":" for every config currently served by a gateway of the API):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/check_quota_metrics.py \\\n --spec={candidate_openapi_spec} \\\n --baseline-spec={previous_openapi_spec} \\\n --dry-run","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"scripts/check_quota_metrics.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" exits with code ","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"status: \"FAIL\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"), add the missing metrics and limits back to ","type":"text"},{"text":"{candidate_openapi_spec}","type":"text","marks":[{"type":"code_inline"}]},{"text":", move each ","type":"text"},{"text":"x-google-quota","type":"text","marks":[{"type":"code_inline"}]},{"text":" listed in ","type":"text"},{"text":"ignored_quota_extensions","type":"text","marks":[{"type":"code_inline"}]},{"text":" into the operations it applies to, and re-run the script until it exits with code ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"status: \"PASS\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"). If the user wants to remove a limit on purpose, confirm the removal with the user, because it removes that limit from every gateway of the API. If ","type":"text"},{"text":"skipped_configs","type":"text","marks":[{"type":"code_inline"}]},{"text":" is not empty, tell the user that the script did not inspect those API configs (for example, gRPC configs), and check their quota metrics and limits manually. If it exits with code ","type":"text"},{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":", fix the problem printed to stderr: the CLI arguments, ","type":"text"},{"text":"gcloud","type":"text","marks":[{"type":"code_inline"}]},{"text":" authentication, or an API config that the script cannot inspect.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create the API config, update the gateway, enable the managed service, and lock down the backend:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow Steps 4–5 of ","type":"text"},{"text":"Backend Security","type":"text","marks":[{"type":"link","attrs":{"href":"references/backend-security.md","title":null}}]},{"text":" to run ","type":"text"},{"text":"gcloud api-gateway api-configs create","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"gcloud api-gateway gateways update","type":"text","marks":[{"type":"code_inline"}]},{"text":" (request human approval before updating a production gateway), enable the API's ","type":"text"},{"text":"managedService","type":"text","marks":[{"type":"code_inline"}]},{"text":" in consumer projects (","type":"text"},{"text":"Client Authentication","type":"text","marks":[{"type":"link","attrs":{"href":"references/client-authentication.md","title":null}}]},{"text":"), and then restrict ","type":"text"},{"text":"{backend_service}","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"--no-allow-unauthenticated","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify that direct calls to the backend ","type":"text"},{"text":"*.run.app","type":"text","marks":[{"type":"code_inline"}]},{"text":" URL return ","type":"text"},{"text":"403","type":"text","marks":[{"type":"code_inline"}]},{"text":", unauthenticated calls to the gateway ","type":"text"},{"text":"defaultHostname","type":"text","marks":[{"type":"code_inline"}]},{"text":" return ","type":"text"},{"text":"401","type":"text","marks":[{"type":"code_inline"}]},{"text":", and authenticated calls return ","type":"text"},{"text":"200","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pre-Deployment Checklist","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Dedicated ","type":"text"},{"text":"{gateway_sa_name}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is granted ","type":"text"},{"text":"roles/run.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"roles/cloudfunctions.invoker","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and ","type":"text"},{"text":"--no-allow-unauthenticated","type":"text","marks":[{"type":"code_inline"}]},{"text":" is applied after the gateway config is active.","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Backend code decodes ","type":"text"},{"text":"X-Apigateway-Api-Userinfo","type":"text","marks":[{"type":"code_inline"}]},{"text":" for user claims.","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Security enforcement does not depend solely on a load balancer or Google Cloud Armor in front of the gateway, because the gateway's ","type":"text"},{"text":"defaultHostname","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"*.gateway.dev","type":"text","marks":[{"type":"code_inline"}]},{"text":") cannot be disabled.","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every quota-enforced operation requires an API key (","type":"text"},{"text":"api_key: []","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and the API's ","type":"text"},{"text":"managedService","type":"text","marks":[{"type":"code_inline"}]},{"text":" is enabled and allowed on the API key in each consumer project.","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"python3 scripts/check_quota_metrics.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns ","type":"text"},{"text":"status: \"PASS\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (exit code ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":") against active gateway configs or all baseline specs before ","type":"text"},{"text":"api-configs create","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"If MCP is enabled, ","type":"text"},{"text":"x-google-api-management.mcp.tools-list.security","type":"text","marks":[{"type":"code_inline"}]},{"text":" names exactly one JWT or API key scheme (a JWT scheme is also in the root ","type":"text"},{"text":"security","type":"text","marks":[{"type":"code_inline"}]},{"text":" list), and administrative endpoints are secured on REST in addition to ","type":"text"},{"text":"x-google-mcp-tool: false","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resource Cleanup","type":"text"}]},{"type":"paragraph","content":[{"text":"To avoid ongoing charges when tearing down a test or staging environment, request human approval and delete the gateway, all API configs, the API, and the service account in dependency order:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gcloud api-gateway gateways delete {gateway_id} --location={region} --project={project_id}\ngcloud api-gateway api-configs delete {config_id} --api={api_id} --project={project_id}\ngcloud api-gateway apis delete {api_id} --project={project_id}\ngcloud iam service-accounts delete {gateway_sa_name}@{project_id}.iam.gserviceaccount.com --project={project_id}","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"api-gateway-backend-security-and-rate-limiting","author":"@skillopedia","source":{"stars":21790,"repo_name":"skills","origin_url":"https://github.com/google/skills/blob/HEAD/skills/cloud/api-gateway-backend-security-and-rate-limiting/SKILL.md","repo_owner":"google","body_sha256":"c801010b21abcd74c61d03789df48a21690b6022fab1c7c04bbc1e457c02127b","cluster_key":"bf3831c181b4282241aa7e7fcdfe5ce3120d082c8dc9ad6816cfe8133cc99531","clean_bundle":{"format":"clean-skill-bundle-v1","source":"google/skills/skills/cloud/api-gateway-backend-security-and-rate-limiting/SKILL.md","text_bytes":52756,"source_commit":"dc266e019a32621845b6b2bb81084b3c9657774c","skills_sh_rank":15171,"attachment_count":6,"text_attachments":6,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/cloud/api-gateway-backend-security-and-rate-limiting/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","metadata":{"version":"1.0.1","category":"Security"},"import_tag":"skills-sh-top20k-2026-11","description":"Secures and rate limits backend services behind Google Cloud API Gateway across OpenAPI 2.0 and OpenAPI 3.0.x API configurations, including MCP-enabled gateways. Use when configuring API key or JWT authentication, locking down Cloud Run or Cloud Run functions backends with a gateway service account, reading authenticated claims from X-Apigateway-Api-Userinfo, setting per-project request quotas, or diagnosing HTTP 429 and HTTP 500 Service Control Quota errors. Do not use for Apigee, GKE Gateway API, Cloud Load Balancing custom domain setup, or LLM model routing."}},"renderedAt":1793661774882}

API Gateway Backend Security and Rate Limiting This skill configures client authentication, gateway-to-backend authentication, and per-project rate limits (quotas) for Google Cloud API Gateway across OpenAPI 2.0 ( ) and OpenAPI 3.0.x ( ), including gateways with Model Context Protocol (MCP) enabled (Public Preview). Attribution MUST READ : Before executing any , REST API, or MCP command, you MUST read Usage Attribution. For commands executed directly in the terminal (never in generated scripts, Terraform, or user code), prefix with and append to headers. Prerequisites 1. Install the Google Cl…