Saving Active Directory from Brute Force: A Two-Phase, RADIUS-First Login on NetScaler
One of the most common ways attackers get into company accounts is by brute-forcing application login forms.
The problem: AD exposed to the internet
If your only login is “username + AD password” checked directly against LDAP, every credential-stuffing bot on the internet is one form-submit away from a bind request on your Domain Controller. Two things go wrong:
- Compromise risk. Guess one weak password and the attacker is inside your systems.
- Lockout storms. Even failed guesses can trigger AD account lockouts — which means real users get locked out, the helpdesk melts down, and the attack doubles as a denial-of-service.
The simplest, cheapest, and most effective fix is structural: never let an unauthenticated request reach the AD password check at all.
The idea: a gate in front (RADIUS first, AD second)
We split login into two factors on two different systems:
- Page 1 — username + a RADIUS token (first factor). The token cannot be guessed the way a password can, and RADIUS is cheap and easy to rate-limit. Attackers die here. Crucially, a failed token never generates an LDAP bind, so AD sees no brute-force traffic and no lockouts.
- Page 2 — the AD password (second factor). This page is only ever shown to someone who already passed the token. By the time AD is asked to validate a password, the request is already trusted enough to be worth the bind.
This is defense in depth with a purpose: the first factor exists to protect the second. NetScaler calls this pattern nFactor authentication: a chain of factors, each with its own login page (schema) and policy, where success on one factor advances the user to the next.
Step 1 — Configure the authentication backends
This article cover the two phases login process for a Netscaler Gateway. For readability I use CLI configuration were possible and GUI screenshots where necessary.
First, define how to talk to each backend: create the RADIUS action and the LDAP action (assumed you already created the LB for the LDAPS servers called lb_ldaps.example.com and created the service account to bind with ldap, in this case svc_netscaler@corp.example.com)
add server radius 10.0.0.70
add authentication radiusAction Radius_act -serverIP 10.0.0.70 -serverPort 1812 -radKey <shared-secret>add authentication ldapAction ldap_act1 -serverName lb_ldaps.example.com -serverPort 636 -secType SSL -ldapBase "dc=corp,dc=example,dc=com" -ldapBindDn svc_netscaler@corp.example.com -ldapBindDnPassword <secret> -ldapLoginName sAMAccountName -groupAttrName memberOf -subAttributeName cn
The LDAP action binds with a low-privilege service account, searches for the user by sAMAccountName, and then binds as that user with their supplied password — over LDAPS (`-secType SSL`, port 636). That is the standard “bind as user” pattern.
Policies
Each action gets a policy that decides when to use it. For a simple linear chain, -rule true is sufficient:
# factor 1
add authentication Policy radpol -rule true -action Radius_act
# factor 2
add authentication Policy ldap_pol -rule true -action ldap_actStep 2 — Create the login schemas (the two pages)
Each factor uses a login page defined by an XML file under /nsconfig/loginschema/. You can use the default schemas or customize them with your own messages.
First page: SingleAuth1 (user + code)
In the GUI:
- Go to Security > AAA — Application Traffic > Login Schema > Profiles tab.
- Click Add.
- Give it a name and click the pencil icon in the Authentication Schema field.
- In the Create Authentication Login Schema dialog, give it a name (for example, SingleAuth1), then open the LoginSchema folder and select SingleAuth.xml.
You can now customize this schema:
- Click Edit (top right) to change field labels.
- By default, the language is English. If you want another language (or multiple languages), select the language tab you want to customize.
Example customization for the first factor page:
- File name: SingleAuth1_schema.xml (this becomes the XML file name).
- Change the field label from “Password” to “Code”.
- Set the help text to “Your radius code”.
- Set the assistive text to “Please supply your username”.
Click Save. A new login schema will appear in the Create Authentication Login Schema page (it may take a bit of time to show up).
To customize multiple languages:
- Select the second language tab.
- Click Edit, use the same schema name, and change the labels and messages.
- When asked if you want to replace the file, click Yes.
When you are done, click Select (this is important — do not just click OK, or the login schema will not be selected). You return to Configure Authentication Login Schema; then click Create.
Second page: OnlyPassword (AD password only)
Back in the Login Schema page:
- Click Add, give it a name (for example, ldap_page2).
- Click the Authentication Schema pencil.
- Open the LoginSchema folder and select OnlyPassword.xml.
If you do not need to customize the labels or help messages, just click Select and you can skip ahead to the warning section below.
Get Stefano Baronio’s stories in your inbox
Join Medium for free to get updates from this writer.
If you need to localize or customize some messages in OnlyPassword.xml, it is a bit tricky: if you only change the text fields, the translation will not work correctly.
Example (English first):
- Select the English tab and click Edit.
- Give it a schema name and change the messages you need.
- For example, to change the help message under the password field, you must invent a label key for this message.
- Delete the original label and use something like:
onlypassword1_stepup_msg ${http.req.user.name}Save the login schema.
Now customize Italian (if needed):
- Select the Italian tab and click Edit.
- Use the same schema name.
- Under the password field, insert the same message:
onlypassword1_stepup_msg ${http.req.user.name}Click Save and confirm replacing the file. Then click Select to return to Configure Authentication Login Schema.
If this nFactor flow is fronting a NetScaler Gateway with StoreFront behind it:
Click More, scroll down, and enable Single Sign On Credential. This is required to SSO the user into StoreFront.
Click OK, a warning will appear:
Even if you follow the suggestion and adjust the placeholder, the warning tends to persist. Accept it and click OK; the configuration still works.
At this point you have the two login schemas prepared for the two pages.
Step 3 — Build the second-factor label and loginSchema policy
Now create the policy label for the second factor, bind the LDAP policy to it, and create the loginSchema policy for the first page (SingleAuth1).
# Create the policy label to bind the second page login schema
add authentication policylabel ldap_page2 -loginSchema ldap_page2# bind the ldap policy to the policy label
bind authentication policylabel ldap_page2 -policyName ldap_pol -priority 100 -gotoPriorityExpression NEXT# Create the login schema policy to bind the first page login schema (user + code)
add authentication loginSchemaPolicy SingleAuth1 -rule true -action SingleAuth1
Step 4 — Create the authentication vServer and profile
Create the authentication virtual server, bind the login schemas and policies, and then build an authentication profile for the NetScaler Gateway.Bind or change the authentication profile on the NetScaler Gateway:
# Create the authentication virtual server
add authentication vserver Auth-srv-radius SSL 0.0.0.0# Bind the ssl certificate
bind ssl vserver Auth-srv-radius -certkeyName gw.domain.com# Set the ssl profile
set ssl vserver Auth-srv-radius -sslProfile ns_default_ssl_profile_frontend# Bind the first page login schema
bind authentication vserver Auth-srv-radius -policy SingleAuth1 -priority 100 -gotoPriorityExpression END# Bind the nFactor policies we just created
bind authentication vserver Auth-srv-radius -policy radpol -priority 100 -nextFactor ldap_page2 -gotoPriorityExpression NEXT# Create the authentication profile we will bind to the Netscaler Gateway
add authentication authnProfile radius_ldap_auth_prof -authnVsName Auth-srv-radius
Bind or change the authentication profile on the NetScaler Gateway:
# Create the vpn vserver gateway
add vpn vserver ns-gw-radius SSL 10.0.0.114 443 -icaOnly ON -dtls OFF -Listenpolicy NONE -authnProfile radius_ldap_auth_prof
# (plus the other things to create a working gateway)
# Or just bind the auth vserver tothe already existint vpn gateway
set vpn vserver ns-gw-radius -authnProfile radius_ldap_auth_prof Step 5 — Final XML and string customizations (optional but important)
If you customized login schema labels and help messages, there is one last step: update the XML and translation string files so that the new label keys work properly.
Edit the OnlyPassword XML
Edit the /nsconfig/loginschema/OnlyPassword1.xml and adjust the relevant block:
<?xml version="1.0" encoding="UTF-8"?><AuthenticateResponse xmlns="http://citrix.com/authentication/response/1">
<Status>success</Status>
<Result>more-info</Result>
<StateContext/>
<AuthenticationRequirements>
<PostBack>/nf/auth/doAuthentication.do</PostBack>
<CancelPostBack>/nf/auth/doLogoff.do</CancelPostBack>
<CancelButtonText>Cancel</CancelButtonText>
<Requirements>
<Requirement><Credential><ID>passwd</ID><SaveID>ExplicitForms-Password</SaveID><Type>password</Type></Credential><Label><Text>onlypassword1_password</Text><Type>nsg-login-label</Type></Label><Input><Text><Secret>true</Secret><ReadOnly>false</ReadOnly><InitialValue/><Constraint>.+</Constraint></Text></Input></Requirement>
<Requirement><Credential><Type>none</Type></Credential><Label>
<!-- Change this line: -->
<!-- From: -->
<!-- <Text>onlypassword1_hello ${http.req.user.name}, onlypassword_please_enter_password_for_step_up_auth</Text> -->
<!-- To: -->
<Text>onlypassword1_stepup_msg ${http.req.user.name}</Text>
<Type>nsg_confirmation</Type></Label><Input/></Requirement>
<Requirement><Credential><ID>saveCredentials</ID><Type>savecredentials</Type></Credential><Label><Text>onlypassword1_remember_my_password</Text><Type>nsg-login-label</Type></Label><Input><CheckBox><InitialValue>false</InitialValue></CheckBox></Input></Requirement>
<Requirement><Credential><ID>loginBtn</ID><Type>none</Type></Credential><Label><Type>none</Type></Label><Input><Button>onlypassword1_log_on</Button></Input></Requirement>
</Requirements>
</AuthenticationRequirements>
</AuthenticateResponse>Edit the English strings
For English, edit /var/netscaler/logon/LogonPoint/custom/strings.en.json and adjust the entries:
{"singleauth1_schema_user_name":"User name:",
"singleauth1_schema_password":"Code:",
"singleauth1_schema_first_factor":"Your radius code",
"singleauth1_schema_remember_my_password":"Remember my password",
"singleauth1_schema_log_on":"Log On",
"singleauth1_schema_please_supply_either_domain\\username_or_user@fully.qualified.domain":"Please supply your domain username",
"onlypassword1_password":"Password:",
// Change this:
// "onlypassword1_hello ${http.req.user.name},onlypassword_please_enter_password_for_step_up_auth":"onlypassword1_stepup_msg ${http.req.user.name}",
// To this:
"onlypassword1_stepup_msg":"Insert the domain password for user ",
"onlypassword1_remember_my_password":"Remember my password","onlypassword1_log_on":"Log On"
}Finally, reload the login schema configuration so NetScaler picks up the updated XML:
set authentication loginSchema ldap_page2 -authenticationSchema "/nsconfig/loginschema/OnlyPassword1.xml" -SSOCredentials YESWe won’t cover the storefront configuration here.
Open the NetScaler Gateway login page in a new private browser window and test the full login flow.
That’s it
##NetScaler #CitrixADC #CitrixGateway #nFactor #MFA#RADIUS #LDAP #ActiveDirectory #CyberSecurity #AppSecuritynetscaler_nFactor #radius #ldap






