One of the major concerns I still have with password policy is the issue of the overhead involved in maintaining so many policy state variables for authentication failure / lockout tracking. It turns what would otherwise be pure read operations into writes, which is already troublesome for some cases. But in the context of replication, the problem can be multiplied by the number of replicas in use. Avoiding this write magnification effect is one of the reasons the initial versions of the ppolicy overlay explicitly prevented its state updates from being replicated. Replicating these state updates for every authentication request simply won't scale.
Unfortunately the braindead account lockout policy really doesn't work well without this sort of state information.
The problem is not much different from the scaling issues we have to deal with in making code run well on multiprocessor / multicore machines. Having developed effective solutions to those problems, we ought to be able to apply the same thinking to this as well.
The key to excellent scaling is the so-called "shared-nothing" approach, where every processor just uses its own local resources and never has to synchronize with ( == wait for) any other processor, but for the most part it's a design ideal, not something you can do perfectly in practice. However, we have some recent examples in the slapd code where we've been able to use this approach to good effect.
In the connection manager, we used to handle monitoring/counter information (number of ops, type of ops, etc) in a single counter, which required a lot of locking overhead to update. We now use an array of counters per thread, and each thread can update its own counters for free, completely eliminating the locking overhead. The trick is in recognizing that this type of info is written far more often than it is read, so optimizing the update case is far more important than optimizing the query case. When someone tries to read the counters that are exposed in back-monitor, then we simply iterate across the arrays and tally up the counters then. Since there's no particular requirement that all the counters be read in the same instant in time, all of these reads/updates can be performed without locking, so again we get it for free, no synchronization overhead at all.
So, it should now be obvious where we should go with the replication issue...
Ideally, you want password policy enforcement rules that don't even need global state at all. IMO, the best approach is still to keep policy state private to each DSA, and this still makes sense for DSAs that are topologically remote. E.g., assume you have a pair of servers, each in two separate cities. It's unlikely that a login attempt on one server will be in any way connected to a simultaneous login attempt on the other server. And in the face of bot attack, the rate of logins will probably be high enough to swamp the channel between the two servers, resulting in queueing delays that ultimately aggregate several of the updates on the attacked server into just a single update on the remote server. (E.g., N separate failure updates on one server will coalesce into a single update on the remote server.) Therefore, most of the time it's pointless for each server to try to immediately update the other with login failure info.
In the case of a local, load-balanced cluster of replicas, where the network latency between DSAs is very low, the natural coalescing of updates may not occur as often. Still, it would be better if the updates didn't happen at all. And in such an environment, where the DSAs are so close together that latency is low, distributing reads is still cheaper than distributing writes. So, the correct way to implement this global state is to keep it distributed separately during writes, and collect it during reads.
I'm looking for a way to express this in the schema and in the ppolicy draft, but I'm not sure how just yet. It strikes me that X.500 probably already has a type of distributed/collective attribute but I haven't looked yet.
Also I think we can take this a step further, but haven't thought it through all the way yet. If you typically have login failures coming from a single client, it should be sufficient to always route that client's requests to the same DSA, and have all of its failure tracking done locally/privately on that DSA.
At the other end, if you have an attack mounted by a number of separate machines, it's not clear that you must necessarily collect the state from every DSA on every authentication request. E.g., if you're setting a lockout based on the number of login failures, once the failure counter on a single DSA reaches the lockout threshold, it doesn't matter any more what the failure counter is on any other DSA, so that DSA no longer needs to look for the values on any other node.
If a client comes along and does a search to retrieve the policy state, e.g. looking for the last successful login or the last failure, then you want whatever DSA receives the request to broadcast the search to all the other DSAs and collate the results for the client by default. (Note that simple aggregation only works for multivalued attributes; for single-valued attributes like pwdLastSuccess you have to know to pick the most recent value.) And probably you should be able to specify a control (like ManageDSAit) to disable this automatic broadcast and only retrieve the value from a single DSA.
I realize that the points listed above about login attacks miss several attack scenarios. I think more of the scenarios need to be outlined and analyzed before moving forward with any recommendations on lockout behavior; the internet today is pretty different from when these lockout mechanisms were first designed.