Pwned Passwords Azure Function: serving the k-anonymity hash API
APIs for the k-anonymity Pwned Passwords implementation
At a glance
- What is it?
- This repository holds the code behind the Pwned Passwords web endpoint, which answers range queries against SHA-1 hashes in Azure Blob Storage using a fixed five character prefix so that a caller never sends a full hash. A second endpoint accepts authenticated SHA-1, NTLM and prevalence data, and the project also ships a Cloudflare Worker variant.
- Who is it for?
- PwnedPasswordsAzureFunction suits an organisation that wants to run its own instance of the Pwned Passwords range API rather than send password prefixes to a third party, and it is the reference implementation of the k-anonymity model that many client libraries implement.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 115 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the endpoint actually does
The README describes the project as the web endpoint which interacts directly with Azure Blob Storage to retrieve the SHA-1 hashes of the Pwned Passwords using a fixed five-length SHA-1 prefix to anonymise requests.
That prefix is the k-anonymity model, and the README points to Troy Hunt's announcement of Pwned Passwords version 2 for the details. The mechanism is simple enough to state in one sentence and important enough to be worth stating carefully. A client that wants to check a password takes the SHA-1 hash of the password, takes the first five characters of it, and sends only those to the server. The server returns every hash in the corpus that begins with those five characters, along with the number of times each has been seen. The client then looks for its own full hash in that list locally, and learns only whether it was found and how many times.
The server never sees the full hash and the client never sends it, so neither party learns the password. The response is large, which is the price, and the five character prefix is the anonymity set size.
The local test call in the README shows the shape of the request, with the five character prefix in the path.
curl -X GET http://localhost:7071/range/21BD1The prefix in that example is the beginning of a SHA-1 hash, and the endpoint is a range query, which is what the naming says.
The ingestion side and the authenticated endpoint
The repository is not only a read path. The to-do list at the end of the README shows the write side, and the first two items are marked complete.
The completed item is an authenticated endpoint which can receive SHA-1, NTLM and prevalence data. So the function is not only serving a static corpus, it is the ingestion point that receives new breach data and stores it. The second completed item is extracting SHA-1 into Azure Blob Storage, either appending in order to a file or updating a count value. That last detail is the important one for understanding the storage model, because the corpus is not a database of individual hash records but a set of files with sorted hashes and counts, and a new breach can be appended or merged by incrementing a count.
The two unfinished items are the ones with the clearest operational meaning. Adding SHA-1 and NTLM hashes to the downloadable corpus, which the list says should be updated monthly. And a corresponding Cloudflare cache item for the corpus or blob storage file must be invalidated, which the list says shouldn't be more than once per day for a cache item.
That second point explains the architecture around the Azure Function. The README mentions that the Pwned Passwords Cloudflare Worker can be found in a separate repository, and the reason a Worker exists is caching: the range responses are immutable while a corpus version is current, so they sit in a cache in front of the origin, and the cache entry is dropped when the corpus is refreshed.
Setting up the function locally
The prerequisites are old, and the README is explicit about it. Visual Studio 2019, VS Code or JetBrains Rider are the recommended IDEs, and the .NET 5.0 SDK is needed. The relevant Azure Functions extensions have to be installed, which is the Azure Development workload for Visual Studio 2019, the Azure Functions extension for VS Code, and the Azure Toolkit plus the Azure Functions core tools for Rider.
An Azure Storage emulator is also required, and there are two choices, the Azure Storage Emulator for Windows and Azurite, which the README lists as running on Windows, macOS and Linux. That choice matters for anyone developing on a Mac or on Linux, since the original emulator is Windows only.
Configuration is done through a local.settings.json file holding the Configuration Manager values, and the README is clear that it must not be committed to the repository, which it notes is already ignored by Git within the Functions project.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "<Your Connection String from Azure Storage Emulator",
"PwnedPasswordsConnectionString": "<Your Connection String from Azure Storage Emulator>",
"BlobContainerName": "<Name of Blob Container you created>",
"TableStorageName": "<Name of Table Storage you created",
"MetadataTableStorageName": "<Name of second Table Storage you created>"
}
}Four named values are involved, and the two table storage names are worth noting, because there are two of them. The blob container holds the hash data, and the two tables appear to hold different things, one of which the README calls metadata. The IsEncrypted flag is set to false in the example, which is a local development value rather than something to carry to a real deployment.
The repository layout and the open-source intent
The repository is a .NET solution, PwnedPasswords.sln, with a shape that separates the parts.
Functions/ is the Azure Function project itself, Functions.Tests/ holds the tests, Shared/ is for code shared between projects, and Tools/ is for command line helpers. The root has Directory.Build.props and Directory.Packages.props, which is the standard way of centralising build settings and package versions in a .NET solution. Ingestion.md is a separate document at the root, and it is the one to read for how data gets into the corpus, which the README does not describe.
The interesting part is the stated purpose. The README says the whole premise around open sourcing the Pwned Passwords Azure Function is to foster further development on the Pwned Passwords project, and that the to-do list was taken from the announcement blog post about open sourcing it. That framing is worth taking seriously as a signal about how the code should be used, and it also explains why the to-do list is kept in the README with checkboxes rather than in a tracker.
The CI badges at the top show a Continuous Integration Build workflow and a separate Publish Test Results workflow, and there is a Contributor Covenant 2.0 code of conduct that the README asks contributors to read first.
For an organisation considering running its own instance, the combination of a solution with a shared library, a test project and a dedicated ingestion document is a reasonable amount of structure for a service of this size.
Where this project is the wrong choice
The first limitation is the toolchain age. The README asks for the .NET 5.0 SDK and recommends Visual Studio 2019. .NET 5 is out of support, and a team that has standardised on a current LTS runtime will find the project needs porting before it can be built on a supported platform. That is a real cost for a service that has to run in someone's cloud for years.
The second is that this is the origin, not the whole system. The endpoint in this repository reads from Azure Blob Storage. To have data to serve, someone has to ingest breach data, format it into the sorted hash-and-count files the function expects, and upload it. Ingestion.md covers that, but the operational work of running the corpus pipeline is the larger part of the effort, and the README's own to-do list marks the monthly corpus refresh as unfinished.
The third is the Azure dependency. The function needs Azure Storage and Azure Functions, and local development needs an emulator or Azurite. A team that runs on another cloud or that does not want Azure in its stack has to port rather than deploy.
The fourth is the caching arrangement, which is where a self-hosted instance can quietly serve stale data. The to-do list has the cache invalidation item open, and the constraint noted there is that invalidation should not happen more than once per day for a cache item. Until that is settled, a self-hosted instance and the Cloudflare Worker fronting the public one can disagree about what the corpus contains.
The fifth is that the public API already exists. For the ordinary case of checking whether a user's password has appeared in a breach, the hosted Pwned Passwords API answers the query in a couple of requests and needs no infrastructure. Running your own instance is worth it when you cannot send password prefixes to a third party, and that is the case the k-anonymity design was built to make possible without.
How it compares with the Cloudflare Worker and with a local hash list
The Cloudflare Worker is the sibling implementation and the README points at it directly. The difference in approach is the position in the request path. The Azure Function is the origin, it talks to the blob storage and does the work, and the Worker sits in front of it as a cache. That arrangement gives the Worker a globally distributed edge and the Function a single source of truth, and it is the shape the to-do list's cache invalidation item is written around.
Running the Worker alone is not an option, because the Worker needs the corpus from somewhere. The two are complementary parts of one system.
The second comparison is with a local hash list, which is the other way to run this check without telling anyone anything. A service that ships or downloads the corpus and searches it locally has the same privacy property as the k-anonymity scheme, and it has none of the network round trips. The cost is distribution, since a corpus is large and it changes, and the cost of keeping that copy current is exactly the work the to-do list leaves open.
A third comparison is with a commercial breach notification service. Those offer a richer product, with monitoring of addresses, domains and shared secrets, and a dashboard for the security team. Pwned Passwords is narrower, it answers one question, and the k-anonymity design is what makes that one question answerable without giving the operator the password.
Licence, maintenance and the state of the work
The licence field on the repository records BSD-3-Clause, with the LICENSE file at the root. A permissive licence with no copyleft obligation is the right choice for a service implementation that organisations will want to fork, host and modify, and the no-endorsement clause means an operator can run their own instance without claiming any relationship with the project.
The repository is not archived and the last push was on 2026-06-30, so it is being worked on, and the recent releases section says there are no GitHub releases at all. That means the deployment artefact is whatever the CI pipeline produces rather than a tagged version, which is a weaker position for anyone who needs to pin a specific build of a security-relevant service.
The environment is .NET 5 and Visual Studio 2019, and the open items in the to-do list are the corpus refresh and the cache invalidation. Both are about the corpus staying current rather than about the code, which is a good sign for the code and a note for the operations: a self-hosted instance without a monthly refresh serves a frozen snapshot of the breaches that existed when it was loaded.
So the honest summary for an adopter is that the code is small, clear and well documented for what it does, that the design decision that made the service usable at scale is the one that also makes it your operational responsibility, and that running it means taking on the part of the work the project itself has not finished.
Editorial conclusion
PwnedPasswordsAzureFunction suits an organisation that wants to run its own instance of the Pwned Passwords range API rather than send password prefixes to a third party, and it is the reference implementation of the k-anonymity model that many client libraries implement. It is the wrong tool for a product that only needs to check a password against the public list, since the hosted API already answers that query, and the wrong choice if you cannot run Azure Storage and Azure Functions. Before deploying, read Ingestion.md, because the function serves hashes someone has to put into the blob container, and read the open to-do list, since the monthly corpus refresh is still marked as unfinished.
Frequently asked questions
What does the Pwned Passwords Azure Function do?
It is the web endpoint that reads SHA-1 hashes of the Pwned Passwords from Azure Blob Storage and serves them by range, using a fixed five-length prefix of the hash so that a request never reveals the full hash. The README points to Troy Hunt's Pwned Passwords version 2 announcement for the k-anonymity model.
How do I run the Pwned Passwords function locally?
Install the .NET 5.0 SDK, an Azure Functions extension and a storage emulator, either the Azure Storage Emulator on Windows or Azurite on any platform. Configure local.settings.json with the connection strings and names, then hit the endpoint with curl -X GET http://localhost:7071/range/21BD1.
What configuration values does local.settings.json need?
The README lists AzureWebJobsStorage, PwnedPasswordsConnectionString, BlobContainerName, TableStorageName and MetadataTableStorageName, with the two table storage entries being separate tables. The file must not be committed, and Git already ignores it inside the Functions project.
Does this repository only serve hashes, or does it also accept them?
Both. The to-do list marks an authenticated endpoint which can receive SHA-1, NTLM and prevalence data as complete, along with the extraction of SHA-1 into Azure Blob Storage either by appending in order to a file or by updating a count value.
What is still unfinished in the Pwned Passwords project?
Two items remain open: adding SHA-1 and NTLM hashes to the downloadable corpus, which should be updated monthly, and invalidating the corresponding Cloudflare cache item for the corpus or blob storage file, which should happen no more than once per day for a cache item.
What licence is the Pwned Passwords Azure Function released under?
BSD-3-Clause, with the terms in the LICENSE file at the root of the repository.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/haveibeenpwned-pwnedpasswordsazurefunction)Was this useful?