GitOps User Guide
This guide helps you configure Nexus GSLB to pull its configuration from a Git repository with mandatory GPG-signed commits.
What you will set up
- A Git repository (Gitea or GitLab CE recommended)
- A directory that contains
gslbd.yamlfor your cluster - GPG signing and an allowlist of trusted signer fingerprints
- The
gslbddaemon configured to poll and reconcile changes every 30s (configurable)
Prerequisites
- A reachable Git server
- A deploy key (SSH private key) for read-only access
- GPG public keys installed on the
gslbdhost and trusted in the local GPG trustdb or explicitly allowlisted in config
Repository layout
infra/gslb-config
└── clusters/
└── prod-eu-glb/
└── gslbd.yaml # full config for the cluster
Signing workflow (GPG)
- Generate or use an existing GPG key. Export the public key and distribute to
gslbdhosts. - Configure Git to sign commits by default:
git config --global user.signingkey <KEYID>
git config --global commit.gpgsign true
- Commit and sign:
# edit clusters/prod-eu-glb/gslbd.yaml
git add clusters/prod-eu-glb/gslbd.yaml
git commit -S -m "Update endpoints"
git push origin main
Configure gslbd for GitOps Add to /etc/gslb/config.yaml:
gitops:
repoURL: "ssh://gitea@git.example.com/infra/gslb-config.git"
branch: "main"
pathPrefix: "clusters/prod-eu-glb"
pollInterval: "30s"
requireSignature: true
allowedSigners: ["ABCD1234EF56...FPR"]
auth:
sshKeyPath: "/etc/gslb/gitops_deploy_key"
Notes
requireSignature: truerejects unsigned or unverified commits.- When
allowedSignersis non-empty, only those fingerprints are accepted (case-insensitive match of%GK).%GKemits the long key ID (16 hex), not the 40-char fingerprint, on common gpg builds — pin the value your node actually prints (git log -1 --pretty=%GK), or list both forms. - The SSH deploy key must be readable by the
gslbdprocess and have permissions0600. - The signer's public key must be imported (and trusted) into a keyring the
gslbdservice can reach at runtime. The unit runsProtectHome=yes, so a keyring under the service user's$HOME(e.g./home/gslb/.gnupg) is masked and verification fails. Put it under the writableStateDirectoryand point the service at it: import withGNUPGHOME=/var/lib/gslbd/gnupg gpg --import <pub.asc>(as the service user), then addGNUPGHOME=/var/lib/gslbd/gnupgto the unit'sEnvironmentFile(/etc/gslb/env) and restart. Verify in-context withsudo -u <svcuser> env GNUPGHOME=… git -C <clone> verify-commit HEAD.
Reconciliation behavior (self-restart model)
GitOps applies the full config via a graceful self-restart — there is no in-process hot-apply and no coordinator. Each node reconciles independently:
- Fetch the configured repo/branch into a local workdir.
- Hash the config file (
gslbd.yaml, fallbackindex.yaml) underpathPrefixand compare to the config currently on disk. If unchanged, do nothing. - Verify the GPG-signed HEAD commit (when
requireSignature: true), optionally restricted toallowedSigners. - Validate the new config (parse + cross-field checks). An invalid commit is rejected and the running config is left untouched — it can never crash-loop the daemon.
- Apply: atomically write the exact repo bytes to the node's own config file (temp + rename), then request a graceful restart. systemd (
Restart=always) brings the daemon back onto the new config.
Because the apply writes the exact repo bytes, after the restart the on-disk config equals the repo, so the next poll is a no-op — no restart loop, no extra persisted state. Every node converges on the git source of truth by the same mechanism.
!!! important "GitOps requires a full restart per change" Unlike data-plane edits (pools/members/services via the API, which are live), a GitOps config change restarts the daemon. This is the intended model for declarative, file-driven config. For changes that should NOT cause a restart, use the API or systemctl reload gslbd (the SIGHUP reload subset).
Metrics:
gslbd_gitops_fetch_total{result},gslbd_gitops_verify_total{result},gslbd_gitops_apply_total{result}gslbd_gitops_last_apply_info{sha,signer}— last successfully applied commit
Rollback
- Revert the change in Git with a signed commit (or checkout a signed tag) and push.
- Each node detects the new commit, verifies it, rewrites its config, and restarts onto it.
Failure modes (running config is always preserved)
- Fetch failure: keep running; retry next tick.
- Signature failure: reject; keep running config.
- Parse/validation failure: reject; keep running config.
- Config write failure: log and keep running (no restart).
Troubleshooting
- Signature failures: check logs and ensure the signer fingerprint is in
allowedSignersor trusted in GPG. - No changes applied: confirm
pathPrefixis correct and containsgslbd.yaml(orindex.yaml). - Access denied: verify the SSH key and repository URL; try a manual clone using
GIT_SSH_COMMAND="ssh -i /etc/gslb/gitops_deploy_key". permission deniedon/etc/gslb/.gslbd-config-*.tmp(change is fetched + validated but never applied; logs showGitOps: failed to write config): the service user can write the file but not create a temp file in/etc/gslb. The atomic apply is temp-file-then-rename, so the directory must be group-writable by the service user. Fix:chown root:<svcuser> /etc/gslb && chmod 2770 /etc/gslb(setgid keeps the group on the rewritten config).ConfigurationDirectory=gslbdoes not fix this if/etc/gslbalready exists root-owned — systemd won't chown a pre-existing directory.Permission denied (publickey)cloning a Forgejo/Gitea repo: the forge's built-in SSH server usually listens on a non-standard port (often222), while:22is the host's own sshd. An scp-style URL (git@host:path) cannot carry a port and the fetcher runs a baregit clonewith no-p, so encode the port in anssh://URL:repoURL: "ssh://git@host:222/org/repo.git". Confirm the port the forge advertises (its repo "SSH" clone button, or the APIssh_url).
Code references
internal/gitops/*(fetcher, verifier)cmd/gslbd/setup_services.go(setupGitOps/reconcileGitOps)internal/config/validator/validate.godeploy/gslbd.service(Restart=always)