penguin/webhook-freedns

freedns webhook for cert-manager

commit ab1d9c2ad266eaa1b384e00dce766c7191be2c48

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-05-21T17:10:40Z
subject2026.05.21.01
commit ab1d9c2ad266eaa1b384e00dce766c7191be2c48
Merge: f1597a6 147f980
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-05-21T17:10:40Z

    2026.05.21.01
    
    Merge remote-tracking branch 'certman/master'

 .gitignore                         |   7 +-
 Dockerfile                         |  29 +--
 Makefile                           |  74 +++++--
 OWNERS                             |   8 +-
 README.md                          |  11 +-
 build.env                          |   4 +
 data.json                          |   1 +
 deploy/freedns-webhook/values.yaml |  18 +-
 freedns/buildinfo_gen.go           |   6 +
 freedns/freedns.go                 |  17 +-
 go.mod                             | 160 +++++++--------
 go.sum                             | 389 +++++++++++++++++++------------------
 image.Makefile                     |  31 ++-
 main.go                            |  14 +-
 renovate.json                      |   6 +
 15 files changed, 455 insertions(+), 320 deletions(-)

diff --cc .gitignore
index a41ba7b,646a48d..ce170fe
--- a/.gitignore
+++ b/.gitignore
@@@ -12,8 -13,9 +13,12 @@@
  *.out
  
  # Ignore the built binary
 -cert-manager-webhook-example
 +cert-manager-webhook-freedns
 +
++# dev-local build settings
++*.work
+ 
  # Make artifacts
+ bin
  _out
  _test
diff --cc Dockerfile
index c92f9fb,22ff7fb..d3ee0e1
--- a/Dockerfile
+++ b/Dockerfile
@@@ -1,24 -1,24 +1,29 @@@
- FROM golang:1.22-alpine3.19 AS build_deps
 -FROM golang:1.23-alpine3.19@sha256:5f3336882ad15d10ac1b59fbaba7cb84c35d4623774198b36ae60edeba45fd84 AS build_deps
++# syntax=docker/dockerfile:1.7
  
--RUN apk add --no-cache git
++FROM --platform=$BUILDPLATFORM golang:1.26-alpine3.23 AS build
  
  WORKDIR /workspace
  
--COPY go.mod .
--COPY go.sum .
++ARG TARGETOS
++ARG TARGETARCH
  
--RUN go mod download
++COPY go.mod go.sum ./
  
--FROM build_deps AS build
++RUN --mount=type=cache,target=/go/pkg/mod \
++    go mod download
  
  COPY . .
  
--RUN CGO_ENABLED=0 go build -o webhook -ldflags '-w -extldflags "-static"' .
++RUN --mount=type=cache,target=/go/pkg/mod \
++    --mount=type=cache,target=/root/.cache/go-build \
++    CGO_ENABLED=0 \
++    GOOS=$TARGETOS \
++    GOARCH=$TARGETARCH \
++    go build -trimpath -o /out/webhook -ldflags='-s -w' .
  
- FROM alpine:3.18
 -FROM alpine:3.23@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11
++FROM scratch
  
--RUN apk add --no-cache ca-certificates
++COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
++COPY --from=build /out/webhook /usr/local/bin/webhook
  
--COPY --from=build /workspace/webhook /usr/local/bin/webhook
--
--ENTRYPOINT ["webhook"]
++ENTRYPOINT ["/usr/local/bin/webhook"]
diff --cc Makefile
index 6fbb534,27d294d..ad1b8d2
--- a/Makefile
+++ b/Makefile
@@@ -7,25 -7,22 +7,22 @@@ IMAGE_TAG := "latest
  
  OUT := $(shell pwd)/_out
  
- KUBEBUILDER_VERSION=1.28.0
+ # FIXME: Required to set the environment variables below. Remove when fixed.
+ ENVTEST_K8S_VERSION=1.35.0
  
 -HELM_FILES := $(shell find deploy/example-webhook)
 +HELM_FILES := $(shell find deploy/freedns-webhook)
  
- test: _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/etcd _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kube-apiserver _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kubectl
- 	TEST_ASSET_ETCD=_test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/etcd \
- 	TEST_ASSET_KUBE_APISERVER=_test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kube-apiserver \
- 	TEST_ASSET_KUBECTL=_test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kubectl \
+ # FIXME: The environment variables are required by the test helper in cert-manager, but not required to run the tests.
+ test: setup-envtest
+ 	TEST_ASSET_ETCD=$(LOCALBIN)/k8s/$(ENVTEST_K8S_VERSION)-$(OS)-$(ARCH)/etcd \
+ 	TEST_ASSET_KUBE_APISERVER=$(LOCALBIN)/k8s/$(ENVTEST_K8S_VERSION)-$(OS)-$(ARCH)/kube-apiserver \
+ 	TEST_ASSET_KUBECTL=$(LOCALBIN)/k8s/$(ENVTEST_K8S_VERSION)-$(OS)-$(ARCH)/kubectl \
  	$(GO) test -v .
  
- _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH).tar.gz: | _test
- 	curl -fsSL https://go.kubebuilder.io/test-tools/$(KUBEBUILDER_VERSION)/$(OS)/$(ARCH) -o $@
- 
- _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/etcd _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kube-apiserver _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)/kubectl: _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH).tar.gz | _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH)
- 	tar xfO $< kubebuilder/bin/$(notdir $@) > $@ && chmod +x $@
- 
  .PHONY: clean
  clean:
- 	rm -r _test $(OUT)
+ 	chmod -R u+w $(LOCALBIN) $(OUT) 2>/dev/null || true
+ 	rm -rf $(LOCALBIN) $(OUT)
  
  .PHONY: build
  build:
@@@ -36,10 -33,59 +33,59 @@@ rendered-manifest.yaml: $(OUT)/rendered
  
  $(OUT)/rendered-manifest.yaml: $(HELM_FILES) | $(OUT)
  	helm template \
 -	    --name example-webhook \
 +	    --name freedns-webhook \
              --set image.repository=$(IMAGE_NAME) \
              --set image.tag=$(IMAGE_TAG) \
 -            deploy/example-webhook > $@
 +            deploy/freedns-webhook > $@
  
