diff --git a/doc/man/man5/slapo-ppolicy.5 b/doc/man/man5/slapo-ppolicy.5
index 0b4c92e2be607a18df6fc2914d0578b8e50ada5e..6d67d151ab65c6126ce74bbe1182ed0c664b9207 100644
--- a/doc/man/man5/slapo-ppolicy.5
+++ b/doc/man/man5/slapo-ppolicy.5
@@ -36,6 +36,9 @@ when considering a single-valued password attribute, while
 the userPassword attribute allows multiple values.  This implementation
 enforces a single value for the userPassword attribute, despite
 its specification.
+.P
+In addition to supporting the IETF Password Policy, this module can
+send the Netscape Password validity controls when configured to do so.
 
 .SH CONFIGURATION
 These 
@@ -84,6 +87,12 @@ that sending the
 error code provides useful information
 to an attacker; sites that are sensitive to security issues should not
 enable this option.
+.TP
+.B ppolicy_send_netscape_controls
+If set, ppolicy will send the password policy expired (2.16.840.1.113730.3.4.4)
+and password policy expiring (2.16.840.1.113730.3.4.5) controls when
+appropriate. The controls are not sent for bind requests where the Password
+policy control has already been requested. Default is not to send the controls.
 
 .SH OBJECT CLASS
 The 
diff --git a/servers/slapd/overlays/ppolicy.c b/servers/slapd/overlays/ppolicy.c
index 362fe91e2c26e83e2ed424f42c11b27f276780f3..422a85ad428cbc8ad44dae4042de168ba94ee5e9 100644
--- a/servers/slapd/overlays/ppolicy.c
+++ b/servers/slapd/overlays/ppolicy.c
@@ -55,6 +55,7 @@ typedef struct pp_info {
 	int use_lockout;		/* send AccountLocked result? */
 	int hash_passwords;		/* transparently hash cleartext pwds */
 	int forward_updates;	/* use frontend for policy state updates */
+	int send_netscape_controls;	/* send netscape password controls */
 } pp_info;
 
 /* Our per-connection info - note, it is not per-instance, it is 
@@ -243,6 +244,13 @@ static ConfigTable ppolicycfg[] = {
 	  "( OLcfgOvAt:12.3 NAME 'olcPPolicyUseLockout' "
 	  "DESC 'Warn clients with AccountLocked' "
 	  "SYNTAX OMsBoolean SINGLE-VALUE )", NULL, NULL },
+	{ "ppolicy_send_netscape_controls", "on|off", 1, 2, 0,
+	  ARG_ON_OFF|ARG_OFFSET,
+	  (void *)offsetof(pp_info,send_netscape_controls),
+	  "( OLcfgOvAt:12.6 NAME 'olcPPolicySendNetscapeControls' "
+	  "DESC 'Send Netscape policy controls' "
+	  "EQUALITY booleanMatch "
+	  "SYNTAX OMsBoolean SINGLE-VALUE )", NULL, NULL },
 	{ NULL, NULL, 0, 0, 0, ARG_IGNORED }
 };
 
@@ -252,7 +260,8 @@ static ConfigOCs ppolicyocs[] = {
 	  "DESC 'Password Policy configuration' "
 	  "SUP olcOverlayConfig "
 	  "MAY ( olcPPolicyDefault $ olcPPolicyHashCleartext $ "
-	  "olcPPolicyUseLockout $ olcPPolicyForwardUpdates ) )",
+	  "olcPPolicyUseLockout $ olcPPolicyForwardUpdates $ "
+	  "olcPPolicySendNetscapeControls ) )",
 	  Cft_Overlay, ppolicycfg },
 	{ NULL, 0, NULL }
 };
@@ -376,6 +385,8 @@ account_locked( Operation *op, Entry *e,
 #define PPOLICY_GRACE  0x81L	/* primitive + 1 */
 
 static const char ppolicy_ctrl_oid[] = LDAP_CONTROL_PASSWORDPOLICYRESPONSE;
+static const char ppolicy_pwd_expired_oid[] = LDAP_CONTROL_X_PASSWORD_EXPIRED;
+static const char ppolicy_pwd_expiring_oid[] = LDAP_CONTROL_X_PASSWORD_EXPIRING;
 
 static LDAPControl *
 create_passcontrol( Operation *op, int exptime, int grace, LDAPPasswordPolicyError err )
@@ -435,6 +446,42 @@ fail:
 	return cp;
 }
 
