Aller au contenu principal

Data handling & privacy

ggscout runs inside your infrastructure and reads secrets from your secrets managers, CI/CD systems, and clusters. This page is for the security review that comes before deploying it: what leaves your network, what never does, how to verify it yourself, and what GitGuardian does with the data afterwards.

How a run handles secret values​

A fetch-and-send run goes through five stages. Secret values exist in clear text only in ggscout's memory, between the first and the fourth stage.

  1. Fetch: ggscout reads secrets and their metadata from the sources you configured, with the credentials you gave it.
  2. Classify: each value is matched against the Secrets Detection Engine detectors, locally. No value is sent to GitGuardian for classification.
  3. Check: for classified secrets, ggscout asks the secret's own provider whether it is still valid. This is the only stage where a value leaves ggscout in clear: it goes to the provider that issued it (GitHub, AWS, Stripe, ...), from your network, never through GitGuardian. It is the same call your application makes when it uses the credential. You can turn this stage off.
  4. Hash: every value is replaced in memory by its hash, see How secrets are hashed.
  5. Send: the hashed inventory is compressed and uploaded to your GitGuardian workspace over TLS.

ggscout writes nothing to disk during fetch-and-send. The fetch command writes the inventory to a local JSON file that already contains hashes, not values.

What ggscout sends to GitGuardian​

Everything below is visible in the output of ggscout fetch, which is byte for byte what send uploads.

DataDetails
Secret hashThe HMSL hash of each value, with the value's length in bytes.
Detector fingerprintFor classified secrets only: the detector name (for example aws_iam), the role of the field in the credential (client_id, password, ...), and the detector fingerprint GitGuardian keys secret incidents on.
LocationSource name and type, resource identifier (for Vault: namespace, mount, and path), key name inside the secret, version, and a link to the secret in your secrets manager when the source exposes one.
Secret metadataCreation, update, and expiry dates, whether this is the latest version, labels, tags, and custom metadata as stored in the source. This is where the inferred environment comes from.
Owner hintsThe owner of the secret or of its source, as an email when available, and how it was determined (creator, last editor, tag, owner setting).
Validity check outcomeStatus (valid, invalid, failed_to_check, ...), the sorted list of scopes, the owner the provider reports (user ID, email, admin flag), the expiry date, the check time, and an error category when the check failed. Never the provider's own error text.
Key fragments for OpenAI and Anthropic keysThe last 4 characters of the key, plus the 6-character version segment for Anthropic keys (03-SW5 in sk-ant-api03-SW5...). These are the fragments the providers themselves display, and they let GitGuardian match the vaulted key with the one listed by your OpenAI or Anthropic integration.
Structured value partsWhen a value is parsed as a URL, the scheme and username are sent in clear and the password is hashed. For an email, the domain is sent in clear and the local part hashed. For an IP address, the version and network. For a timestamp, its components. The full value is always hashed as well.
Workload dataFor Kubernetes sources: service accounts, and the deployments that mount each secret. These are not secrets and are sent as is.
Run metadataA run identifier, the ggscout version, the detection engine version, the schema version, and the collection time.

What ggscout never sends to GitGuardian:

  • the clear text of any secret value, in any stage;
  • the credentials ggscout uses to reach your sources (Vault token, cloud credentials, kubeconfig);
  • anything from a path or namespace your include and exclude filters leave out;
  • usage telemetry or crash reports. The only GitGuardian endpoints ggscout calls are listed in Network calls a run makes.

Logs, errors, and telemetry​

In release builds, secret values and source credentials are wrapped in a type that is never printed or serialized unhashed, including at debug verbosity and in error messages. The provider's reply to a validity check is reduced to a status and an error category before anything is logged, and the logs of the HTTP and analyzer libraries are capped below the level at which they print request or response bodies, whatever RUST_LOG says. A debug log shared with GitGuardian support contains paths, source names, counts, and error text returned by your sources (for example permission denied), not values.

How secrets are hashed​

Every value gets the HMSL hash, a slow, memory-hard scrypt hash. Values a detector recognized also get the detector fingerprint listed in the table above; like the HMSL hash, it is one way and deterministic.

The HMSL hash​

The function is scrypt with N = 2048, r = 8, p = 1, a 32-byte output, and a fixed salt: the SHA-256 digest of the string GitGuardian. The result is 64 hexadecimal characters. Nothing else goes into it: no workspace key, no per-secret salt.

import hashlib

salt = hashlib.sha256(b"GitGuardian").digest()
hashlib.scrypt(b"p@ssword123", salt=salt, n=2048, r=8, p=1, dklen=32).hex()
# 'ec49c2f41a02d20f9854e131f082987aee8d76118bcbaafeebc90aa90e66a60b'

For one value, the hashing stage does this:

  1. Takes the raw bytes of the value, exactly as the source returned them. No trimming or normalization: abc and abc\n give different hashes.
  2. Runs scrypt over them with the salt and parameters above. Each hash needs about 2 MiB of RAM and a few milliseconds of one CPU core, which is why ggscout hashes in parallel at the end of the run.
  3. Keeps the hex string and frees the value.

Why these choices:

  • One way. scrypt is a key derivation function. There is no operation that turns the hash back into the value, for GitGuardian or for anyone else.
  • Deterministic. The salt is fixed on purpose: a per-secret or per-workspace salt would stop GitGuardian from recognizing the same secret in two places, which is the point of the inventory.
  • Full hash, not a prefix. ggshield hmsl check sends only the first 5 hexadecimal characters of each hash to the public HasMySecretLeaked service and filters candidates locally, because that service is untrusted by design. ggscout sends the full hash, because it talks to your own workspace and GitGuardian needs an exact match to link the secret to your incidents.