- _test $(OUT) _test/kubebuilder-$(KUBEBUILDER_VERSION)-$(OS)-$(ARCH):
- 	mkdir -p $@
+ ## Location to install dependencies to
+ LOCALBIN ?= $(shell pwd)/bin
+ $(LOCALBIN):
+ 	mkdir -p "$(LOCALBIN)"
+ 
+ ## Tool Binaries
+ 
+ ENVTEST ?= $(LOCALBIN)/setup-envtest
+ 
+ #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
+ ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
+   [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
+   printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')
+ 
+ #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
+ ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
+   [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
+   printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
+ 
+ .PHONY: setup-envtest
+ setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
+ 	@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
+ 	@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
+ 		echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
+ 		exit 1; \
+ 	}
+ 
+ .PHONY: envtest
+ envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
+ $(ENVTEST): $(LOCALBIN)
+ 	$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
+ 
+ # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
+ # $1 - target path with name of binary
+ # $2 - package url which can be installed
+ # $3 - specific version of package
+ define go-install-tool
+ @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
+ set -e; \
+ package=$(2)@$(3) ;\
+ echo "Downloading $${package}" ;\
+ rm -f "$(1)" ;\
+ GOBIN="$(LOCALBIN)" go install $${package} ;\
+ mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
+ } ;\
+ ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
+ endef
+ 
+ define gomodver
+ $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
+ endef
diff --cc README.md
index ab2b930,24ec32c..b54245e
--- a/README.md
+++ b/README.md
@@@ -1,56 -1,63 +1,63 @@@
 -<p align="center">
 -  <img src="https://raw.githubusercontent.com/cert-manager/cert-manager/d53c0b9270f8cd90d908460d69502694e1838f5f/logo/logo-small.png" height="256" width="256" alt="cert-manager project logo" />
 -</p>
 +# Introduction
 +First, [RTFM](https://cert-manager.io/docs/configuration/acme/dns01/).
  
 -# ACME webhook example
 +Have you read it? If you haven't go read it. Cuz I'll keep everything short.
  
 -The ACME issuer type supports an optional 'webhook' solver, which can be used
 -to implement custom DNS01 challenge solving logic.
 +This is a dns01 solver for [FreeDNS](https://freedns.afraid.org/).
  
 -This is useful if you need to use cert-manager with a DNS provider that is not
 -officially supported in cert-manager core.
 -
 -## Why not in core?
 -
 -As the project & adoption has grown, there has been an influx of DNS provider
 -pull requests to our core codebase. As this number has grown, the test matrix
 -has become un-maintainable and so, it's not possible for us to certify that
 -providers work to a sufficient level.
 -
 -By creating this 'interface' between cert-manager and DNS providers, we allow
 -users to quickly iterate and test out new integrations, and then packaging
 -those up themselves as 'extensions' to cert-manager.
 -
 -We can also then provide a standardised 'testing framework', or set of
 -conformance tests, which allow us to validate that a DNS provider works as
 -expected.
 -
 -## Creating your own webhook
 -
 -Webhook's themselves are deployed as Kubernetes API services, in order to allow
 -administrators to restrict access to webhooks with Kubernetes RBAC.
 -
 -This is important, as otherwise it'd be possible for anyone with access to your
 -webhook to complete ACME challenge validations and obtain certificates.
 -
 -To make the set up of these webhook's easier, we provide a template repository
 -that can be used to get started quickly.
 -
 -When implementing your webhook, you should set the `groupName` in the
 -[values.yml](deploy/example-webhook/values.yaml) of your chart to a domain name that 
 -you - as the webhook-author - own. It should not need to be adjusted by the users of
 -your chart.
 -
 -### Creating your own repository
 -
 -### Running the test suite
 -
 -All DNS providers **must** run the DNS01 provider conformance testing suite,
 -else they will have undetermined behaviour when used with cert-manager.
 -
 -**It is essential that you configure and run the test suite when creating a
 -DNS01 webhook.**
 -
 -An example Go test file has been provided in [main_test.go](https://github.com/cert-manager/webhook-example/blob/master/main_test.go).
 -
 -You can run the test suite with:
 +Pull requests welcome. I'm now somewhat familiar with golang. You can also look at
 +other and choose the one that fits your need.
  
 +## Install
  ```bash
 -$ TEST_ZONE_NAME=example.com. make test
 +$ cd deploy
 +$ helm show values freedns-webhook > my-values.yaml
 +$ edit my-values.yaml
 +$ helm install -n cert-manager [INSTALLATION_NAME] freedns-webhook/ -f my-values.yaml
 +```
 +
 +## ClusterIssuer for Let's encrypt staging
 +```yaml
 +apiVersion: cert-manager.io/v1
 +kind: ClusterIssuer
 +metadata:
 +  name: letsencrypt-staging
 +spec:
 +  acme:
 +    email: myemail@example.com
 +    server: https://acme-staging-v02.api.letsencrypt.org/directory
 +    privateKeySecretRef:
 +      name: le-staging
 +    solvers:
 +    - dns01:
 +        webhook:
 +          groupName: acme.freedns.afraid.org
 +          solverName: freedns-solver
 +          config:
 +            secretName: freedns-auth
  ```
  
 -The example file has a number of areas you must fill in and replace with your
 -own options in order for tests to pass.
 +## FreeDNS webhook settings
 +Normally if you haven't changed anything, the default namespace should be
 +`cert-manager`. It should be within the same namespace for the webhook when
 +you do `helm install webhook -n cert-manager`.
 +
 +Additionally, the following names can be customized
 +* acme.freedns.afraid.org
 +
 +### UPDATE
- 2024-10-30
++## 2026-05-21
++- Merged the latest upstream changes.
++- Added `SOCKS5_PROXY` support to work around FreeDNS IP-based blocking.
++- Added Helm values support for pinning container images by digest.
++- Fixed the logging implementation to use cert-manager’s logging infrastructure correctly.
++- Moved the default container image registry from Docker Hub to GHCR.
++
++## 2024-10-30
 +- Merged from upstream, now works on 1.31 cluster
 +
- 2024-11-02
++## 2024-11-02
 +- Webhook will now properly logs its actions
 +- Removed permissions to read secrets from pod for obvious reansons
 +  - Authentication details are now requested from Helm
 +  - You should remove the old secret `freedns-auth`. It is now handled by Helm.
diff --cc build.env
index 0000000,0000000..64e6d22
new file mode 100644
--- /dev/null
+++ b/build.env
@@@ -1,0 -1,0 +1,4 @@@
++IMAGE_NAME=ghcr.io/tgckpg/cert-manager-webhook-freedns
++IMAGE_TAG=2026.05.21.01
++
++BUILDX_BUILDER=container-builder
diff --cc data.json
index 0000000,0000000..44ffc8d
new file mode 100644
--- /dev/null
+++ b/data.json
@@@ -1,0 -1,0 +1,1 @@@
++{"props":{"resources":{"repository":[{"name":"actions","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Workflows, workflow runs and artifacts.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-actions","fgp":["Read-only","Read and write"],"human_name":"actions","resource_group":"#repository-permissions-for-actions","title":"Actions"}},{"name":"administration","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Repository creation, deletion, settings, teams, and collaborators.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-administration","fgp":["Read-only","Read and write"],"human_name":"administration","resource_group":"#repository-permissions-for-administration","title":"Administration"}},{"name":"agent_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage agent repository secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-agent-secrets","fgp":["Read-only","Read and write"],"human_name":"agent secrets","resource_group":"#repository-permissions-for-agent-secrets","title":"Agent secrets"}},{"name":"agent_tasks","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View, create, and manage agent tasks in a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-agent-tasks","fgp":["Read-only","Read and write"],"human_name":"agent tasks","resource_group":"#repository-permissions-for-agent-tasks","title":"Agent tasks"}},{"name":"agent_variables","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage agent repository variables.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-agent-variables","fgp":["Read-only","Read and write"],"human_name":"agent variables","resource_group":"#repository-permissions-for-agent-variables","title":"Agent variables"}},{"name":"artifact_metadata","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create and retrieve artifact metadata for a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-artifact-metadata","fgp":["Read-only","Read and write"],"human_name":"artifact metadata","resource_group":"#repository-permissions-for-artifact-metadata","title":"Artifact metadata"}},{"name":"attestations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create and retrieve attestations for a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-attestations","fgp":["Read-only","Read and write"],"human_name":"attestations api","resource_group":"#repository-permissions-for-attestations","title":"Attestations"}},{"name":"code_quality","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Access and manage code quality data for a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-code-quality","fgp":["Read-only","Read and write"],"human_name":"code quality","resource_group":"#repository-permissions-for-code-quality","title":"Code quality"}},{"name":"security_events","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage code scanning alerts.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-code-scanning-alerts","fgp":["Read-only","Read and write"],"human_name":"security events","resource_group":"#repository-permissions-for-code-scanning-alerts","title":"Code scanning alerts"}},{"name":"codespaces","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create, edit, delete and list Codespaces.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-codespaces","fgp":["Read-only","Read and write"],"human_name":"codespaces","resource_group":"#repository-permissions-for-codespaces","title":"Codespaces"}},{"name":"codespaces_lifecycle_admin","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage the lifecycle of Codespaces, including starting and stopping.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-codespaces-lifecycle-admin","fgp":["Read-only","Read and write"],"human_name":"codespaces lifecycle admin","resource_group":"#repository-permissions-for-codespaces-lifecycle-admin","title":"Codespaces lifecycle admin"}},{"name":"codespaces_metadata","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Access Codespaces metadata including the devcontainers and machine type.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-codespaces-metadata","fgp":["Read-only"],"human_name":"codespaces metadata","resource_group":"#repository-permissions-for-codespaces-metadata","title":"Codespaces metadata"}},{"name":"codespaces_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Restrict Codespaces user secrets modifications to specific repositories.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-codespaces-secrets","fgp":["Read and write"],"human_name":"codespaces secrets","resource_group":"#repository-permissions-for-codespaces-secrets","title":"Codespaces secrets"}},{"name":"statuses","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Commit statuses.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-commit-statuses","fgp":["Read-only","Read and write"],"human_name":"commit statuses","resource_group":"#repository-permissions-for-commit-statuses","title":"Commit statuses"}},{"name":"contents","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Repository contents, commits, branches, downloads, releases, and merges.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-contents","fgp":["Read-only","Read and write"],"human_name":"code","resource_group":"#repository-permissions-for-contents","title":"Contents"}},{"name":"copilot_agent_settings","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage Copilot cloud agent settings for a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-copilot-agent-settings","fgp":["Read-only","Read and write"],"human_name":"copilot agent settings","resource_group":"#repository-permissions-for-copilot-agent-settings","title":"Copilot agent settings"}},{"name":"repository_custom_properties","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Read and write repository custom properties values at the repository level, when allowed by the property.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-custom-properties","fgp":["Read-only","Read and write"],"human_name":"custom properties for repositories","resource_group":"#repository-permissions-for-custom-properties","title":"Custom properties"}},{"name":"vulnerability_alerts","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Retrieve Dependabot alerts.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-dependabot-alerts","fgp":["Read-only","Read and write"],"human_name":"Dependabot alerts","resource_group":"#repository-permissions-for-dependabot-alerts","title":"Dependabot alerts"}},{"name":"dependabot_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Dependabot repository secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-dependabot-secrets","fgp":["Read-only","Read and write"],"human_name":"dependabot secrets","resource_group":"#repository-permissions-for-dependabot-secrets","title":"Dependabot secrets"}},{"name":"deployments","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Deployments and deployment statuses.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-deployments","fgp":["Read-only","Read and write"],"human_name":"deployments","resource_group":"#repository-permissions-for-deployments","title":"Deployments"}},{"name":"discussions","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Discussions and related comments and labels.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-discussions","fgp":["Read-only","Read and write"],"human_name":"discussions","resource_group":"#repository-permissions-for-discussions","title":"Discussions"}},{"name":"environments","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage repository environments.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-environments","fgp":["Read-only","Read and write"],"human_name":"environments","resource_group":"#repository-permissions-for-environments","title":"Environments"}},{"name":"issues","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Issues and related comments, assignees, labels, and milestones.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-issues","fgp":["Read-only","Read and write"],"human_name":"issues","resource_group":"#repository-permissions-for-issues","title":"Issues"}},{"name":"merge_queues","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage a repository's merge queues","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-merge-queues","fgp":["Read-only","Read and write"],"human_name":"merge queues","resource_group":"#repository-permissions-for-merge-queues","title":"Merge queues"}},{"name":"metadata","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Search repositories, list collaborators, and access repository metadata.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-metadata","fgp":["Read-only"],"human_name":"metadata","resource_group":"#repository-permissions-for-metadata","title":"Metadata"}},{"name":"pages","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Retrieve Pages statuses, configuration, and builds, as well as create new builds.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-pages","fgp":["Read-only","Read and write"],"human_name":"pages","resource_group":"#repository-permissions-for-pages","title":"Pages"}},{"name":"pull_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Pull requests and related comments, assignees, labels, milestones, and merges.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-pull-requests","fgp":["Read-only","Read and write"],"human_name":"pull requests","resource_group":"#repository-permissions-for-pull-requests","title":"Pull requests"}},{"name":"repository_advisories","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage repository security advisories.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-repository-security-advisories","fgp":["Read-only","Read and write"],"human_name":"repository advisories","resource_group":"#repository-permissions-for-repository-security-advisories","title":"Repository security advisories"}},{"name":"repo_secret_scanning_dismissal_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage secret scanning alert dismissal requests","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-secret-scanning-alert-dismissal-requests","fgp":["Read-only","Read and write"],"human_name":"secret scanning alert dismissal requests","resource_group":"#repository-permissions-for-secret-scanning-alert-dismissal-requests","title":"Secret scanning alert dismissal requests"}},{"name":"secret_scanning_alerts","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage secret scanning alerts.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-secret-scanning-alerts","fgp":["Read-only","Read and write"],"human_name":"secret scanning alerts","resource_group":"#repository-permissions-for-secret-scanning-alerts","title":"Secret scanning alerts"}},{"name":"secret_scanning_bypass_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Review and manage repository secret scanning push protection bypass requests.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-secret-scanning-push-protection-bypass-requests","fgp":["Read-only","Read and write"],"human_name":"secret scanning push protection bypass requests","resource_group":"#repository-permissions-for-secret-scanning-push-protection-bypass-requests","title":"Secret scanning push protection bypass requests"}},{"name":"secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Actions repository secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-secrets","fgp":["Read-only","Read and write"],"human_name":"secrets","resource_group":"#repository-permissions-for-secrets","title":"Secrets"}},{"name":"actions_variables","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Actions repository variables.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-variables","fgp":["Read-only","Read and write"],"human_name":"actions variables","resource_group":"#repository-permissions-for-variables","title":"Variables"}},{"name":"repository_hooks","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage the post-receive hooks for a repository.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-webhooks","fgp":["Read-only","Read and write"],"human_name":"repository hooks","resource_group":"#repository-permissions-for-webhooks","title":"Webhooks"}},{"name":"workflows","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Update GitHub Action workflow files.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#repository-permissions-for-workflows","fgp":["Read and write"],"human_name":"workflows","resource_group":"#repository-permissions-for-workflows","title":"Workflows"}}],"organization":[{"name":"organization_api_insights","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View statistics on how the API is being used for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-api-insights","fgp":["Read-only"],"human_name":"organization API insights","resource_group":"#organization-permissions-for-api-insights","title":"API Insights"}},{"name":"organization_administration","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage access to an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-administration","fgp":["Read-only","Read and write"],"human_name":"organization administration","resource_group":"#organization-permissions-for-administration","title":"Administration"}},{"name":"organization_agent_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage agent organization secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-agent-secrets","fgp":["Read-only","Read and write"],"human_name":"organization agent secrets","resource_group":"#organization-permissions-for-agent-secrets","title":"Agent secrets"}},{"name":"organization_agent_variables","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage agent organization variables.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-agent-variables","fgp":["Read-only","Read and write"],"human_name":"organization agent variables","resource_group":"#organization-permissions-for-agent-variables","title":"Agent variables"}},{"name":"organization_user_blocking","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage users blocked by the organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-blocking-users","fgp":["Read-only","Read and write"],"human_name":"organization user blocking","resource_group":"#organization-permissions-for-blocking-users","title":"Blocking users"}},{"name":"organization_campaigns","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage campaigns.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-campaigns","fgp":["Read-only","Read and write"],"human_name":"organization campaigns","resource_group":"#organization-permissions-for-campaigns","title":"Campaigns"}},{"name":"organization_copilot_spaces","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage Copilot Spaces for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-copilot-spaces","fgp":["Read-only","Read and write"],"human_name":"organization copilot spaces","resource_group":"#organization-permissions-for-copilot-spaces","title":"Copilot Spaces"}},{"name":"organization_copilot_agent_settings","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Copilot cloud agent settings for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-copilot-agent-settings","fgp":["Read-only","Read and write"],"human_name":"organization copilot agent settings","resource_group":"#organization-permissions-for-copilot-agent-settings","title":"Copilot agent settings"}},{"name":"org_copilot_content_exclusion","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Copilot content exclusion rules for the organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-copilot-content-exclusion","fgp":["Read-only","Read and write"],"human_name":"organization Copilot content exclusion","resource_group":"#organization-permissions-for-copilot-content-exclusion","title":"Copilot content exclusion"}},{"name":"organization_custom_org_roles","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create, edit, delete and list custom organization roles. View system organization roles.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-custom-organization-roles","fgp":["Read-only","Read and write"],"human_name":"custom organization roles","resource_group":"#organization-permissions-for-custom-organization-roles","title":"Custom organization roles"}},{"name":"organization_custom_properties","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Read and write repository custom properties values and administer definitions at the organization level.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-custom-properties","fgp":["Read-only","Read and write","Admin"],"human_name":"organization custom properties for repositories","resource_group":"#organization-permissions-for-custom-properties","title":"Custom properties"}},{"name":"custom_properties_for_organizations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Read and write organization custom properties values at the organization level, when allowed by the property.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-custom-properties-for-organizations","fgp":["Read-only","Read and write"],"human_name":"custom properties for organizations","resource_group":"#organization-permissions-for-custom-properties-for-organizations","title":"Custom properties for organizations"}},{"name":"organization_custom_roles","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create, edit, delete and list custom repository roles.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-custom-repository-roles","fgp":["Read-only","Read and write"],"human_name":"custom repository roles","resource_group":"#organization-permissions-for-custom-repository-roles","title":"Custom repository roles"}},{"name":"organization_events","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View events triggered by an activity in an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-events","fgp":["Read-only"],"human_name":"organization events","resource_group":"#organization-permissions-for-events","title":"Events"}},{"name":"organization_copilot_seat_management","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Copilot Business seats and settings","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-github-copilot-business","fgp":["Read-only","Read and write"],"human_name":"organization copilot seat management","resource_group":"#organization-permissions-for-github-copilot-business","title":"GitHub Copilot Business"}},{"name":"organization_runner_custom_images","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage hosted runner custom images available to an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-hosted-runner-custom-images","fgp":["Read-only","Read and write"],"human_name":"organization runner custom images","resource_group":"#organization-permissions-for-hosted-runner-custom-images","title":"Hosted runner custom images"}},{"name":"issue_fields","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage issue fields for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-issue-fields","fgp":["Read-only","Read and write"],"human_name":"issue fields","resource_group":"#organization-permissions-for-issue-fields","title":"Issue Fields"}},{"name":"issue_types","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage issue types for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-issue-types","fgp":["Read-only","Read and write"],"human_name":"issue types","resource_group":"#organization-permissions-for-issue-types","title":"Issue Types"}},{"name":"members","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Organization members and teams.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-members","fgp":["Read-only","Read and write"],"human_name":"members","resource_group":"#organization-permissions-for-members","title":"Members"}},{"name":"organization_models","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage model access for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-models","fgp":["Read-only"],"human_name":"models","resource_group":"#organization-permissions-for-models","title":"Models"}},{"name":"organization_network_configurations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage hosted compute network configurations available to an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-network-configurations","fgp":["Read-only","Read and write"],"human_name":"organization network configurations","resource_group":"#organization-permissions-for-network-configurations","title":"Network configurations"}},{"name":"organization_copilot_metrics","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View organization Copilot metrics.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-copilot-metrics","fgp":["Read-only"],"human_name":"organization copilot metrics","resource_group":"#organization-permissions-for-organization-copilot-metrics","title":"Organization Copilot metrics"}},{"name":"organization_announcement_banners","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and modify announcement banners for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-announcement-banners","fgp":["Read-only","Read and write"],"human_name":"organization announcement banners","resource_group":"#organization-permissions-for-organization-announcement-banners","title":"Organization announcement banners"}},{"name":"organization_secret_scanning_bypass_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Review and manage secret scanning push protection bypass requests.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-bypass-requests-for-secret-scanning","fgp":["Read-only","Read and write"],"human_name":"organization secret scanning push protection bypass requests","resource_group":"#organization-permissions-for-organization-bypass-requests-for-secret-scanning","title":"Organization bypass requests for secret scanning"}},{"name":"organization_codespaces","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Codespaces for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-codespaces","fgp":["Read-only","Read and write"],"human_name":"organization codespaces","resource_group":"#organization-permissions-for-organization-codespaces","title":"Organization codespaces"}},{"name":"organization_codespaces_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Codespaces Secrets for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-codespaces-secrets","fgp":["Read-only","Read and write"],"human_name":"organization codespaces secrets","resource_group":"#organization-permissions-for-organization-codespaces-secrets","title":"Organization codespaces secrets"}},{"name":"organization_codespaces_settings","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Codespaces settings for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-codespaces-settings","fgp":["Read-only","Read and write"],"human_name":"organization codespaces settings","resource_group":"#organization-permissions-for-organization-codespaces-settings","title":"Organization codespaces settings"}},{"name":"organization_credentials","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage organization credentials","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-credentials","fgp":["Read-only","Read and write"],"human_name":"organization credentials","resource_group":"#organization-permissions-for-organization-credentials","title":"Organization credentials"}},{"name":"organization_dependabot_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Dependabot organization secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-dependabot-secrets","fgp":["Read-only","Read and write"],"human_name":"organization dependabot secrets","resource_group":"#organization-permissions-for-organization-dependabot-secrets","title":"Organization dependabot secrets"}},{"name":"organization_dependabot_dismissal_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Review and manage Dependabot alert dismissal requests.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-dismissal-requests-for-dependabot","fgp":["Read-only","Read and write"],"human_name":"organization Dependabot dismissal requests","resource_group":"#organization-permissions-for-organization-dismissal-requests-for-dependabot","title":"Organization dismissal requests for Dependabot"}},{"name":"organization_code_scanning_dismissal_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Review and manage code scanning alert dismissal requests.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-dismissal-requests-for-code-scanning","fgp":["Read-only","Read and write"],"human_name":"organization code scanning dismissal requests","resource_group":"#organization-permissions-for-organization-dismissal-requests-for-code-scanning","title":"Organization dismissal requests for code scanning"}},{"name":"organization_private_registries","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage private registries for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-organization-private-registries","fgp":["Read-only","Read and write"],"human_name":"organization private registries","resource_group":"#organization-permissions-for-organization-private-registries","title":"Organization private registries"}},{"name":"organization_plan","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View an organization's plan.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-plan","fgp":["Read-only"],"human_name":"organization plan","resource_group":"#organization-permissions-for-plan","title":"Plan"}},{"name":"organization_projects","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage projects for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-projects","fgp":["Read-only","Read and write","Admin"],"human_name":"organization projects","resource_group":"#organization-permissions-for-projects","title":"Projects"}},{"name":"secret_scanning_dismissal_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Review and manage secret scanning alert dismissal requests","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-secret-scanning-alert-dismissal-requests","fgp":["Read-only","Read and write"],"human_name":"secret scanning alert dismissal requests","resource_group":"#organization-permissions-for-secret-scanning-alert-dismissal-requests","title":"Secret scanning alert dismissal requests"}},{"name":"organization_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Actions organization secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-secrets","fgp":["Read-only","Read and write"],"human_name":"organization secrets","resource_group":"#organization-permissions-for-secrets","title":"Secrets"}},{"name":"organization_self_hosted_runners","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage Actions self-hosted runners available to an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-self-hosted-runners","fgp":["Read-only","Read and write"],"human_name":"organization self hosted runners","resource_group":"#organization-permissions-for-self-hosted-runners","title":"Self-hosted runners"}},{"name":"organization_actions_variables","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Actions organization variables.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-variables","fgp":["Read-only","Read and write"],"human_name":"organization actions variables","resource_group":"#organization-permissions-for-variables","title":"Variables"}},{"name":"organization_hooks","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage the post-receive hooks for an organization.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#organization-permissions-for-webhooks","fgp":["Read-only","Read and write"],"human_name":"organization hooks","resource_group":"#organization-permissions-for-webhooks","title":"Webhooks"}}],"user":[{"name":"blocking","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage users blocked by the user.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-block-another-user","fgp":["Read-only","Read and write"],"human_name":"blocking","resource_group":"#user-permissions-for-block-another-user","title":"Block another user"}},{"name":"codespaces_user_secrets","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage Codespaces user secrets.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-codespaces-user-secrets","fgp":["Read-only","Read and write"],"human_name":"codespaces user secrets","resource_group":"#user-permissions-for-codespaces-user-secrets","title":"Codespaces user secrets"}},{"name":"copilot_messages","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"This application will receive your GitHub ID, your GitHub Copilot Chat session messages (not including messages sent to another application), and timestamps of provided GitHub Copilot Chat session messages. This permission must be enabled for Copilot Extensions.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-copilot-chat","fgp":["Read-only"],"human_name":"Copilot Chat","resource_group":"#user-permissions-for-copilot-chat","title":"Copilot Chat"}},{"name":"copilot_editor_context","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"This application will receive bits of Editor Context (e.g. currently opened file) whenever you send it a message through Copilot Chat.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-copilot-editor-context","fgp":["Read-only"],"human_name":"Copilot Editor Context","resource_group":"#user-permissions-for-copilot-editor-context","title":"Copilot Editor Context"}},{"name":"user_copilot_requests","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Send Copilot requests.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-copilot-requests","fgp":["Read-only"],"human_name":"user copilot requests","resource_group":"#user-permissions-for-copilot-requests","title":"Copilot Requests"}},{"name":"emails","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage a user's email addresses.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-email-addresses","fgp":["Read-only","Read and write"],"human_name":"email addresses","resource_group":"#user-permissions-for-email-addresses","title":"Email addresses"}},{"name":"user_events","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View events triggered by a user's activity.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-events","fgp":["Read-only"],"human_name":"user events","resource_group":"#user-permissions-for-events","title":"Events"}},{"name":"followers","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"A user's followers","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-followers","fgp":["Read-only","Read and write"],"human_name":"followers","resource_group":"#user-permissions-for-followers","title":"Followers"}},{"name":"gpg_keys","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage a user's GPG keys.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-gpg-keys","fgp":["Read-only","Read and write"],"human_name":"gpg keys","resource_group":"#user-permissions-for-gpg-keys","title":"GPG keys"}},{"name":"gists","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create and modify a user's gists and comments.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-gists","fgp":["Read and write"],"human_name":"gists","resource_group":"#user-permissions-for-gists","title":"Gists"}},{"name":"keys","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Git SSH keys","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-git-ssh-keys","fgp":["Read-only","Read and write"],"human_name":"keys","resource_group":"#user-permissions-for-git-ssh-keys","title":"Git SSH keys"}},{"name":"interaction_limits","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Interaction limits on repositories","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-interaction-limits","fgp":["Read-only","Read and write"],"human_name":"interaction limits","resource_group":"#user-permissions-for-interaction-limits","title":"Interaction limits"}},{"name":"user_models","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Allows access to GitHub Models.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-models","fgp":["Read-only"],"human_name":"models","resource_group":"#user-permissions-for-models","title":"Models"}},{"name":"plan","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View a user's plan.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-plan","fgp":["Read-only"],"human_name":"plan","resource_group":"#user-permissions-for-plan","title":"Plan"}},{"name":"private_repository_invitations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View a user's invitations to private repositories","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-private-repository-invitations","fgp":["Read-only"],"human_name":"private repository invitations","resource_group":"#user-permissions-for-private-repository-invitations","title":"Private repository invitations"}},{"name":"profile","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage a user's profile settings.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-profile","fgp":["Read and write"],"human_name":"profile","resource_group":"#user-permissions-for-profile","title":"Profile"}},{"name":"git_signing_ssh_public_keys","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage a user's SSH signing keys.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-ssh-signing-keys","fgp":["Read-only","Read and write"],"human_name":"git signing ssh public keys","resource_group":"#user-permissions-for-ssh-signing-keys","title":"SSH signing keys"}},{"name":"starring","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"List and manage repositories a user is starring.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-starring","fgp":["Read-only","Read and write"],"human_name":"starring","resource_group":"#user-permissions-for-starring","title":"Starring"}},{"name":"watching","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"List and change repositories a user is subscribed to.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#user-permissions-for-watching","fgp":["Read-only","Read and write"],"human_name":"watching","resource_group":"#user-permissions-for-watching","title":"Watching"}}],"business":[{"name":"enterprise_custom_enterprise_roles","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage enterprise custom roles and assignments.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-custom-enterprise-roles","fgp":["Read-only","Read and write"],"human_name":"custom enterprise roles","resource_group":"#enterprise-permissions-for-custom-enterprise-roles","title":"Custom enterprise roles"}},{"name":"enterprise_custom_properties","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View repository custom properties and administer definitions at the enterprise level.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-custom-properties","fgp":["Read-only","Read and write"],"human_name":"enterprise custom properties for repositories","resource_group":"#enterprise-permissions-for-custom-properties","title":"Custom properties"}},{"name":"enterprise_ai_controls","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage enterprise-wide AI controls configuration","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-ai-controls","fgp":["Read-only","Read and write"],"human_name":"enterprise AI controls","resource_group":"#enterprise-permissions-for-enterprise-ai-controls","title":"Enterprise AI controls"}},{"name":"enterprise_copilot_metrics","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View enterprise Copilot metrics.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-copilot-metrics","fgp":["Read-only"],"human_name":"enterprise copilot metrics","resource_group":"#enterprise-permissions-for-enterprise-copilot-metrics","title":"Enterprise Copilot metrics"}},{"name":"enterprise_credentials","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage enterprise credentials","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-credentials","fgp":["Read-only","Read and write"],"human_name":"enterprise credentials","resource_group":"#enterprise-permissions-for-enterprise-credentials","title":"Enterprise credentials"}},{"name":"enterprise_custom_org_roles","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create, edit, delete and list custom organization roles at the enterprise level. View system organization roles.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-custom-organization-roles","fgp":["Read-only","Read and write"],"human_name":"enterprise custom organization roles","resource_group":"#enterprise-permissions-for-enterprise-custom-organization-roles","title":"Enterprise custom organization roles"}},{"name":"enterprise_custom_properties_for_organizations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View organization custom properties and administer definitions at the enterprise level.","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-custom-properties-for-organizations","fgp":["Read-only","Read and write","Admin"],"human_name":"enterprise custom properties for organizations","resource_group":"#enterprise-permissions-for-enterprise-custom-properties-for-organizations","title":"Enterprise custom properties for organizations"}},{"name":"enterprise_organizations","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Create and remove enterprise organizations","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-organizations","fgp":["Read and write"],"human_name":"enterprise organizations","resource_group":"#enterprise-permissions-for-enterprise-organizations","title":"Enterprise organizations"}},{"name":"enterprise_people","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"Manage user access to the enterprise","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-people","fgp":["Read-only","Read and write"],"human_name":"enterprise people","resource_group":"#enterprise-permissions-for-enterprise-people","title":"Enterprise people"}},{"name":"enterprise_sso","metadata":{"actions":{"none":"No access","read":"Read-only","write":"Read and write","admin":"Admin"},"description":"View and manage enterprise single sign-on configuration","docs_url":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens#enterprise-permissions-for-enterprise-single-sign-on","fgp":["Read-only","Read and write"],"human_name":"enterprise single sign-on configuration","resource_group":"#enterprise-permissions-for-enterprise-single-sign-on","title":"Enterprise single sign-on"}}]},"view":{"disabledForAllActions":false,"grantedPermissions":{"repository":{},"organization":{},"user":{},"business":{}},"integrationView":false,"resourceDocsUrl":"https://docs.github.com/rest/overview/permissions-required-for-fine-grained-personal-access-tokens"},"showSections":{"repository":false,"organization":false,"user":true,"enterprise":false},"appMode":null}}
diff --cc deploy/freedns-webhook/values.yaml
index 137c488,9423bd4..a0bca56
--- a/deploy/freedns-webhook/values.yaml
+++ b/deploy/freedns-webhook/values.yaml
@@@ -1,20 -1,22 +1,22 @@@
- # The GroupName here is used to identify your company or business unit that
- # created this webhook.
- # For freedns, this may be "acme.freedns.afraid.org".
- # This name will need to be referenced in each Issuer's `webhook` stanza to
+ # The groupName avoids naming conflicts on the Kubernetes API, it should be set by the
+ # author of the webhook a unique domain that the author owns. For
+ # example: some-provider-webhook.mycompany.tld , or
+ # some-provider-webhook.mypersonalsite.tld if your webhook is a personal open-source
+ # project.
+ # Once set to a unique domain name by the webhook author, it does not need to be further
+ # adjusted by the users of a webhook chart!
+ # The name will need to be referenced in each Issuer's `webhook` stanza to
  # inform cert-manager of where to send ChallengePayload resources in order to
  # solve the DNS01 challenge.
- # This group name should be **unique**, hence using your own company's domain
- # here is recommended.
 -groupName: acme.mycompany.com
 +groupName: acme.freedns.afraid.org
  
  certManager:
    namespace: cert-manager
    serviceAccountName: cert-manager
  
  image:
-   repository: penguinade/cert-manager-webhook-freedns
-   tag: 2024.11.02.05
 -  repository: mycompany/webhook-image
 -  tag: latest
++  repository: ghcr.io/tgckpg/cert-manager-webhook-freedns
++  tag: 2026.05.21.01
    pullPolicy: IfNotPresent
  
  nameOverride: ""
diff --cc freedns/buildinfo_gen.go
index 0000000,0000000..ed9f890
new file mode 100644
--- /dev/null
+++ b/freedns/buildinfo_gen.go
@@@ -1,0 -1,0 +1,6 @@@
++package freedns
++
++const (
++	IMAGE_TAG   = "2026.05.21.01"
++	Timestamp   = "20260521.170618"
++)
diff --cc freedns/freedns.go
index ffe2771,0000000..86bbf65
mode 100755,000000..100755
--- a/freedns/freedns.go
+++ b/freedns/freedns.go
@@@ -1,458 -1,0 +1,459 @@@
 +package freedns
 +
 +import (
 +	"context"
 +	"errors"
 +	"fmt"
 +	"io"
 +	"log"
 +	"net"
 +	"net/http"
 +	"net/url"
 +	"os"
 +	"strconv"
 +	"strings"
 +
 +	logf "github.com/cert-manager/cert-manager/pkg/logs"
++	logr "github.com/go-logr/logr"
 +	"golang.org/x/net/html"
 +	"golang.org/x/net/proxy"
 +)
 +
 +type FreeDNSOperations interface {
 +	Login()
 +	SelectDomain()
 +	AddRecord()
 +	FindRecord()
 +	DeleteRecord()
 +}
 +
 +type FreeDNS struct {
 +	AuthCookie *http.Cookie
 +	DomainId   string
 +	LoggedOut  bool
++	Logger     logr.Logger
 +}
 +
 +type contextDialer interface {
 +	DialContext(ctx context.Context, network, address string) (net.Conn, error)
 +}
 +
 +const URI_LOGIN = "https://freedns.afraid.org/zc.php?step=2"
 +const URI_DOMAIN = "https://freedns.afraid.org/domain/"
 +const URI_ADD_RECORD = "https://freedns.afraid.org/subdomain/save.php?step=2"
 +const URI_SUBDOMAIN = "https://freedns.afraid.org/subdomain/?limit="
 +const URI_SUBDOMAIN_EDIT = "https://freedns.afraid.org/subdomain/edit.php?data_id="
 +const URI_LOGOUT = "https://freedns.afraid.org/logout/"
 +const URI_DELETE_RECORD = "https://freedns.afraid.org/subdomain/delete2.php?data_id[]=%s&submit=delete%%20selected"
 +
 +func GetDomainFromZone(Zone string) string {
 +	_segs := strings.Split(strings.TrimSuffix(Zone, "."), ".")
 +	_segs = _segs[len(_segs)-2:]
 +	return strings.Join(_segs, ".")
 +}
 +
 +func newHTTPClient() (*http.Client, error) {
 +	transport := http.DefaultTransport.(*http.Transport).Clone()
 +
 +	socksAddr := os.Getenv("SOCKS5_PROXY")
 +	if socksAddr != "" {
 +		log.Printf("using SOCKS5 proxy from SOCKS5_PROXY: %s", redactProxyAddr(socksAddr))
 +		dialer, err := proxy.SOCKS5("tcp", socksAddr, nil, proxy.Direct)
 +		if err != nil {
 +			return nil, err
 +		}
 +
 +		ctxDialer, ok := dialer.(interface {
 +			DialContext(ctx context.Context, network, address string) (net.Conn, error)
 +		})
 +		if !ok {
 +			return nil, fmt.Errorf("SOCKS5 dialer does not support DialContext")
 +		}
 +
 +		transport.Proxy = nil
 +		transport.DialContext = ctxDialer.DialContext
 +	} else {
 +		transport.Proxy = http.ProxyFromEnvironment
 +	}
 +
 +	return &http.Client{
 +		Transport: transport,
 +		CheckRedirect: func(req *http.Request, via []*http.Request) error {
 +			return http.ErrUseLastResponse
 +		},
 +	}, nil
 +}
 +
 +func redactProxyAddr(addr string) string {
 +	u, err := url.Parse(addr)
 +	if err == nil && u.User != nil {
 +		u.User = url.UserPassword("xxxxx", "xxxxx")
 +		return u.String()
 +	}
 +
 +	return addr
 +}
 +
 +func _HttpRequest(method string, reqURL string, postData url.Values, exCookie *http.Cookie) (*http.Response, string, error) {
 +	client, err := newHTTPClient()
 +	if err != nil {
 +		return nil, "", err
 +	}
 +
 +	var body io.Reader
 +
 +	switch method {
 +	case http.MethodGet:
 +		body = nil
 +
 +	case http.MethodPost:
 +		body = strings.NewReader(postData.Encode())
 +
 +	default:
 +		return nil, "", fmt.Errorf("method %q is not supported", method)
 +	}
 +
 +	req, err := http.NewRequest(method, reqURL, body)
 +	if err != nil {
 +		return nil, "", err
 +	}
 +
 +	req.Header.Set("User-Agent", "github.com/tgckpg/cert-manager-webhook-freedns (2022.03.15)")
 +
 +	if method == http.MethodPost {
 +		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 +	}
 +
 +	if exCookie != nil {
 +		req.AddCookie(exCookie)
 +	}
 +
 +	resp, err := client.Do(req)
 +	if err != nil {
 +		return resp, "", err
 +	}
 +	defer resp.Body.Close()
 +
 +	respData, err := io.ReadAll(resp.Body)
 +	if err != nil {
 +		return resp, "", err
 +	}
 +
 +	return resp, string(respData), nil
 +}
 +
 +func (dnsObj *FreeDNS) Login(Username string, Password string) error {
 +
 +	authData := url.Values{}
 +	authData.Set("username", Username)
 +	authData.Set("password", Password)
 +	authData.Set("submit", "Login")
 +	authData.Set("action", "auth")
 +
 +	resp, respString, err := _HttpRequest("POST", URI_LOGIN, authData, nil)
 +	if err != nil {
 +		return err
 +	}
- 	fmt.Println(err)
 +
 +	if strings.Contains(respString, "Invalid UserID/Pass") {
 +		return errors.New("Invalid UserID/Pass")
 +	}
 +
 +	for _, cookie := range resp.Cookies() {
 +		if cookie.Name == "dns_cookie" {
 +			dnsObj.AuthCookie = cookie
 +			dnsObj.LoggedOut = false
 +		}
 +	}
 +
 +	return nil
 +}
 +
 +func (dnsObj *FreeDNS) Logout() error {
 +	if dnsObj.LoggedOut {
 +		return nil
 +	}
 +
 +	_, _, err := _HttpRequest("GET", URI_LOGOUT, nil, dnsObj.AuthCookie)
 +	if err != nil {
 +		return err
 +	}
 +
 +	dnsObj.LoggedOut = true
 +	return nil
 +}
 +
 +func (dnsObj *FreeDNS) SelectDomain(DomainName string) error {
 +	if dnsObj.AuthCookie == nil {
 +		return errors.New("Not logged in")
 +	}
 +
 +	resp, respStr, err := _HttpRequest("GET", URI_DOMAIN, nil, dnsObj.AuthCookie)
 +	if err != nil {
 +		return err
 +	}
 +
 +	if resp.StatusCode == 302 {
 +		return errors.New("dns_cookie maybe expired")
 +	}
 +
 +	htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
 +
 +	inBold := false
 +	lookForA := false
 +	dnsObj.DomainId = ""
 +
 +	// Begin search for domain id
 +loop:
 +	for {
 +		tt := htmlTokens.Next()
 +		switch tt {
 +		case html.ErrorToken:
 +			break loop
 +		case html.TextToken:
 +			if inBold && strings.TrimSpace(htmlTokens.Token().Data) == DomainName {
- 				logf.V(logf.InfoLevel).Info(fmt.Sprintf("Found HTMLTextNode that contains \"%s\", try looking for domain id", DomainName))
++				dnsObj.Logger.V(logf.InfoLevel).Info(fmt.Sprintf("Found HTMLTextNode that contains \"%s\", try looking for domain id", DomainName))
 +				lookForA = true
 +			}
 +			// The [Manage] anchor is next to the bold tag
 +			//    <b>DOMAIN_NAME</b> <a href="">[Manage]</a>
 +		case html.StartTagToken:
 +			_t, hasAttr := htmlTokens.TagName()
 +			tagName := string(_t)
 +			inBold = tagName == "b"
 +			if lookForA && tagName == "a" && hasAttr {
 +				for {
 +					attrKey, attrValue, moreAttr := htmlTokens.TagAttr()
 +					_href := string(attrValue)
 +					if string(attrKey) == "href" && strings.HasPrefix(_href, "/subdomain/?limit=") {
 +						dnsObj.DomainId = strings.TrimPrefix(_href, "/subdomain/?limit=")
- 						logf.V(logf.InfoLevel).Info(fmt.Sprintf("Domain id for \"%s\" is %s", DomainName, dnsObj.DomainId))
++						dnsObj.Logger.V(logf.InfoLevel).Info(fmt.Sprintf("Domain id for \"%s\" is %s", DomainName, dnsObj.DomainId))
 +						break loop
 +					}
 +					if !moreAttr {
 +						break
 +					}
 +				}
 +			}
 +		}
 +	}
 +
 +	if dnsObj.DomainId == "" {
 +		return errors.New(fmt.Sprintf("Unable to locate domain id for \"%s\" under /domain/ page", DomainName))
 +	}
 +
 +	return nil
 +}
 +
 +func (dnsObj *FreeDNS) AddRecord(RecordType string, Subdomain string, Address string, Wildcard bool, ttl string) error {
 +	if dnsObj.DomainId == "" {
 +		return errors.New("No domain selected")
 +	}
 +
- 	logf.V(logf.InfoLevel).Info(fmt.Sprintf("Adding %s Record: %s %s", RecordType, Subdomain, Address))
++	dnsObj.Logger.V(logf.InfoLevel).Info(fmt.Sprintf("Adding %s Record: %s %s", RecordType, Subdomain, Address))
 +
 +	recordData := url.Values{}
 +	recordData.Set("type", RecordType)
 +	recordData.Set("domain_id", dnsObj.DomainId)
 +	recordData.Set("subdomain", Subdomain)
 +	recordData.Set("address", Address)
 +	recordData.Set("send", "Save!")
 +	if Wildcard {
 +		recordData.Set("wildcard", "1")
 +	}
 +
 +	resp, respStr, err := _HttpRequest("POST", URI_ADD_RECORD, recordData, dnsObj.AuthCookie)
 +	if err != nil {
 +		return err
 +	}
 +
 +	if resp.StatusCode != 302 {
 +
 +		// Record already exists, treat this as success
 +		if strings.Contains(respStr, "already have another already existent") {
- 			logf.V(logf.InfoLevel).Info("Record already exists")
++			dnsObj.Logger.V(logf.InfoLevel).Info("Record already exists")
 +			return nil
 +		}
 +
 +		// Try get a sense of the problem
 +		var errorMesgs []string
 +		lookForNextEl := 0
 +		lookForText := false
 +		_strBuffer := ""
 +		htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
 +	loop:
 +		for {
 +			tt := htmlTokens.Next()
 +			switch tt {
 +			case html.ErrorToken:
 +				break loop
 +			case html.TextToken:
 +				_text := strings.TrimSpace(string(htmlTokens.Text()))
 +				// Search for the "1 error" / "N errors" message
 +				if strings.HasSuffix(_text, "error") || strings.HasSuffix(_text, "errors") {
 +					_text = strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(_text, "s"), "error"))
 +					_n, _ := strconv.ParseInt(_text, 10, 8)
 +					// + 1 because we are already inside a font tag
 +					// The next closing </font> is ourself
 +					lookForNextEl = int(_n) + 1
 +				} else if lookForText {
 +					_strBuffer = _strBuffer + _text
 +				}
 +
 +			case html.StartTagToken:
 +				_t, _ := htmlTokens.TagName()
 +				tagName := string(_t)
 +				if tagName == "font" && 0 < lookForNextEl {
 +					lookForText = true
 +					_strBuffer = ""
 +				}
 +			case html.EndTagToken:
 +				_t, _ := htmlTokens.TagName()
 +				tagName := string(_t)
 +				if tagName == "font" && 0 < lookForNextEl {
 +					lookForText = false
 +					errorMesgs = append(errorMesgs, strings.TrimSpace(_strBuffer))
 +					lookForNextEl--
 +				}
 +			}
 +		}
 +
 +		if 0 < len(errorMesgs) {
 +			return errors.New(strings.Join(errorMesgs, ", "))
 +		}
 +
 +		return errors.New("Unknown error while submitting record")
 +	}
 +
 +	_Location, err := resp.Location()
 +	if err != nil {
 +		return err
 +	}
 +
 +	if strings.HasPrefix(_Location.Path, "/zc.php") {
- 		logf.V(logf.DebugLevel).Info("Error on AddRecord: Cookie expired")
++		dnsObj.Logger.V(logf.DebugLevel).Info("Error on AddRecord: Cookie expired")
 +		return errors.New("dns_cookie maybe expired")
 +	}
 +
 +	return nil
 +}
 +
 +func (dnsObj *FreeDNS) DeleteRecord(RecordId string) error {
 +
- 	logf.V(logf.InfoLevel).Info(fmt.Sprintf("(id=%s) Removing Record", RecordId))
++	dnsObj.Logger.V(logf.InfoLevel).Info(fmt.Sprintf("(id=%s) Removing Record", RecordId))
 +
 +	resp, _, err := _HttpRequest("GET", fmt.Sprintf(URI_DELETE_RECORD, RecordId), nil, dnsObj.AuthCookie)
 +	if err != nil {
 +		return err
 +	}
 +
 +	if resp.StatusCode != 302 {
 +		return errors.New("Unexpected " + fmt.Sprint(resp.StatusCode) + " from remote while deleting record")
 +	}
 +
 +	_Location, err := resp.Location()
 +	if err != nil {
 +		return err
 +	}
 +
 +	if strings.HasPrefix(_Location.Path, "/zc.php") {
 +		return errors.New("dns_cookie maybe expired")
 +	}
 +
 +	return nil
 +}
 +
 +func (dnsObj *FreeDNS) FindRecord(Subdomain string, RecordType string, Address string) (string, error) {
 +	if dnsObj.DomainId == "" {
 +		return "", errors.New("No domain selected")
 +	}
 +
 +	resp, respStr, err := _HttpRequest("GET", URI_SUBDOMAIN+dnsObj.DomainId, nil, dnsObj.AuthCookie)
 +	if err != nil {
 +		return "", err
 +	}
 +
 +	if resp.StatusCode == 302 {
 +		return "", errors.New("dns_cookie maybe expired")
 +	}
 +
 +	var DeepSearchCandidates []string
 +	CurrRecordId := ""
 +	CurrRecordType := ""
 +	CurrRecordAddr := ""
 +	CurrTagName := ""
 +	lookForNextEl := 0
 +
 +	htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
 +loop:
 +	for {
 +		tt := htmlTokens.Next()
 +		switch tt {
 +		case html.ErrorToken:
 +			break loop
 +		case html.TextToken:
 +			if CurrTagName == "a" && lookForNextEl == 1 && CurrRecordAddr == "" {
 +				CurrRecordAddr = strings.TrimSpace(string(htmlTokens.Text()))
 +			} else if CurrTagName == "td" {
 +				if lookForNextEl == 1 {
 +					CurrRecordType = string(htmlTokens.Text())
 +					lookForNextEl = 2
 +				} else if lookForNextEl == 2 {
 +					_Addr := string(htmlTokens.Text())
 +					if CurrRecordType == RecordType && CurrRecordAddr == Subdomain {
 +						if _Addr == Address {
 +							return CurrRecordId, nil
 +						} else if strings.HasSuffix(_Addr, "...") && strings.HasPrefix(Address, strings.TrimSuffix(_Addr, "...")) {
 +							DeepSearchCandidates = append(DeepSearchCandidates, CurrRecordId)
 +						}
 +					}
 +					lookForNextEl = 0
 +				}
 +			}
 +		/** Each record is displayed with the following structure
 +		 *   <td bgcolor="#eeeeee">
 +		 *       <a href="edit.php?data_id=0000000">
 +		 *          [DOMAIN_NAME]
 +		 *       </a> (<b><font color="blue">G</font></b>)
 +		 *   </td>
 +		 *   <td bgcolor="#eeeeee">TXT</td>
 +		 *   <td bgcolor="#eeeeee">"google-site-verification=truncated_text...</td>
 +		 */
 +		case html.StartTagToken:
 +			_t, hasAttr := htmlTokens.TagName()
 +			CurrTagName = string(_t)
 +			if CurrTagName == "a" && hasAttr {
 +				for {
 +					attrKey, attrValue, moreAttr := htmlTokens.TagAttr()
 +					_href := string(attrValue)
 +					if string(attrKey) == "href" && strings.Contains(_href, "edit.php?data_id=") {
 +						lookForNextEl = 1
 +						CurrRecordAddr = ""
 +						CurrRecordId = strings.TrimPrefix(_href, "edit.php?data_id=")
 +						break
 +					}
 +					if !moreAttr {
 +						break
 +					}
 +				}
 +			}
 +
 +		}
 +	}
 +
 +	// Begin deep search for truncated records
 +	htmlAddr := strings.ReplaceAll(html.EscapeString(Address), "&#34;", "&quot;")
 +	for _, RecordId := range DeepSearchCandidates {
- 		logf.V(logf.DebugLevel).Info("Searching in " + RecordId)
++		dnsObj.Logger.V(logf.DebugLevel).Info("Searching in " + RecordId)
 +		_, respStr, err := _HttpRequest("GET", URI_SUBDOMAIN_EDIT+RecordId, nil, dnsObj.AuthCookie)
 +		if err != nil {
 +			continue
 +		}
 +
 +		if strings.Contains(respStr, htmlAddr) {
 +			return RecordId, nil
 +		}
 +	}
 +
 +	return "", errors.New("No such record")
 +}
