Skip to content

fix: Enforce webhook signature for Forgejo#2421

Merged
chmouel merged 1 commit intotektoncd:mainfrom
chmouel:srvkp-10609-forgejo-webhok-validation
Feb 12, 2026
Merged

fix: Enforce webhook signature for Forgejo#2421
chmouel merged 1 commit intotektoncd:mainfrom
chmouel:srvkp-10609-forgejo-webhok-validation

Conversation

@chmouel
Copy link
Copy Markdown
Member

@chmouel chmouel commented Jan 28, 2026

Auto-created Ticket

#2422

Implemented signature validation for Gitea and Forgejo webhooks to ensure the authenticity of incoming requests. This involves checking the X-Forgejo-Signature and X-Gitea-Signature headers against a computed HMAC-SHA256 hash of the request payload using a configured webhook secret.

The validation enforce that only requests originating from a trusted source with the correct secret are processed, enhancing the security of webhook integrations. This change also includes necessary test cases to verify the correct functioning of the validation logic.
This is not a breaking change, since user were not supposed to use gitea other than for local testing before 🙃

📝 Description of the Change

👨🏻‍ Linked Jira

Jira: https://issues.redhat.com/browse/SRVKP-10609

🔗 Linked GitHub Issue

Fixes #

🚀 Type of Change

  • 🐛 Bug fix (fix:)
  • ✨ New feature (feat:)
  • 💥 Breaking change (feat!:, fix!:)
  • 📚 Documentation update (docs:)
  • ⚙️ Chore (chore:)
  • 💅 Refactor (refactor:)
  • 🔧 Enhancement (enhance:)
  • 📦 Dependency update (deps:)

🧪 Testing Strategy

  • Unit tests
  • Integration tests
  • End-to-end tests
  • Manual testing
  • Not Applicable

🤖 AI Assistance

  • I have not used any AI assistance for this PR.
  • I have used AI assistance for this PR.

If you have used AI assistance, please provide the following details:

Which LLM was used?

  • GitHub Copilot
  • ChatGPT (OpenAI)
  • Claude (Anthropic)
  • Cursor
  • Gemini (Google)
  • Other: ____________

Extent of AI Assistance:

  • Documentation and research only
  • Unit tests or E2E tests only
  • Code generation (parts of the code)
  • Full code generation (most of the PR)
  • PR description and comments
  • Commit message(s)

Important

If the majority of the code in this PR was generated by an AI, please add a Co-authored-by trailer to your commit message.
For example:

Co-authored-by: Gemini gemini@google.com
Co-authored-by: ChatGPT noreply@chatgpt.com
Co-authored-by: Claude noreply@anthropic.com
Co-authored-by: Cursor noreply@cursor.com
Co-authored-by: Copilot Copilot@users.noreply.github.com

**💡You can use the script ./hack/add-llm-coauthor.sh to automatically add
these co-author trailers to your commits.

✅ Submitter Checklist

  • 📝 My commit messages are clear, informative, and follow the project's How to write a git commit message guide. The Gitlint linter ensures in CI it's properly validated
  • ✨ I have ensured my commit message prefix (e.g., fix:, feat:) matches the "Type of Change" I selected above.
  • ♽ I have run make test and make lint locally to check for and fix any
    issues. For an efficient workflow, I have considered installing
    pre-commit and running pre-commit install to
    automate these checks.
  • 📖 I have added or updated documentation for any user-facing changes.
  • 🧪 I have added sufficient unit tests for my co
    ...(truncated)

Copilot AI review requested due to automatic review settings January 28, 2026 13:59
@pipelines-as-code
Copy link
Copy Markdown

pipelines-as-code bot commented Jan 28, 2026

🔍 PR Lint Feedback

Note: This automated check helps ensure your PR follows our contribution guidelines.

⚠️ Items that need attention:

🤖 AI attribution

The following commits lack an explicit AI attribution footer:

  • e492f7b fix: Enforce webhook signature for Forgejo