+static LDAPControl *
+create_passexpiry( Operation *op, int expired, int warn )
+{
+	BerElementBuffer berbuf;
+	BerElement *ber = (BerElement *) &berbuf;
+	LDAPControl c = { 0 }, *cp;
+	char buf[sizeof("-2147483648")];
+	struct berval bv = { .bv_val = buf, .bv_len = sizeof(buf) };
+	int rc;
+
+	BER_BVZERO( &c.ldctl_value );
+
+	bv.bv_len = snprintf( bv.bv_val, bv.bv_len, "%d", warn );
+
+	ber_init2( ber, NULL, LBER_USE_DER );
+	ber_printf( ber, "O", &bv );
+
+	if (ber_flatten2( ber, &c.ldctl_value, 0 ) == -1) {
+		return NULL;
+	}
+	cp = op->o_tmpalloc( sizeof( LDAPControl ) + c.ldctl_value.bv_len, op->o_tmpmemctx );
+	if ( expired ) {
+		cp->ldctl_oid = (char *)ppolicy_pwd_expired_oid;
+	} else {
+		cp->ldctl_oid = (char *)ppolicy_pwd_expiring_oid;
+	}
+	cp->ldctl_iscritical = 0;
+	cp->ldctl_value.bv_val = (char *)&cp[1];
+	cp->ldctl_value.bv_len = c.ldctl_value.bv_len;
+	AC_MEMCPY( cp->ldctl_value.bv_val, c.ldctl_value.bv_val, c.ldctl_value.bv_len );
+fail:
+	(void)ber_free_buf(ber);
+
+	return cp;
+}
+
 static LDAPControl **
 add_passcontrol( Operation *op, SlapReply *rs, LDAPControl *ctrl )
 {
@@ -913,7 +960,9 @@ ctrls_cleanup( Operation *op, SlapReply *rs, LDAPControl **oldctrls )
 	assert( rs->sr_ctrls[0] != NULL );
 
 	for ( n = 0; rs->sr_ctrls[n]; n++ ) {
-		if ( rs->sr_ctrls[n]->ldctl_oid == ppolicy_ctrl_oid ) {
+		if ( rs->sr_ctrls[n]->ldctl_oid == ppolicy_ctrl_oid ||
+			rs->sr_ctrls[n]->ldctl_oid == ppolicy_pwd_expired_oid ||
+			rs->sr_ctrls[n]->ldctl_oid == ppolicy_pwd_expiring_oid ) {
 			op->o_tmpfree( rs->sr_ctrls[n], op->o_tmpmemctx );
 			rs->sr_ctrls[n] = (LDAPControl *)(-1);
 			break;
@@ -944,6 +993,7 @@ ppolicy_bind_response( Operation *op, SlapReply *rs )
 {
 	ppbind *ppb = op->o_callback->sc_private;
 	slap_overinst *on = ppb->on;
+	pp_info *pi = on->on_bi.bi_private;
 	Modifications *mod = ppb->mod, *m;
 	int pwExpired = 0;
 	int ngut = -1, warn = -1, age, rc;
@@ -955,6 +1005,7 @@ ppolicy_bind_response( Operation *op, SlapReply *rs )
 	char nowstr_usec[ LDAP_LUTIL_GENTIME_BUFSIZE+8 ];
 	struct berval timestamp, timestamp_usec;
 	BackendInfo *bi = op->o_bd->bd_info;
+	LDAPControl *ctrl = NULL;
 	Entry *e;
 
 	/* If we already know it's locked, just get on with it */
@@ -1233,7 +1284,6 @@ locked:
 		Operation op2 = *op;
 		SlapReply r2 = { REP_RESULT };
 		slap_callback cb = { NULL, slap_null_cb, NULL, NULL };
-		pp_info *pi = on->on_bi.bi_private;
 		LDAPControl c, *ca[2];
 
 		op2.o_tag = LDAP_REQ_MODIFY;
@@ -1273,14 +1323,20 @@ locked:
 	}
 
 	if ( ppb->send_ctrl ) {
-		LDAPControl *ctrl = NULL;
-		pp_info *pi = on->on_bi.bi_private;
 
 		/* Do we really want to tell that the account is locked? */
 		if ( ppb->pErr == PP_accountLocked && !pi->use_lockout ) {
 			ppb->pErr = PP_noError;
 		}
 		ctrl = create_passcontrol( op, warn, ngut, ppb->pErr );
+	} else if ( pi->send_netscape_controls ) {
+		if ( ppb->pErr != PP_noError || ngut > 0 ) {
+			ctrl = create_passexpiry( op, 1, 0 );
+		} else if ( warn > 0 ) {
+			ctrl = create_passexpiry( op, 0, warn );
+		}
+	}
+	if ( ctrl ) {
 		ppb->oldctrls = add_passcontrol( op, rs, ctrl );
 		op->o_callback->sc_cleanup = ppolicy_ctrls_cleanup;
 	}
@@ -2512,6 +2568,21 @@ int ppolicy_initialize()
 		return code;
 	}
 
+	/* We don't expect to receive these controls, only send them */
+	code = register_supported_control( LDAP_CONTROL_X_PASSWORD_EXPIRED,
+		0, NULL, NULL, NULL );
+	if ( code != LDAP_SUCCESS ) {
+		Debug( LDAP_DEBUG_ANY, "Failed to register control %d\n", code, 0, 0 );
+		return code;
+	}
+
+	code = register_supported_control( LDAP_CONTROL_X_PASSWORD_EXPIRING,
+		0, NULL, NULL, NULL );
+	if ( code != LDAP_SUCCESS ) {
+		Debug( LDAP_DEBUG_ANY, "Failed to register control %d\n", code, 0, 0 );
+		return code;
+	}
+
 	ldap_pvt_thread_mutex_init( &chk_syntax_mutex );
 
 	ppolicy.on_bi.bi_type = "ppolicy";