diff --cc go.mod
index b9b94c0,2e3a86b..26e5616
--- a/go.mod
+++ b/go.mod
@@@ -1,13 -1,13 +1,13 @@@
 -module github.com/cert-manager/webhook-example
 +module github.com/cert-manager/webhook-freedns
  
- go 1.22.0
+ go 1.25.0
  
  require (
- 	github.com/cert-manager/cert-manager v1.15.1
- 	github.com/miekg/dns v1.1.61
- 	github.com/stretchr/testify v1.9.0
- 	k8s.io/apiextensions-apiserver v0.30.2
- 	k8s.io/client-go v0.30.2
+ 	github.com/cert-manager/cert-manager v1.20.2
+ 	github.com/miekg/dns v1.1.72
+ 	github.com/stretchr/testify v1.11.1
+ 	k8s.io/apiextensions-apiserver v0.35.4
+ 	k8s.io/client-go v0.35.4
  )
  
  require (
diff --cc image.Makefile
index acffdcf,0000000..f64dcf4
mode 100644,000000..100644
--- a/image.Makefile
+++ b/image.Makefile
@@@ -1,32 -1,0 +1,51 @@@
- IMAGE_NAME := penguinade/cert-manager-webhook-freedns
- IMAGE_TAG := dev
++include build.env
++-include build.env.work
++export
 +
- BUILDX_BUILDER := container-builder
++IMAGE_NAME ?= webhook
++IMAGE_TAG  ?= dev
++
++BUILDX_BUILDER ?= container-builder
++BUILDINFO_FILE := freedns/buildinfo_gen.go
 +
 +ensure-buildx:
 +	@if ! docker buildx inspect $(BUILDX_BUILDER) >/dev/null 2>&1; then \
 +		echo "Creating buildx builder $(BUILDX_BUILDER)..."; \
 +		docker buildx create \
 +			--name $(BUILDX_BUILDER) \
 +			--driver docker-container \
 +			--driver-opt network=host \
 +			--bootstrap --use; \
 +	else \
 +		echo "Using existing buildx builder $(BUILDX_BUILDER)"; \
 +		docker buildx use $(BUILDX_BUILDER); \
 +	fi
 +
- build: ensure-buildx
++.buildinfo:
++	@mkdir -p $(dir $(BUILDINFO_FILE))
++	@printf '%s\n' \
++		'package freedns' \
++		'' \
++		'const (' \
++		'	IMAGE_TAG   = "$(IMAGE_TAG)"' \
++		'	Timestamp   = "'$$(TZ=UTC date +%Y%m%d.%H%M%S)'"' \
++		')' \
++	> $(BUILDINFO_FILE)
++
++build: .buildinfo ensure-buildx
 +	docker buildx build \
 +		--platform linux/amd64,linux/arm64 \
 +		-f Dockerfile \
 +		-t $(IMAGE_NAME):$(IMAGE_TAG) .
 +
- push: ensure-buildx
++push: .buildinfo ensure-buildx
 +	docker buildx build \
 +		--platform linux/amd64,linux/arm64 \
 +		-f Dockerfile \
 +		-t $(IMAGE_NAME):$(IMAGE_TAG) \
 +		--push .
 +
- .PHONY: push
++inspect:
++	docker buildx imagetools inspect $(IMAGE_NAME):$(IMAGE_TAG)
++
++.PHONY: push build .buildinfo
diff --cc main.go
index 7f16f30,969e6d2..8c4d4d5
--- a/main.go
+++ b/main.go
@@@ -4,22 -4,16 +4,24 @@@ import 
  	"encoding/json"
  	"fmt"
  	"os"
 +	"strings"
  
  	extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 +	"k8s.io/client-go/kubernetes"
  	"k8s.io/client-go/rest"
  
- 	logf "github.com/cert-manager/cert-manager/pkg/logs"
- 	"github.com/cert-manager/webhook-freedns/freedns"
  	"github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
  	"github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd"
++	logf "github.com/cert-manager/cert-manager/pkg/logs"
++	"github.com/cert-manager/webhook-freedns/freedns"
  )
  
  var GroupName = os.Getenv("GROUP_NAME")
 +var UserName = os.Getenv("FREEDNS_USERNAME")
 +var Password = os.Getenv("FREEDNS_PASSWORD")
 +
