Api Key Exposure in Rails with Firestore
Api Key Exposure in Rails with Firestore — how this specific combination creates or exposes the vulnerability
When a Ruby on Rails application interacts with Google Cloud Firestore, developers often store the Firestore service account key as a JSON file on disk or embed it in environment variables. If the Rails app is misconfigured or accidentally exposes these files through source control, logs, or debug endpoints, an API key exposure occurs. In this context, the Firestore key functions as a bearer token: any holder can make requests to the project’s Firestore REST or gRPC interfaces as permitted by the key’s IAM bindings.
Rails applications commonly load the key via GOOGLE_APPLICATION_CREDENTIALS or by reading a JSON file directly. If the Rails app runs in a container or shared host where the filesystem is inspectable, or if error messages include stack traces pointing to the key location, the key may be discoverable. For example, a verbose Rails log that prints the contents of ENV["FIRESTORE_KEY_JSON"] would constitute an API key exposure. Similarly, a misconfigured Rails route such as /debug/firestore_key that returns the key in plaintext would create a direct exposure path.
Once exposed, an attacker can use the key to read or write documents, depending on the service account’s roles (e.g., roles/datastore.user or more permissive roles). This can lead to data exfiltration, unauthorized modification of application state, or pivoting to other Google Cloud services if the same key is reused. The risk is amplified when the Firestore database contains sensitive user data or when the Rails app is part of a larger system where Firestore permissions are broad.
An attacker may also leverage the exposed key to perform SSRF from the server side, reaching internal metadata services, or to abuse Firestore’s export functionality to move large datasets off the environment. Because the key is a long-lived credential, continued access is possible until the key is rotated and the exposure is remediated.
Firestore-Specific Remediation in Rails — concrete code fixes
Remediation centers on preventing the key from appearing in logs, URLs, or error responses, and on minimizing the scope of the key if it must be used. Avoid embedding the full service account JSON in Rails environment variables; instead use workload identity federation or short-lived credentials where possible.
When you must use a key file, store it outside the repository and reference it securely. In production, prefer the default Compute Engine or GKE service accounts so Rails can use Application Default Credentials without a key file. For local development, use a restricted service account with the least privilege necessary.
To limit impact, scope the service account to only the Firestore operations required. For example, if the app only needs to read configuration documents, grant datastore.entities.get on specific document paths rather than datastore.entities.list or owner roles. Use Firestore’s native security rules to add an additional layer of access control for user-facing requests, ensuring that server-side keys are not relied upon for per-user authorization alone.
Example: Safe Firestore client initialization in Rails
Initialize the Firestore client without exposing the key in logs or error output. Avoid printing the client or its configuration in Rails console or views.
require "google/cloud/firestore"
# Use Application Default Credentials in production (no key file).
# On your development machine, set GOOGLE_APPLICATION_CREDENTIALS to a restricted key.
begin
firestore = Google::Cloud::Firestore.new
rescue => e
Rails.logger.error "Firestore initialization failed"
# Do NOT log e.message if it may contain project or credential details
raise "Unable to connect to Firestore"
end
Example: Controlled document access with explicit project and credentials
When you must specify a project ID explicitly, avoid leaking the key in URLs or logs. Use environment variables that are scrubbed from logs and do not include the full JSON in error messages.
project_id = ENV.fetch("GCLOUD_PROJECT")
# key_path is used by the client library via ADC; do not print key_path.
key_path = ENV.fetch("FIRESTORE_KEY_PATH")
begin
firestore = Google::Cloud::Firestore.new project: project_id, keyfile: key_path
docs = firestore.collections("users").where("active", "==", true).get
docs.each do |doc|
Rails.logger.debug "Loaded user document #{doc.id}"
# Avoid logging document contents that may contain PII
end
rescue Google::Cloud::Error => e
Rails.logger.error "Firestore read error"
raise "Unable to retrieve data"
end
Example: Minimal read-only access via environment configuration
Configure the Rails app to use a service account restricted to read-only access on specific collections. Never generate or expose the key via Rails routes or debugging endpoints.
# config/initializers/firestore.rb
require "google/cloud/firestore"
project_id = ENV.fetch("GCLOUD_PROJECT")
# keyfile is set only if a key is necessary; otherwise rely on ADC.
if ENV["FIRESTORE_KEY_PATH"].present?
firestore = Google::Cloud::Firestore.new project: project_id, keyfile: ENV["FIRESTORE_KEY_PATH"]
else
firestore = Google::Cloud::Firestore.new project: project_id
end
# Use the client safely within service objects; do not expose it globally.
module FirestoreService
def self.users
firestore.collections("users").where("suspended", "==", false).get
end
end
Security hygiene notes
- Do not include the service account JSON in version control or in Rails
secrets.ymlorcredentials.yml.encas plain text. - Rotate keys immediately if any exposure is suspected. In Google Cloud, disable the key and create a new one with restricted permissions.
- Audit IAM bindings regularly: ensure the service account does not have broader roles than necessary.
- Use Rails log filtering to prevent sensitive environment variables from appearing in logs.