---
key: "vdp-bug-bounty-pentest"
title: "Start with a VDP, then add bug bounties and penetration tests"
url: "https://softwaresecurity.com/domains/appsec/vulnerability-response/vdp-bug-bounty-pentest/"
api: "https://softwaresecurity.com/api/v1/articles/vdp-bug-bounty-pentest.json"
domain: "appsec"
topic: "Application security"
type: "Guide"
identifiers: []
tags: ["Security testing","Vulnerability disclosure"]
stages: ["test","respond"]
roles: ["appsec-engineer","security-leader"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Start with a VDP, then add bug bounties and penetration tests

Publish a vulnerability disclosure policy and security.txt first, add a bug bounty only when you can triage and fix at the pace rewards bring, and buy penetration tests with a written scope, rules of engagement and a retest.

Canonical URL: https://softwaresecurity.com/domains/appsec/vulnerability-response/vdp-bug-bounty-pentest/

## In short

Publish a vulnerability disclosure policy (VDP) and an RFC 9116 `security.txt` first: they cost little, laws and regulators increasingly require them, and they give outsiders a safe, legal way to report. Add a bug bounty only when you acknowledge reports within days and fix at the pace rewards bring, and start it private. Use penetration tests for scoped, time-boxed depth before launches and for assurance, with written rules of engagement and a retest of every fix.

- A VDP is the floor: scope, a reporting channel, safe harbor, response targets and a disclosure window. A bug bounty is the same policy plus rewards.
- `security.txt` needs at least one `Contact` and exactly one `Expires`, served over HTTPS as `text/plain; charset=utf-8` on every public host. Keep `Expires` less than a year ahead.
- Safe harbor binds only you. Leave out assets you cannot authorize testing on, such as a vendor's SaaS.
- Publish response targets and meet them. CISA's baseline is acknowledgement within 3 business days, assessment within 7 days and resolution within 90.
- Pay by impact on your assets, publish the table, and settle duplicates, payment timing and eligibility before launch.
- Buy pentests with a written scope, signed rules of engagement, a stop procedure, immediate notice of critical findings and a retest in the contract.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| `security.txt` location | `https://<host>/.well-known/security.txt` on every public hostname | A file covers only the host that serves it. The legacy `/security.txt` path may redirect. | RFC 9116, section 3 |
| `security.txt` response | HTTPS, `Content-Type: text/plain; charset=utf-8` | — | RFC 9116, section 3 |
| `security.txt` fields | One or more `Contact`; exactly one `Expires`; `Policy`, `Canonical` and a signature recommended | — | RFC 9116, sections 2.3 and 2.5 |
| `Expires` | Less than a year ahead; renew at least two weeks before it passes | The two-week margin is local. | RFC 9116, section 2.5.5 |
| Acknowledge a report | Within 3 business days, from a person | — | CISA BOD 20-01 |
| Initial assessment | Within 7 days of receipt | — | CISA BOD 20-01 |
| Resolution | Within 90 days of receipt; critical issues much sooner | — | CISA BOD 20-01 |
| Disclosure window | 90 days from the report, or earlier once a fix ships, by agreement | CISA describes 45 to 90 days as the range researchers commonly accept. | CISA BOD 20-01 FAQ |
| First bug bounty | Private, invitation-only, narrow scope | Local starting point; widen it only while triage and fix targets hold. | — |
| Penetration test retest | In the contract; repeat the original test cases against the fixed build | — | NIST SP 800-115, section 8.3 |

## Verify it

1. Fetch `/.well-known/security.txt` from every public hostname and confirm a 200 over HTTPS, `text/plain; charset=utf-8`, a working `Contact` and one `Expires` in the future.
2. Send a test report through each published channel and time the reply against your stated target.
3. Compare the policy's scope with the asset inventory and third-party contracts; drop, or get written authorization for, anything you cannot authorize.
4. From last quarter's reports, compute median days to acknowledge, assess and resolve, and count valid reports open for more than 90 days.
5. Before a pentest starts, confirm the signed rules of engagement list the exclude list, test windows, tester source addresses, emergency contacts and stop conditions.
6. After fixes, confirm the retest report gives every original finding a status backed by the same test case.

## What you will learn

- Choose between a VDP, a bug bounty and a penetration test, and put them in order.
- Write a disclosure policy with scope, safe harbor, response targets and a disclosure window.
- Publish, sign and continuously check `security.txt` on every public host.
- Scope a penetration test with rules of engagement, reporting requirements and a retest.

Audience: AppSec leads, product security teams and engineering managers who receive outside vulnerability reports or buy security testing

Before you start:

- Know which domains, apps and products you run, and which third parties host them.
- Read HTTP response headers.
- For the code: nginx or another web server, GnuPG and Python 3.11 or later.

Time: About 38 minutes: 12 to read, 4 on the code and 22 for the exercise

## VDP, bug bounty or penetration test: which does what?

A vulnerability disclosure program (VDP) is a published promise: a channel for anyone to report security problems, rules for good-faith testing and a commitment to respond. A bug bounty adds money for valid findings, which brings more testers and more reports, including more noise. A penetration test buys a fixed amount of expert time against an agreed scope and ends with a report. They answer different questions, so add them in order rather than choosing one.

| Question | VDP | Bug bounty | Penetration test |
| --- | --- | --- | --- |
| Who tests | Anyone who finds something | Researchers drawn by rewards, invited first | A team you contract |
| Coverage | Whatever people happen to find | Broad and continuous on in-scope assets | Deep, within the agreed scope and dates |
| Cost | Staff time to triage and fix | Rewards, platform or triage fees, staff time | A fixed fee, usually priced in tester-days |
| Output | Individual reports | Individual reports, paid by severity | A report with method, findings and limits, then a retest |
| Main risk | Reports with nowhere to go | More volume than you can triage or fix | A narrow test read as a clean bill of health |

## Why publish a VDP first?

Start with the VDP. It is cheap, covers every asset you can authorize, and is increasingly required. From 11 December 2027, the EU Cyber Resilience Act requires manufacturers of products with digital elements to have and enforce a coordinated vulnerability disclosure policy, provide a contact address for reports (Annex I, Part II, points 5 and 6) and name a single point of contact in the user information. In the UK, makers of consumer connectable products have had to publish a reporting contact and response timelines since 29 April 2024. US federal civilian agencies have needed a VDP since CISA's Binding Operational Directive 20-01 in 2020.

Add a bug bounty when the VDP runs smoothly. The OWASP Vulnerability Disclosure Cheat Sheet reserves bounties for organizations that already have a mature disclosure process and the internal capacity to fix, and CISA notes that rewards can raise both report volume and the share of low-quality submissions. Start private, with a narrow scope and invited researchers, and widen it only while your response targets hold. Penetration tests sit alongside both: commission one before a major launch or high-risk change, or when a customer or standard requires it. The UK NCSC treats a test as assurance for your own vulnerability management rather than your main way to find bugs; ideally you already know most of what the testers report.

## What goes in a vulnerability disclosure policy?

ISO/IEC 29147:2018 covers how a vendor receives reports and publishes advisories, and ISO/IEC 30111:2019 covers the internal handling process. Both are being revised, and in March 2026 the ISO/IEC working group reported a working draft that combines their content in one structure. You don't need either to start. disclose.io publishes public-domain policy templates, including one with a disclosure window and a standalone safe-harbor clause, and CISA publishes a template for federal agencies. Have counsel review whichever you adapt. A complete policy covers:

- Scope: the domains, apps and products covered, and what is excluded, especially third-party services you cannot authorize testing on.
- Rules: no denial of service, social engineering or physical testing; use only your own test accounts; stop at a proof of concept and report any personal data you reach.
- Reporting: the channel, the details needed to reproduce, and whether anonymous reports are accepted.
- Safe harbor: authorization under anti-hacking and anti-circumvention laws, a waiver of conflicting terms of service, and a promise not to pursue good-faith research.
- Commitments: response targets, status updates, credit and a disclosure window.
- Payment: state that the VDP pays nothing, or link to the bounty policy that does.

## Where does safe harbor stop?

Safe harbor binds only the organization that offers it, and the disclose.io text says so explicitly. You cannot authorize testing of systems you don't control, so a help desk or status page run by a vendor stays out of scope unless that vendor agrees in writing. Avoid shrinking the scope later: CISA warns that researchers may still rely on the earlier scope. Once a report arrives, /domains/appsec/vulnerability-response/coordinated-disclosure/ covers reproduction, fixes and the advisory.

## What goes in security.txt?

RFC 9116 defines a small text file at `/.well-known/security.txt` that tells researchers where to report. It needs at least one `Contact`, listed in order of preference, and exactly one `Expires`; the RFC recommends keeping `Expires` less than a year ahead, because stale contacts send reports to the wrong place. Serve it over HTTPS as `text/plain; charset=utf-8`. A file covers only the host that serves it, not subdomains, so publish one on every public hostname. `Policy` links to your VDP, and `Canonical` lists the URLs where the file should live.

IANA's field registry has grown since the RFC: `CSAF` (2023) points to a CSAF `provider-metadata.json` file, and `Bug-Bounty` (March 2026) takes `True` or `False` to say whether you pay rewards. The file grants no permission to test; only your policy can.

```text
# Applies to example.com only; each host needs its own file.
Contact: mailto:security@example.com
Contact: https://example.com/security/report
Expires: 2027-04-10T00:00:00Z
Encryption: https://example.com/security/pgp-key.asc
Policy: https://example.com/security/disclosure-policy
Acknowledgments: https://example.com/security/thanks
Preferred-Languages: en, de
Canonical: https://example.com/.well-known/security.txt
Bug-Bounty: False
```

## Serve it with the right type

Single-page app hosting often answers unknown paths with `index.html`, which hides a missing file behind a 200. Map the path explicitly. nginx's `charset` directive adds the parameter to `text/plain` responses, and a relative redirect keeps the legacy path working behind proxies.

```nginx
# Inside the HTTPS server block of every public hostname
location = /.well-known/security.txt {
    alias /srv/security/security.txt;
    default_type text/plain;
    charset utf-8;              # Content-Type: text/plain; charset=utf-8
}
location = /security.txt {      # legacy path
    absolute_redirect off;      # relative Location, correct behind proxies
    return 301 /.well-known/security.txt;
}
```

## Sign it with a key kept off the web server

RFC 9116 recommends an OpenPGP cleartext signature together with `Canonical`, so researchers can tell that the file and its location are genuine. An attacker who controls the site can edit an unsigned file to redirect reports, so sign elsewhere, publish the public key and monitor the file for changes.

```bash
# Run where the signing key lives, not on the web server
gpg --batch --yes --local-user security@example.com --clear-sign \
    --output security.txt security.txt.unsigned
gpg --verify security.txt
```

## Check every host in CI

Run this check on a schedule against every public hostname. It fails when the file is missing, served with the wrong type, lacks a `Contact`, has an `Expires` that is malformed, nearly due or more than a year away, uses a plain-HTTP link, or is served from a URL that `Canonical` doesn't list. It unwraps signed files but does not verify the signature; run `gpg --verify` for that.

```python
"""Fail CI when a host's security.txt is missing, malformed or nearly stale.

Usage: python check_securitytxt.py https://example.com/.well-known/security.txt
"""
import sys
import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone

URI_FIELDS = {"contact", "encryption", "policy", "acknowledgments", "canonical", "hiring", "csaf"}


def parse(text):
    """Return (name, value) pairs; unwrap an OpenPGP clear-signed file first."""
    text = text.replace("\r\n", "\n")
    if text.startswith("-----BEGIN PGP SIGNED MESSAGE-----"):
        body = text.split("\n\n", 1)[1].split("-----BEGIN PGP SIGNATURE-----")[0]
        text = "\n".join(l[2:] if l.startswith("- ") else l for l in body.splitlines())
    pairs = []
    for line in text.splitlines():
        if line.strip() and not line.startswith("#") and ":" in line:
            name, value = line.split(":", 1)
            pairs.append((name.strip().lower(), value.strip()))
    return pairs


def problems(url, text, content_type, now=None):
    now = now or datetime.now(timezone.utc)
    fields = parse(text)
    found = []
    if content_type.replace(" ", "").lower() != "text/plain;charset=utf-8":
        found.append(f"Content-Type is {content_type!r}")
    if not any(name == "contact" for name, _ in fields):
        found.append("no Contact field")
    expires = [value for name, value in fields if name == "expires"]
    if len(expires) != 1:
        found.append(f"{len(expires)} Expires fields; exactly one is required")
    else:
        try:
            when = datetime.fromisoformat(expires[0].upper())  # RFC 3339 allows t and z
            if when.tzinfo is None:
                raise ValueError("no UTC offset")
        except ValueError as err:
            found.append(f"Expires {expires[0]!r} is not an RFC 3339 date-time: {err}")
        else:
            if when < now + timedelta(days=14):  # local policy: renew two weeks early
                found.append(f"Expires {expires[0]} is less than 14 days away")
            if when > now + timedelta(days=365):  # RFC 9116 recommends under a year
                found.append(f"Expires {expires[0]} is more than a year away")
    for name, value in fields:
        if name in URI_FIELDS and value.lower().startswith("http:"):
            found.append(f"{name} uses http: {value}")
    canonical = [value for name, value in fields if name == "canonical"]
    if canonical and url not in canonical:
        found.append(f"{url} is not listed in Canonical")
    return found


if __name__ == "__main__":
    url = sys.argv[1]
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:  # follows redirects
            if resp.url != url:
                print(f"note: redirected to {resp.url}")
            body = resp.read(32 * 1024).decode("utf-8", "replace")  # RFC 9116 parse limit
            issues = problems(resp.url, body, resp.headers.get("Content-Type", ""))
    except (urllib.error.URLError, OSError) as err:
        issues = [f"cannot fetch {url}: {err}"]
    for issue in issues:
        print("FAIL:", issue)
    sys.exit(1 if issues else 0)
```

## What response targets and severity ratings should you set?

Publish targets you can meet, then measure against them. BOD 20-01 requires federal agencies to set targets for acknowledgement, initial assessment and resolution, and CISA recommends no more than 3 business days to acknowledge, 7 days to assess and 90 days to resolve. That is a sensible baseline anywhere. A named person should reply, not only an auto-responder, and updates should continue until the case closes.

Rate technical severity with CVSS 4.0 or a published taxonomy such as Bugcrowd's open-source Vulnerability Rating Taxonomy, which runs from P1 (critical) to P5 (informational). Then adjust for the asset: the same bug matters more on the payments API than on a marketing page. Record a reason for every out-of-scope or duplicate decision. A demand for payment before details are shared is often a scam; ask for the report through your channel. If triage finds reliable evidence that a malicious actor exploited a product with digital elements that you place on the EU market, the Cyber Resilience Act has required an early warning to the coordinating CSIRT and ENISA within 24 hours since 11 September 2026; a researcher's good-faith proof of concept doesn't count as exploitation. Ranking is covered in /frameworks/vulnerability-prioritization/, and the rest of the case in the coordinated disclosure guide. Behind the policy, run reports through a PSIRT process as described in /domains/appsec/vulnerability-response/psirt-operating-model/, and publish fixes with CVE records and CSAF advisories as in /domains/appsec/vulnerability-response/security-advisories-csaf-vex/.

## How much should a bug bounty pay?

Tie rewards to severity on your own assets, and publish the table before launch. For calibration, GitHub's reward guidelines in October 2026 listed $10,000 for a critical finding in its public program and $30,000 or more in its private one, down to $250 and $1,000 for low severity, and swag rather than cash for defense-in-depth reports. Decide these rules before the first report arrives:

- Duplicates: pay the first valid, reproducible report; later reporters get thanks, not money.
- Timing: pay at triage or at fix. Paying at triage stops researchers waiting on your release schedule.
- Eligibility: exclude employees and recent contractors, collect tax forms, and screen payees against the sanctions lists that bind you.
- Budget: rewards plus platform or triage fees plus the engineering time to fix what arrives.

## Should you run a bounty yourself or through a platform?

Both models work. Google, Microsoft and Apple run their own intake portals, while GitHub runs its bounty on a commercial platform. Compare what you would have to build:

| Need | Platform | In-house |
| --- | --- | --- |
| Researchers | An existing community and invitation-only programs | You attract them with your policy and reputation |
| Triage | Optional managed first-line triage, for a fee | Your own staff, including cover |
| Payments | Identity checks, tax forms and payouts handled for you | Finance builds the same process |
| Data | Reports held on a third-party system; check residency and export | Reports stay in your systems |
| Cost | Platform fees plus rewards | Staff time plus rewards |

## How do you scope a penetration test?

Start from a scoping document, not a price list. The NCSC recommends scoping with the risk owners, staff who know the system and a representative of the testers, and recording the technical boundaries, test types, timeframe, effort in tester-days, any scenarios, what the testers need from you, compliance needs and reporting requirements. Share your latest vulnerability assessment so the testers can judge how accurate it is. Flag unusual technology, such as mainframes or custom hardware, in the bid so suppliers can staff for it.

For custom applications, prefer grey-box or white-box testing with accounts for every role and access to source or design documents. NIST SP 800-115 notes that white-box techniques tend to find defects in custom code more efficiently, while black-box testing suits interactions between components and with users. Name a requirement baseline, such as OWASP ASVS 5.0.0 level 2, so findings and gaps are judged against something specific. Ask for redacted sample reports and the experience of the people who will do the work: the NCSC notes that a test cannot be reduced to a checklist, so its quality depends on the testers.

## Agree rules of engagement before testing starts

The rules of engagement turn the scope into permission. NIST SP 800-115 Appendix B is a usable template that ends with a signature page for the test lead and senior management. In the cloud, read the provider's policy too: AWS lets customers test listed services without prior approval but forbids denial of service, request flooding and Route 53 zone walking, and requires a form before simulated phishing, malware or command-and-control tests. Microsoft and Google Cloud also allow customers to test their own resources within their usage rules.

| Item | What to agree |
| --- | --- |
| Authorization | Signed by someone who can authorize every in-scope asset, plus written consent from any hosting provider or SaaS owner that requires it |
| Targets | Hostnames, address ranges, apps, API versions and environments in scope, and an exclude list |
| Windows and sources | Dates, hours and the testers' source addresses, so monitoring can tell them apart |
| Accounts and data | Test accounts for each role and tenant, synthetic data, and what testers may create, change or delete |
| Techniques | Whether social engineering, denial-of-service checks or physical access are allowed |
| Contacts and stop | Contacts on both sides who answer during testing, criteria for halting, and how testing resumes |
| Critical findings | Raised with your contact as soon as they are found, not held for the final report |
| Evidence | How test data is stored, shared and destroyed after the engagement |
| Deliverables | Report format, severity scheme, debrief, retest window and any attestation letter |

## What should a penetration test report and retest include?

The report should give each issue a risk rating, evidence, reproduction steps and a fix, and state what was not tested and why. The NCSC adds two items that make a test worth more than its findings: an opinion on the accuracy of your own vulnerability assessment, and advice on improving it. Ask for one severity scheme, such as CVSS 4.0, with a justification whenever the testers depart from it. Then re-rate the findings yourself, because only you know the business impact.

Put the retest in the contract. NIST SP 800-115 points out that a retest proves a fix only when it repeats the original test, so ask for the same test cases against the fixed build and a status for every finding. Treat each finding you didn't already know about as a gap in your own testing, and work out which control should have caught it. A clean report covers only the scope and dates tested; it does not say the system is secure.

## Guided practice

Fenwick, a SaaS company, plans to launch a public bug bounty on 2026-11-01. Review its current setup and decide what must change first. The company, hosts and figures are fictional.

### Supplied material

```text
security.txt checks on 2026-10-10:
  www.fenwick.example  200 text/plain; charset=utf-8  Contact: mailto:security@fenwick.example  Expires: 2026-08-31T00:00:00Z
  app.fenwick.example  200 text/html (the single-page app returns index.html for every path)
  api.fenwick.example  404
Policy draft:
  Scope: "All Fenwick systems, including help.fenwick.example" (a CNAME to a third-party help-desk SaaS)
  "Researchers may never publish details of any issue."
  "We will respond when we can." No safe-harbor or authorization language.
VDP results, July to September 2026: 24 reports; median 9 business days to acknowledge; 6 valid; 3 valid reports open for more than 90 days.
Pentest statement of work: black box, 3 tester-days, scope app.fenwick.example only, findings delivered in the final report only, no retest.
```

### 1. Fix \`security.txt\`

Say what is wrong on each host and how to fix it.

Worked answer: www's file expired on 2026-08-31: renew it with an `Expires` less than a year ahead. app serves HTML because the app catches the path: add an explicit route that returns the file as `text/plain; charset=utf-8`. api has no file, and each host needs its own. Add `Policy` and `Canonical`, sign the files, and run the CI check on all three hosts.

### 2. Fix the policy

Rewrite the three problem statements.

Worked answer: Remove `help.fenwick.example` unless the SaaS vendor authorizes testing in writing; Fenwick cannot authorize it. Replace the publication ban with a coordinated window, such as 90 days or earlier once a fix ships. Add safe-harbor language, for example from disclose.io, and concrete targets: acknowledge within 3 business days, assess within 7 days, resolve within 90.

### 3. Decide on the bounty

Make a go or no-go call on the 1 November launch and give the reason.

Worked answer: No-go. A 9-day median acknowledgement and 3 valid reports past 90 days show the VDP is not keeping up, and rewards are likely to raise volume. Meet the targets for a quarter, then start a private program with a narrow scope.

### 4. Fix the pentest statement of work

List the changes to the scope and terms.

Worked answer: Make it grey box with accounts for each role, add the API, size the effort to the new scope, require critical findings to be raised as soon as they are found, include a retest that repeats the original test cases, and attach signed rules of engagement with contacts, windows, source addresses and stop conditions.

### 5. Report VDP health

Choose the measures for a quarterly report and fill them in for last quarter.

Worked answer: Reports received (24), valid reports (6), median time to acknowledge (9 business days against a 3-day target), median time to assess and to resolve (missing from the records, so start capturing them), and valid reports open for more than 90 days (3). BOD 20-01 asks federal agencies for a similar set.

Deliverable: A corrected `security.txt` for each host, a revised policy, a go or no-go note on the bounty and a revised pentest statement of work.

Scope: Paper exercise with fictional hosts and figures. Use the checker script against your own hostnames for the `security.txt` step.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [RFC 9116: A File Format to Aid in Security Vulnerability Disclosure (security.txt)](https://www.rfc-editor.org/rfc/rfc9116.html). April 2022.
- [ISO/IEC 29147:2018 Information technology: Vulnerability disclosure](https://www.iso.org/standard/72311.html)
- [ISO/IEC 30111:2019 Information technology: Vulnerability handling processes](https://www.iso.org/standard/69725.html)
- [disclose.io dioterms: public-domain VDP, safe harbor and bug bounty templates](https://github.com/disclose/dioterms)
- [CISA BOD 20-01: Develop and Publish a Vulnerability Disclosure Policy](https://www.cisa.gov/news-events/directives/bod-20-01-develop-and-publish-vulnerability-disclosure-policy). 2 September 2020.
- [Regulation (EU) 2024/2847, Cyber Resilience Act (Article 14; Annex I, Part II)](https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng). Official Journal, 20 November 2024.
- [OWASP Vulnerability Disclosure Cheat Sheet: Bug Bounty Programs](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#bug-bounty-programs)
- [GitHub Security Bug Bounty: reward guidelines](https://bounty.github.com/rewards)
- [UK NCSC: Penetration testing](https://www.ncsc.gov.uk/guidance/penetration-testing)
- [NIST SP 800-115: Technical Guide to Information Security Testing and Assessment](https://csrc.nist.gov/pubs/sp/800/115/final). September 2008.