If no AI assistance was used for a commit, you can ignore this warning.
Otherwise add an Assisted-by: or Co-authored-by: footer referencing the AI used.


ℹ️ Next Steps

  • Review and address the items above
  • Push new commits to update this PR
  • This comment will be automatically updated when issues are resolved
🔧 Admin Tools (click to expand)

Automated Issue/Ticket Creation:

  • /issue-create - Generate a GitHub issue from this PR content using AI
  • /jira-create - Create a SRVKP Jira ticket from this PR content using AI

⚠️ Important: Always review and edit generated content before finalizing tickets/issues.
The AI-generated content should be used as a starting point and may need adjustments.

These commands are available to maintainers and will post the generated content as PR comments for review.

🤖 This feedback was generated automatically by the PR CI system

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @chmouel, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the security posture of Gitea and Forgejo webhook integrations by introducing robust signature validation. By requiring and verifying cryptographic signatures on incoming webhook payloads, the system can now confidently ascertain that requests originate from trusted sources and have not been tampered with. This change hardens the application against potential spoofing attacks and ensures data integrity, backed by extensive test cases.

Highlights

  • Enhanced Webhook Security: Implemented mandatory signature validation for Gitea and Forgejo webhooks to ensure the authenticity and integrity of incoming requests.
  • HMAC-SHA256 Validation: Utilizes HMAC-SHA256 hashing to verify 'X-Forgejo-Signature' and 'X-Gitea-Signature' headers against a configured webhook secret.
  • Comprehensive Test Coverage: Added new unit and integration tests to thoroughly validate the signature checking logic under various scenarios, including valid, invalid, and missing signatures/secrets.
  • Non-Breaking Change: This security enhancement is designed to be non-breaking, as previous Gitea usage was primarily for local testing without strict validation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a critical security feature by enforcing webhook signature validation for Gitea and Forgejo, ensuring the authenticity of incoming requests. The implementation is robust, correctly using HMAC-SHA256 and constant-time comparison to prevent timing attacks. The error handling for missing signatures or secrets is clear and improves security posture. The changes are well-supported by a comprehensive set of unit tests that cover various success and failure scenarios. The updates to the E2E test framework to accommodate webhook secrets are also appropriate. I have one suggestion to improve the idempotency of the test setup.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements webhook signature validation for Gitea and Forgejo providers to enhance security by ensuring only authenticated webhook requests are processed. The implementation uses HMAC-SHA256 for signature verification and makes webhook secrets mandatory for all Gitea/Forgejo webhooks.

Changes:

  • Added signature validation logic in the Gitea provider's Validate method using HMAC-SHA256
  • Updated test infrastructure to support webhook secrets through environment variables
  • Added comprehensive unit tests covering multiple validation scenarios

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/provider/gitea/gitea.go Implements webhook signature validation using HMAC-SHA256 with constant-time comparison, enforcing mandatory webhook secrets
pkg/provider/gitea/gitea_test.go Adds comprehensive test coverage for validation scenarios including valid/invalid signatures, missing secrets, and malformed inputs
test/pkg/gitea/test.go Updates TestPR to retrieve webhook secret from TEST_EL_WEBHOOK_SECRET environment variable
test/pkg/gitea/scm.go Extends CreateGiteaRepo signature to accept webhookSecret parameter and configures it in webhook creation
test/pkg/gitea/crd.go Creates webhook-secret Kubernetes secret from TEST_EL_WEBHOOK_SECRET and configures it in the Repository CR

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +55
webhookSecret, _ := os.LookupEnv("TEST_EL_WEBHOOK_SECRET")
if err := secret.Create(ctx, topts.ParamsRun, map[string]string{"secret": webhookSecret}, ns, "webhook-secret"); err != nil {
return err
}
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

When TEST_EL_WEBHOOK_SECRET environment variable is not set, LookupEnv will return an empty string. This means the secret will be created with an empty value, and the webhook secret reference will still be set in the GitProvider spec. This could lead to confusing behavior where a secret is configured but empty. Consider checking if the webhook secret is empty and handling that case appropriately, or add a check to ensure TEST_EL_WEBHOOK_SECRET is set before proceeding.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