What a hash reveals​

A hash reveals that two values are identical, and nothing else about the value. GitGuardian uses that equality in three ways:

  • to match a vaulted secret with the secret incidents of your workspace;
  • to flag duplicated and reused secrets across your sources, since the same value gives the same hash wherever it is stored;
  • to flag publicly leaked secrets, by matching against the leaks known to HasMySecretLeaked.

ggscout sends the length next to the hash so the inventory can tell a 6-character password from a 40-character token without seeing either. The hash itself says nothing about how strong the value is: the hash of password is as random-looking as the hash of a 64-character token. Weakness only shows if someone guesses.

What guessing costs​

The residual risk is the one any hash carries: a value weak enough to be guessed can be confirmed by whoever holds its hash. Each guess costs the full scrypt work, a few milliseconds of CPU and 2 MiB of RAM, and that memory cost is what keeps GPUs and ASICs from speeding it up much. A dictionary word or a common password is within reach; a random token of 20 characters or more is not. The fixed salt does not change this: a precomputed table has to be built for this scheme specifically, pays the same cost per entry, and only covers values a dictionary attack would find anyway.

What ggscout reads from your sources​

ggscout reads only what the credential you give it can list and read, narrowed further by your include and exclude filters. Where the source exposes metadata (tags or labels, creation and update dates, versions, owner-like fields), ggscout keeps only the fields in the table above. In the source's audit log, every read is attributed to that credential, so give ggscout its own to make its activity easy to trace and revoke. The exact API calls differ per source and are described on each integration page.

Network calls a run makes​

ggscout opens outbound connections only. It listens on no port.

DestinationWhenWhat for
Your secrets managers, CI/CD systems, clustersfetch, fetch-and-sendRead secrets and metadata. Write, only in write or read/write mode during sync-secrets.
The providers of your classified secrets (GitHub, AWS, Stripe, ...)fetch, fetch-and-send, unless [check] enabled = falseValidity, scopes, owner, expiry.
Your GitGuardian API endpointfetch-and-send, send, ping, sync-secretsPOST /v1/nhi/inventory/upload (gzip) to upload the hashed inventory, POST /v1/nhi/ping to report each source's identifier, environment, owner, and read or write mode, and the nhi/get-secrets-to-sync and nhi/sync-secret-result endpoints for secret synchronization.

Connections to GitGuardian use TLS with a certificate bundle embedded in the binary (Mozilla's root store), so they verify certificates even in a scratch container. Add your own CA with the SSL_CERT_FILE environment variable, and route through a proxy with HTTPS_PROXY. The GitGuardian API token travels in the Authorization header.

If the cluster running ggscout has no route to GitGuardian, ggbridge carries the traffic over a mutually authenticated TLS tunnel you control.

Permissions ggscout needs​

On GitGuardian: a service account token with the nhi:send-inventory scope. Add nhi:write-vault only if you use secret synchronization. With these scopes alone, the token cannot read incidents, sources, or members. See GitGuardian authentication.

On your sources: read and list on the paths you inventory, nothing else. Scope the policy to a folder, a project, or a single secret when you can. Each integration page gives the exact policy; for HashiCorp Vault, for example, that is read and list on metadata/ and read on data/ for KV v2.

Writing to a source is off by default (mode = "read"). When you enable it, grant create and update only on a path reserved for ggscout. In this flow, the secret to vault comes from a GitGuardian incident and travels GitGuardian → ggscout → your vault over TLS; it is the one case where a clear-text value crosses from GitGuardian into your network, at your request. ggscout never deletes from a source.

Verify it yourself​

  1. Run ggscout fetch config.toml instead of fetch-and-send. It writes inventory.json and sends nothing.
  2. Open the file. Every hash field is a 64-character hexadecimal string. Search for a value you know is in the source: it is not there.
  3. Recompute one hash with the snippet in The HMSL hash, or with ggshield hmsl fingerprint, and compare.
  4. When satisfied, run ggscout send config.toml to upload that same file, or switch to fetch-and-send.

To verify what you run:

  • Container images on ghcr.io/gitguardian/ggscout/* are signed with Sigstore cosign from the release workflow. Check a tag with:

    cosign verify ghcr.io/gitguardian/ggscout/chainguard:0.35.0 \
    --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    --certificate-identity-regexp '^https://github.com/GitGuardian/ggscout/'
  • Binaries and Python wheels on the releases page ship with a SHA256SUMS file.

  • The images run as a non-root user (UID 65532). The scratch variant contains the ggscout binary and nothing else.

Source code access is available on request through your GitGuardian contact.

Where your data is stored, and for how long​

The inventory is stored in your GitGuardian workspace, visible to the users and roles you allow, and surfaced in the NHI inventory. On SaaS, it stays in the region of your workspace (US or EU). On self-hosted GitGuardian, ggscout only talks to your own instance.

  • Raw uploads. GitGuardian keeps a rolling window of your most recent upload payloads per workspace, so the inventory can be rebuilt when its data model changes. Older payloads are deleted automatically.
  • Processed inventory. Each secret is kept with the dates it was first and last seen. A secret that your runs stop reporting leaves the active inventory after a grace period and no longer counts for policies; its history stays with the workspace.

See Data retention for how long GitGuardian keeps workspace data overall, and the Trust Center for our security and compliance documentation. If you need a signed document for an internal review, ask your GitGuardian contact.