A minimal WebFinger server
Find a file
2025-03-17 14:14:46 -04:00
cmd initial commit 2025-03-17 14:14:46 -04:00
configs initial commit 2025-03-17 14:14:46 -04:00
internal initial commit 2025-03-17 14:14:46 -04:00
vendor initial commit 2025-03-17 14:14:46 -04:00
.gitignore initial commit 2025-03-17 14:14:46 -04:00
go.mod initial commit 2025-03-17 14:14:46 -04:00
go.sum initial commit 2025-03-17 14:14:46 -04:00
LICENSE initial commit 2025-03-17 14:14:46 -04:00
README.md initial commit 2025-03-17 14:14:46 -04:00

webfingerd

A minimal webfinger server intended to remain as simple as possible, intended mainly for OIDC discovery. No user database needed.

Heavily inspired by (and derived from) Noah Snelson's excellent project, carpal.

How does this work?

Instead of requiring a user database of some kind like LDAP, we first check if the request follows certain rules (like a matching Host header for a domain we control), then we simply fill in a template to create resources on the fly. It turns out that this works well for our purposes most of the time, even if the user doesn't really exist. This also has the incidental benefit of making it impossible to enumerate valid users by spamming a webfinger server, something that I'd consider very important to prevent if using this as the discovery backend for a service like Tailscale.

Doesn't that break the standard?

That's a good question. Based on my interpretation of the standard, no it does not. Section 9.4 of the RFC says that the client can't expect the returned information to be accurate.

A WebFinger resource has no means of ensuring that information provided by a user is accurate. Likewise, neither the resource nor the client can be absolutely guaranteed that information has not been manipulated either at the server or along the communication path between the client and server. Use of HTTPS helps to address some concerns with manipulation of information along the communication path, but it clearly cannot address issues where the resource provided incorrect information, either due to being provided false information or due to malicious behavior on the part of the server administrator. As with any information service available on the Internet, users should be wary of information received from untrusted sources.

On the other hand, Section 4.2 says that if the server has no information about a resource, it must return a 404.

If the "resource" parameter is a value for which the server has no information, the server MUST indicate that it was unable to match the request as per Section 10.4.5 of RFC 2616.

[RFC 2616] 10.4.5 404 Not Found

The server has not found anything matching the Request-URI. No indication is given of whether the condition is temporary or permanent. The 410 (Gone) status code SHOULD be used if the server knows, through some internally configurable mechanism, that an old resource is permanently unavailable and has no forwarding address. This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.

This raises an interesting question. Does making up a resource on the fly count as "having no information" if we ensure the returned resource could become valid in the future?

Given that information, a more nuanced answer would be maybe.

I guess I'm ok with that, what do I need to use it.

You need a reverse proxy of some kind to terminate TLS, and it must be configured not to respond to WebFinger requests over HTTP per Section 4 of the standard.

If you intend to use the strict_host_checking option, you also need to ensure that the Host header is passed to webfingerd. An example config snippet for Nginx is located in configs/nginx.example.conf

Building webfingerd

To build webfingerd, simply clone this repository and run go build cmd/webfingerd.go

Pre-made distro packaging

TODO

Podman/Docker

TODO

Configuring webfingerd

webfingerd expects it's configuration to be located in /etc/webfingerd/config.yml. If this is not the case, use the WEBFINGERD_CONFIG_FILE environment variable to specify the actual location of the config file. Additonally, the listen address/port can be specified with WEBFINGERD_LISTEN_ADDRESS.

Templates

The templates are specified as yaml, and the following attributes are available to be used as variables:

  • SubjectName: The subject name from the query URI
  • SubjectHost: The subject host from the query URI
  • DefaultHost: The default host specified in the config_file
  • HostHeader: The host found in the request header
  • Host: If not using strict_host_checking, this will be the subject host if it exists in the list of hosts in the config file, otherwise it will be the default host. If using strict_host_checking, it will always be the HostHeader.

TODO: write proper documentation for resource fields.

For now, webfingerd support the same resource fields as carpal, and their documentation is better than mine so you should read it. I intend to expand on this later, but for now the available fields are currently defined in internal/resource/main.go.

Strict Host Checking

This allows the simplest configuration. Simply set strict_host_checking to true, and pass the Host header to the webfinger. Everything will just work.

No Strict Host Checking

You'll need this if you have multiple hosts pointing to this server. Simply specify the list of allowed hosts in the config file. If override_hosts is set to True, should the host in the request URI does not match any allowed hosts, it will be replaced with the value specified in default_host. If override_hosts is false, the server will return a 404.

Known Limitations

  • Possibly standards non-compliant (as explained above)
  • Somewhat limited RFC 3986 validation. Not perfect, but good enough for now. Should be more fenced in though. See internal/rfc3986/main.go.
  • Multi-host functionality is unpolished

Disclaimer

This is my first real project in Go. I may be unknowingly abusing the language in horrible ways. Please be gentle.