++var logger = logf.Log.WithName("freedns")
+ 
  func main() {
  	if GroupName == "" {
  		panic("GROUP_NAME must be specified")
@@@ -97,8 -83,7 +99,10 @@@ func (c *customDNSProviderSolver) Name(
  // cert-manager itself will later perform a self check to ensure that the
  // solver has correctly configured the DNS provider.
  func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
- 	dnsObj := freedns.FreeDNS{}
 -	cfg, err := loadConfig(ch.Config)
++	dnsObj := freedns.FreeDNS{
++		Logger: logger,
++	}
 +	err := dnsObj.Login(UserName, Password)
  	if err != nil {
  		return err
  	}
@@@ -130,27 -102,8 +134,27 @@@
  // This is in order to facilitate multiple DNS validations for the same domain
  // concurrently.
  func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
 -	// TODO: add code that deletes a record from the DNS provider's console
 -	return nil
 +
 +	if c.freedns.AuthCookie == nil {
 +		return nil
 +	}
 +
 +	dnsObj := c.freedns
 +
 +	_addr := strings.TrimRight(ch.ResolvedFQDN, ".")
 +	_key := "\"" + ch.Key + "\""
 +	_id, err := c.freedns.FindRecord(_addr, "TXT", _key)
 +
- 	logf.V(logf.InfoLevel).Info(fmt.Sprintf("(id=%s) TXT Record: %s %s", _id, _addr, _key))
++	logger.V(logf.InfoLevel).Info(fmt.Sprintf("(id=%s) TXT Record: %s %s", _id, _addr, _key))
 +
 +	if _id != "" {
 +		err = c.freedns.DeleteRecord(_id)
 +		if err != nil {
 +			return err
 +		}
 +	}
 +
 +	return dnsObj.Logout()
  }
  
  // Initialize will be called when the webhook first starts.
@@@ -166,12 -119,13 +170,14 @@@ func (c *customDNSProviderSolver) Initi
  	///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO
  	///// YOUR CUSTOM DNS PROVIDER
  
 -	//cl, err := kubernetes.NewForConfig(kubeClientConfig)
 -	//if err != nil {
 -	//	return err
 -	//}
 -	//
 -	//c.client = cl
 +	cl, err := kubernetes.NewForConfig(kubeClientConfig)
 +	if err != nil {
 +		return err
 +	}
 +	c.client = cl
 +
++	logger.V(logf.InfoLevel).Info("FreeDNS webhook starting", "build", freedns.Timestamp, "tag", freedns.IMAGE_TAG)
+ 
  	///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE
  	return nil
  }