that's fine

Comment on lines +132 to +146
func (v *Provider) Validate(_ context.Context, _ *params.Run, event *info.Event) error {
signature := event.Request.Header.Get(ForgejoSignatureHeader)
if signature == "" {
signature = event.Request.Header.Get(GiteaSignatureHeader)
}
if signature == "" {
return fmt.Errorf("no signature has been detected, for security reason we are not allowing webhooks without a secret")
}

secret := event.Provider.WebhookSecret
if secret == "" {
return fmt.Errorf("no webhook secret has been set, in repository CR or secret")
}

return validateSignature(signature, event.Request.Payload, []byte(secret))
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

The documentation in docs/content/docs/dev/_index.md (lines 86-88) states that "Pipelines-as-Code detects a Gitea install and lets the user set an empty webhook secret (by default it's enforced)." However, this implementation now requires webhook secrets for all Gitea/Forgejo webhooks and returns an error if either the signature or secret is missing. This is a breaking change from the documented behavior. The documentation should be updated to reflect that webhook secrets are now mandatory for Gitea/Forgejo, or the implementation should be modified to match the documented behavior of allowing empty webhook secrets.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this will be fixed in subsequent pr after #2420

@chmouel chmouel force-pushed the srvkp-10609-forgejo-webhok-validation branch from a5641cd to f79d333 Compare January 28, 2026 14:28
@chmouel
Copy link
Copy Markdown
Member Author

chmouel commented Jan 28, 2026

/compliance --pr_compliance.enable_ticket_labels=true

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 28, 2026

PR Compliance Guide 🔍

(Compliance updated until commit f79d333)

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟢
🎫 #2422
🟢 Reject Forgejo/Gitea webhook requests when neither X-Forgejo-Signature nor
X-Gitea-Signature header is present.
Reject Forgejo/Gitea webhook requests when no webhook secret is configured for the
repository.
Validate webhook signatures by computing HMAC-SHA256 over the raw request payload using
the configured webhook secret and compare to the provided signature in constant time.
Add unit tests covering valid Forgejo header, valid Gitea header, signature mismatch,
invalid hex signature, missing secret, and missing signature.
Update Gitea/Forgejo E2E test setup to provision a webhook secret and configure created
webhooks/repositories to use it so end-to-end tests continue to pass with signature
enforcement.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing nil checks: Validate() dereferences event.Request and event.Provider without guarding against nil,
which can panic instead of returning a controlled error.

Referred Code
signature := event.Request.Header.Get(ForgejoSignatureHeader)
if signature == "" {
	signature = event.Request.Header.Get(GiteaSignatureHeader)
}
if signature == "" {
	return fmt.Errorf("no signature has been detected, for security reason we are not allowing webhooks without a secret")
}

secret := event.Provider.WebhookSecret
if secret == "" {
	return fmt.Errorf("no webhook secret has been set, in repository CR or secret")
}

return validateSignature(signature, event.Request.Payload, []byte(secret))

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated event fields: The new webhook signature enforcement does not validate that event, event.Request, and
event.Provider are non-nil before reading headers/payload/secret, risking crashes on
malformed external input.

Referred Code
signature := event.Request.Header.Get(ForgejoSignatureHeader)
if signature == "" {
	signature = event.Request.Header.Get(GiteaSignatureHeader)
}
if signature == "" {
	return fmt.Errorf("no signature has been detected, for security reason we are not allowing webhooks without a secret")
}

secret := event.Provider.WebhookSecret
if secret == "" {
	return fmt.Errorf("no webhook secret has been set, in repository CR or secret")
}

return validateSignature(signature, event.Request.Payload, []byte(secret))

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit f79d333
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing env validation: The webhook secret is read from TEST_EL_WEBHOOK_SECRET without validating that it is
set/non-empty before creating webhook-secret, which can lead to empty/invalid secrets and
unclear failures downstream.

Referred Code
webhookSecret, _ := os.LookupEnv("TEST_EL_WEBHOOK_SECRET")
if err := secret.Create(ctx, topts.ParamsRun, map[string]string{"secret": webhookSecret}, ns, webhookSecretName); err != nil {
	return err
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Signature format assumption: The new validation assumes the X-Forgejo-Signature/X-Gitea-Signature header is raw hex
HMAC-SHA256 (decoded via hex.DecodeString), which should be confirmed against actual
Forgejo/Gitea webhook signature formats to avoid rejecting valid requests.

Referred Code
func (v *Provider) Validate(_ context.Context, _ *params.Run, event *info.Event) error {
	signature := event.Request.Header.Get(ForgejoSignatureHeader)
	if signature == "" {
		signature = event.Request.Header.Get(GiteaSignatureHeader)
	}
	if signature == "" {
		return fmt.Errorf("no signature has been detected, for security reason we are not allowing webhooks without a secret")
	}

	secret := event.Provider.WebhookSecret
	if secret == "" {
		return fmt.Errorf("no webhook secret has been set, in repository CR or secret")
	}

	return validateSignature(signature, event.Request.Payload, []byte(secret))
}

func validateSignature(signature string, payload, secret []byte) error {
	signatureBytes, err := hex.DecodeString(signature)
	if err != nil {
		return fmt.Errorf("gitea/forgejo webhook signature is not valid hex: %w", err)


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update

@chmouel chmouel linked an issue Jan 28, 2026 that may be closed by this pull request
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 4, 2026

PR-Agent failed to apply 'global' repo settings

The configuration file needs to be a valid TOML, please fix it.


Error message:
cannot access local variable 'file_data' where it is not associated with a value

Configuration content:
[github_app]
pr_commands = [ "/review" ]

[pr_reviewer]
enable_review_labels_security = true
enable_chat_in_code_suggestions=false
enable_chat_in_code_suggestions = false
enable_summary=false
enable_help_text=false

[pr_description]
publish_description_as_comment = true

[pr_code_suggestions]
commitable_code_suggestions = true

[pr_test]
num_tests = 0

@chmouel
Copy link
Copy Markdown
Member Author

chmouel commented Feb 5, 2026

/retest

@chmouel
Copy link
Copy Markdown
Member Author

chmouel commented Feb 5, 2026

/test go-testing

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 5, 2026

PR-Agent: could not fine a component named go-testing in a supported language in this PR.

@chmouel chmouel force-pushed the srvkp-10609-forgejo-webhok-validation branch 3 times, most recently from bfed013 to 400ce4f Compare February 10, 2026 09:33
@chmouel
Copy link
Copy Markdown
Member Author

chmouel commented Feb 11, 2026

/retest

Implemented signature validation for Gitea and Forgejo webhooks to
ensure the authenticity of incoming requests. This involves checking the
`X-Forgejo-Signature` and `X-Gitea-Signature` headers against a computed
HMAC-SHA256 hash of the request payload using a configured webhook
secret.

The validation ensures that only requests originating from a trusted
source with the correct secret are processed, enhancing the security of
webhook integrations. This change also includes necessary test cases to
verify the correct functioning of the validation logic.

Jira: https://issues.redhat.com/browse/SRVKP-10609
Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
@chmouel chmouel force-pushed the srvkp-10609-forgejo-webhok-validation branch 2 times, most recently from ac20034 to e492f7b Compare February 11, 2026 19:56
@chmouel
Copy link
Copy Markdown
Member Author

chmouel commented Feb 12, 2026

/retest

@chmouel chmouel merged commit 404fc85 into tektoncd:main Feb 12, 2026
13 checks passed
@chmouel chmouel deleted the srvkp-10609-forgejo-webhok-validation branch February 12, 2026 08:45
@chmouel chmouel restored the srvkp-10609-forgejo-webhok-validation branch February 19, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce HMAC signature validation for Forgejo/Gitea webhooks

3 participants