Build a Secure Password Change Portal for Segmented Networks

Mike BeckerMike Becker
14 min read

We’re deploying a secure password change portal in a DMZ, accessible only from the Corporate network—OT is locked out. OT users, stuck with RemoteApp access, must hit the Corporate side to reset DMZ passwords. We’re using ASP.NET Core, HTTPS, and enterprise-grade security to get it done, now with self-service resets via email 2FA. Let’s roll!

Mission Objective

Our goal is to deploy a fortified ASP.NET Core web application that enables users to reset their AD passwords within a DMZ environment, with support for both standard password changes (including expired passwords) and self-service password resets for forgotten passwords using email-based 2FA. The portal must:

  • Be accessible exclusively from the Corporate network, blocking OT access.

  • Use HTTPS with a valid SSL certificate.

  • Follow enterprise security best practices, including encrypted credential storage.

  • Support expired password resets (365-day maximum age due to MFA).

  • Provide self-service password reset via email-based 2FA (no Azure SSPR or Duo).

  • Reflect corporate branding with specific colors and a logo.

  • Be replicable across other segmented networks.

Key Constraints:

  • The connection flows from Corporate to DMZ (e.g., via A record https://rds.company.com:8082 or IP https://172.x.x.x:8082).

  • OT users, reliant on RemoteApp, must use a Corporate machine for DMZ password resets.

  • DMZ certificates (Trusted Root CA and Intermediate CA) must be imported on the Corporate side.

  • The Corporate machine must add the DMZ IP (172.x.x.x) to the Trusted Sites GPO.

Note: On the Corporate side, create an A record mapping rds.company.com to the DMZ server IP (e.g., 172.x.x.x) for seamless access. Use https://rds.company.com:8082 for all Corporate-side connections.

Gear Check

Before we deploy, let’s gear up with the right tools:

  • Development Machine:

  • Target Server:

    • Windows Server with IIS locked and loaded.

    • .NET 8 Hosting Bundle (same link as SDK).

    • AD service account with permissions to change and reset passwords in the DMZ domain (e.g., a service account with "Reset Password" permission).

    • SSL certificate with a thumbprint for HTTPS defense (e.g., a valid thumbprint).

  • Network:

    • Corporate network bridge to the DMZ server.

    • DMZ certificates (Trusted Root CA and Intermediate CA) for Corporate-side import.

    • A record mapping rds.company.com to a 172.x.x.x IP on the Corporate side.

  • SMTP Server:

    • IP-restricted SMTP server at 172.x.x.x:25 (no authentication required). Ensure the portal server is whitelisted.
  • Assets:

    • Corporate logo (e.g., company_logo.png).

    • Branding colors (e.g., Orange: #f57f26, Blue: #233269, Gray: #73808a).

Execution Plan

Let’s execute this mission with precision, step by step.

Step 1: Initialize the ASP.NET Core Project

Set up the command center on your development machine.

  • Create the Project: Navigate to a staging directory (e.g., C:\Temp\PasswordPortal on Windows):

  •     cd C:\Temp\PasswordPortal
        dotnet new webapp -n PasswordPortal -f net8.0
        cd PasswordPortal
    
  • Configure the Project File: Edit PasswordPortal.csproj for the mission:

  •   <Project Sdk="Microsoft.NET.Sdk.Web">
        <PropertyGroup>
          <TargetFramework>net8.0</TargetFramework>
          <Nullable>enable</Nullable>
          <ImplicitUsings>enable</ImplicitUsings>
        </PropertyGroup>
        <ItemGroup>
          <PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.2" />
          <PackageReference Include="MailKit" Version="4.3.0" />
        </ItemGroup>
      </Project>
    
  • Restore the arsenal:

  •     dotnet restore
    
Step 2: Build the Password Change and Reset Logic

Assemble the core combat units: credential helper, controller, and UI views.

  • Add Credential Helper: Deploy CredentialHelper.cs in the project root:

  •   using Microsoft.Extensions.Configuration;
    
      public class CredentialHelper
      {
          private readonly IConfiguration _configuration;
    
          public CredentialHelper(IConfiguration configuration)
          {
              _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
          }
    
          public (string username, string password) GetCredential(string targetName)
          {
              string? username = _configuration["ServiceCredentials:Username"];
              string? password = _configuration["ServiceCredentials:Password"];
              if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
              {
                  throw new InvalidOperationException("Service credentials not configured.");
              }
              return (username, password);
          }
      }
    
  • This pulls credentials from the environment.

  • Create the Controller: Drop Controllers\PasswordController.cs into the fray:

  •   using Microsoft.AspNetCore.Mvc;
      using System.DirectoryServices.AccountManagement;
      using System.Runtime.InteropServices;
      using MailKit.Net.Smtp;
      using MimeKit;
      using Microsoft.Extensions.Options;
    
      public class PasswordController : Controller
      {
          private readonly CredentialHelper _credentialHelper;
          private readonly EmailSettings _emailSettings;
          private static readonly Dictionary<string, (string Code, DateTime Expiry)> _verificationCodes = new Dictionary<string, (string, DateTime)>();
          private static readonly object _lock = new object();
    
          public PasswordController(CredentialHelper credentialHelper, IOptions<EmailSettings> emailSettings)
          {
              _credentialHelper = credentialHelper;
              _emailSettings = emailSettings.Value;
          }
    
          [HttpGet]
          public IActionResult Index()
          {
              if (TempData["SuccessMessage"] != null)
              {
                  ViewBag.Message = TempData["SuccessMessage"].ToString();
              }
              return View();
          }
    
          [HttpPost]
          public IActionResult ChangePassword(string username, string currentPassword, string newPassword)
          {
              if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(currentPassword) || string.IsNullOrEmpty(newPassword))
              {
                  ViewBag.Message = "All fields are required.";
                  return View("Index");
              }
    
              if (!OperatingSystem.IsWindows())
              {
                  ViewBag.Message = "This application is only supported on Windows.";
                  return View("Index");
              }
    
              try
              {
                  var (serviceAccountUsername, serviceAccountPassword) = _credentialHelper.GetCredential("ResetPortal");
                  if (string.IsNullOrEmpty(serviceAccountUsername) || string.IsNullOrEmpty(serviceAccountPassword))
                  {
                      ViewBag.Message = "Service account credentials not found.";
                      return View("Index");
                  }
    
                  using (var context = new PrincipalContext(ContextType.Domain, "dmz.domain.local", serviceAccountUsername, serviceAccountPassword))
                  {
                      var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
                      if (user == null)
                      {
                          ViewBag.Message = "User not found in AD.";
                          return View("Index");
                      }
    
                      bool isPasswordExpired = false;
                      try
                      {
                          if (!context.ValidateCredentials(username, currentPassword))
                          {
                              if (user.LastPasswordSet.HasValue)
                              {
                                  var maxPasswordAge = TimeSpan.FromDays(365);
                                  var passwordAge = DateTime.Now - user.LastPasswordSet.Value;
                                  if (passwordAge > maxPasswordAge)
                                  {
                                      isPasswordExpired = true;
                                  }
                              }
                              if (!isPasswordExpired)
                              {
                                  ViewBag.Message = "Current password incorrect.";
                                  return View("Index");
                              }
                          }
                      }
                      catch (Exception)
                      {
                          isPasswordExpired = true;
                      }
    
                      if (isPasswordExpired)
                      {
                          user.SetPassword(newPassword);
                          user.Save();
                          ViewBag.Message = "Password reset successfully (expired password).";
                      }
                      else
                      {
                          user.ChangePassword(currentPassword, newPassword);
                          user.Save();
                          ViewBag.Message = "Password changed successfully!";
                      }
                  }
              }
              catch (Exception ex)
              {
                  ViewBag.Message = $"Error: {ex.Message}";
              }
    
              return View("Index");
          }
    
          [HttpGet]
          public IActionResult ForgotPassword()
          {
              return View("ForgotPassword");
          }
    
          [HttpPost]
          public IActionResult ForgotPasswordRequest(string username)
          {
              if (string.IsNullOrEmpty(username))
              {
                  ViewBag.ErrorMessage = "Username is required.";
                  return View("ForgotPassword");
              }
    
              if (!OperatingSystem.IsWindows())
              {
                  ViewBag.ErrorMessage = "This application is only supported on Windows.";
                  return View("ForgotPassword");
              }
    
              try
              {
                  var (serviceAccountUsername, serviceAccountPassword) = _credentialHelper.GetCredential("ResetPortal");
                  using (var context = new PrincipalContext(ContextType.Domain, "dmz.domain.local", serviceAccountUsername, serviceAccountPassword))
                  {
                      var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
                      if (user == null)
                      {
                          ViewBag.ErrorMessage = "User not found in AD.";
                          return View("ForgotPassword");
                      }
    
                      var code = new Random().Next(100000, 999999).ToString();
                      var expiry = DateTime.Now.AddMinutes(10);
                      lock (_lock)
                      {
                          _verificationCodes[username] = (code, expiry);
                      }
    
                      var message = new MimeMessage();
                      message.From.Add(new MailboxAddress("DMZ Portal", _emailSettings.FromAddress));
                      message.To.Add(new MailboxAddress("", user.EmailAddress ?? "default@company.com"));
                      message.Subject = "Password Reset Verification Code";
                      message.Body = new TextPart("plain") { Text = $"Your verification code is: {code}. This code expires in 10 minutes." };
    
                      using (var client = new SmtpClient())
                      {
                          client.ServerCertificateValidationCallback = (s, c, h, e) => true;
                          client.Connect("172.x.x.x", 25, false);
                          client.Send(message);
                          client.Disconnect(true);
                      }
    
                      ViewBag.InfoMessage = "A verification code has been sent to your email. Please enter it below.";
                      TempData["Username"] = username;
                      return View("ForgotPasswordVerify");
                  }
              }
              catch (Exception ex)
              {
                  ViewBag.ErrorMessage = $"Error sending verification code: {ex.Message}";
                  return View("ForgotPassword");
              }
          }
    
          [HttpPost]
          public IActionResult ForgotPasswordVerify(string username, string verificationCode, string newPassword)
          {
              string redirectUrl = "/Password/Index";
              try
              {
                  ViewBag.InfoMessage = null;
                  ViewBag.ErrorMessage = null;
    
                  if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(verificationCode) || string.IsNullOrEmpty(newPassword))
                  {
                      ViewBag.ErrorMessage = "All fields are required.";
                      TempData["Username"] = username ?? TempData["Username"]?.ToString();
                      return View();
                  }
    
                  if (!OperatingSystem.IsWindows())
                  {
                      ViewBag.ErrorMessage = "This application is only supported on Windows.";
                      TempData["Username"] = username ?? TempData["Username"]?.ToString();
                      return View();
                  }
    
                  (string Code, DateTime Expiry) storedCode;
                  lock (_lock)
                  {
                      if (!_verificationCodes.TryGetValue(username, out storedCode))
                      {
                          ViewBag.ErrorMessage = "Verification code not found for this username.";
                          TempData["Username"] = username ?? TempData["Username"]?.ToString();
                          return View();
                      }
                      if (storedCode.Code != verificationCode)
                      {
                          ViewBag.ErrorMessage = "Invalid verification code.";
                          TempData["Username"] = username ?? TempData["Username"]?.ToString();
                          return View();
                      }
                      if (DateTime.Now > storedCode.Expiry)
                      {
                          ViewBag.ErrorMessage = "Verification code has expired.";
                          TempData["Username"] = username ?? TempData["Username"]?.ToString();
                          return View();
                      }
                      _verificationCodes.Remove(username);
                  }
    
                  var (serviceAccountUsername, serviceAccountPassword) = _credentialHelper.GetCredential("ResetPortal");
                  if (string.IsNullOrEmpty(serviceAccountUsername) || string.IsNullOrEmpty(serviceAccountPassword))
                  {
                      ViewBag.ErrorMessage = "Service account credentials not found.";
                      TempData["Username"] = username ?? TempData["Username"]?.ToString();
                      return View();
                  }
    
                  using (var context = new PrincipalContext(ContextType.Domain, "dmz.domain.local", serviceAccountUsername, serviceAccountPassword))
                  {
                      var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);
                      if (user == null)
                      {
                          ViewBag.ErrorMessage = "User not found in AD.";
                          TempData["Username"] = username ?? TempData["Username"]?.ToString();
                          return View();
                      }
    
                      user.SetPassword(newPassword);
                      user.Save();
                      TempData["SuccessMessage"] = "Password reset successfully! You can now log in with your new password.";
                  }
    
                  redirectUrl = Url.Action("Index", "Password", new { scheme = Request.Scheme, host = Request.Host }) ?? "/Password/Index";
              }
              catch (Exception ex)
              {
                  ViewBag.ErrorMessage = $"Error resetting password: {ex.Message}";
                  TempData["Username"] = username ?? TempData["Username"]?.ToString();
                  return View();
              }
    
              return Redirect(redirectUrl);
          }
      }
    

    Create _ViewImports.cshtml:

    • In Views\_ViewImports.cshtml, add:

    •   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
      
  • Craft the UI View: In Views\Password\Index.cshtml, deploy the front line:

  •   @using Microsoft.AspNetCore.Mvc.TagHelpers
      @{
          ViewData["Title"] = "DMZ Password Change Portal";
      }
    
      <!DOCTYPE html>
      <html>
      <head>
          <title>DMZ Password Change Portal</title>
          <style>
              body {
                  background-color: #233269;
                  display: flex;
                  justify-content: center;
                  align-items: center;
                  height: 100vh;
                  margin: 0;
                  font-family: Arial, sans-serif;
              }
              .password-container {
                  background-color: white;
                  padding: 20px;
                  border-radius: 10px;
                  width: 300px;
                  text-align: center;
                  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
              }
              .company-logo {
                  background-image: url('/company_logo.png');
                  background-size: contain;
                  background-repeat: no-repeat;
                  background-position: center;
                  width: 300px;
                  height: 60px;
                  display: block;
                  margin: 0 auto 10px auto;
                  text-indent: -9999px;
              }
              .form-group {
                  margin-bottom: 15px;
                  text-align: left;
              }
              .form-group label {
                  color: #73808a;
                  display: block;
                  margin-bottom: 5px;
              }
              .form-group input {
                  width: 100%;
                  padding: 8px;
                  box-sizing: border-box;
                  border: 1px solid #73808a;
                  border-radius: 4px;
              }
              .btn-change {
                  background-color: #f57f26;
                  color: white;
                  padding: 10px 20px;
                  border: none;
                  border-radius: 5px;
                  cursor: pointer;
                  width: 100%;
              }
              .btn-change:hover {
                  background-color: #d66e1e;
              }
              .message {
                  color: #233269;
                  margin-top: 10px;
              }
              .error {
                  color: #d32f2f;
                  margin-top: 10px;
              }
              .forgot-password {
                  margin-top: 10px;
                  display: block;
                  color: #233269;
                  text-decoration: underline;
              }
              .forgot-password:hover {
                  color: #f57f26;
              }
              .success {
                  color: #2e7d32;
                  margin-top: 10px;
              }
          </style>
      </head>
      <body>
          <div class="password-container">
              <div class="company-logo">Company</div>
              <h2 style="color: #f57f26;">DMZ Password Change Portal</h2>
              <form asp-controller="Password" asp-action="ChangePassword" method="post">
                  <div class="form-group">
                      <label for="username">Username:</label>
                      <input type="text" id="username" name="username" required />
                  </div>
                  <div class="form-group">
                      <label for="currentPassword">Current Password:</label>
                      <input type="password" id="currentPassword" name="currentPassword" required />
                  </div>
                  <div class="form-group">
                      <label for="newPassword">New Password:</label>
                      <input type="password" id="newPassword" name="newPassword" required />
                  </div>
                  <button type="submit" class="btn-change">Change Password</button>
                  @if (!string.IsNullOrEmpty(ViewBag.Message))
                  {
                      <div class="@(ViewBag.Message.Contains("successfully") ? "message" : "error")">@ViewBag.Message</div>
                  }
                  @if (!string.IsNullOrEmpty(TempData["SuccessMessage"]?.ToString()))
                  {
                      <div class="success">@TempData["SuccessMessage"]</div>
                  }
              </form>
              <a href="@Url.Action("ForgotPassword", "Password")" class="forgot-password">Forgot Password?</a>
          </div>
      </body>
      </html>
    
  • ForgotPassword.cshtml (in Views\Password\):

    •     @using Microsoft.AspNetCore.Mvc.TagHelpers
        @{
            ViewData["Title"] = "Forgot Password";
        }
      
        <!DOCTYPE html>
        <html>
        <head>
            <title>DMZ Password Change Portal - Forgot Password</title>
            <style>
                body {
                    background-color: #233269;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    height: 100vh;
                    margin: 0;
                    font-family: Arial, sans-serif;
                }
                .password-container {
                    background-color: white;
                    padding: 20px;
                    border-radius: 10px;
                    width: 300px;
                    text-align: center;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                }
                .company-logo {
                    background-image: url('/company_logo.png');
                    background-size: contain;
                    background-repeat: no-repeat;
                    background-position: center;
                    width: 300px;
                    height: 60px;
                    display: block;
                    margin: 0 auto 10px auto;
                    text-indent: -9999px;
                }
                .form-group {
                    margin-bottom: 15px;
                    text-align: left;
                }
                .form-group label {
                    color: #73808a;
                    display: block;
                    margin-bottom: 5px;
                }
                .form-group input {
                    width: 100%;
                    padding: 8px;
                    box-sizing: border-box;
                    border: 1px solid #73808a;
                    border-radius: 4px;
                }
                .btn-change {
                    background-color: #f57f26;
                    color: white;
                    padding: 10px 20px;
                    border: none;
                    border-radius: 5px;
                    cursor: pointer;
                    width: 100%;
                }
                .btn-change:hover {
                    background-color: #d66e1e;
                }
                .message {
                    color: #233269;
                    margin-top: 10px;
                }
                .error {
                    color: #d32f2f;
                    margin-top: 10px;
                }
                .forgot-password {
                    margin-top: 10px;
                    display: block;
                    color: #233269;
                    text-decoration: underline;
                }
                .forgot-password:hover {
                    color: #f57f26;
                }
            </style>
        </head>
        <body>
            <div class="password-container">
                <div class="company-logo">Company</div>
                <h2 style="color: #f57f26;">Forgot Password</h2>
                <p>Enter your username to receive a verification code via email.</p>
                <form asp-controller="Password" asp-action="ForgotPasswordRequest" method="post">
                    <div class="form-group">
                        <label for="username">Username:</label>
                        <input type="text" id="username" name="username" required />
                    </div>
                    <button type="submit" class="btn-change">Send Verification Code</button>
                    @if (!string.IsNullOrEmpty(ViewBag.ErrorMessage))
                    {
                        <div class="error">@ViewBag.ErrorMessage</div>
                    }
                </form>
                <a href="@Url.Action("Index", "Password")" class="forgot-password" style="margin-top: 10px; display: block; color: #233269; text-decoration: underline;">Back to Password Change</a>
            </div>
        </body>
        </html>
      
  • ForgotPasswordVerify.cshtml (in Views\Password\)

      @using Microsoft.AspNetCore.Mvc.TagHelpers
      @{
          ViewData["Title"] = "Forgot Password Verify";
      }
    
      <!DOCTYPE html>
      <html>
      <head>
          <title>DMZ Password Change Portal - Forgot Password Verify</title>
          <style>
              body {
                  background-color: #233269;
                  display: flex;
                  justify-content: center;
                  align-items: center;
                  height: 100vh;
                  margin: 0;
                  font-family: Arial, sans-serif;
              }
              .password-container {
                  background-color: white;
                  padding: 20px;
                  border-radius: 10px;
                  width: 300px;
                  text-align: center;
                  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
              }
              .company-logo {
                  background-image: url('/company_logo.png');
                  background-size: contain;
                  background-repeat: no-repeat;
                  background-position: center;
                  width: 300px;
                  height: 60px;
                  display: block;
                  margin: 0 auto 10px auto;
                  text-indent: -9999px;
              }
              .form-group {
                  margin-bottom: 15px;
                  text-align: left;
              }
              .form-group label {
                  color: #73808a;
                  display: block;
                  margin-bottom: 5px;
              }
              .form-group input {
                  width: 100%;
                  padding: 8px;
                  box-sizing: border-box;
                  border: 1px solid #73808a;
                  border-radius: 4px;
              }
              .btn-change {
                  background-color: #f57f26;
                  color: white;
                  padding: 10px 20px;
                  border: none;
                  border-radius: 5px;
                  cursor: pointer;
                  width: 100%;
              }
              .btn-change:hover {
                  background-color: #d66e1e;
              }
              .message {
                  color: #233269;
                  margin-top: 10px;
              }
              .error {
                  color: #d32f2f;
                  margin-top: 10px;
              }
          </style>
      </head>
      <body>
          <div class="password-container">
              <div class="company-logo">Company</div>
              <h2 style="color: #f57f26;">Forgot Password Verify</h2>
              @if (!string.IsNullOrEmpty(ViewBag.InfoMessage))
              {
                  <p class="message">@ViewBag.InfoMessage</p>
              }
              <p>Enter the verification code sent to your email and your new password.</p>
              <form asp-controller="Password" asp-action="ForgotPasswordVerify" method="post">
                  <input type="hidden" name="username" value="@TempData["Username"]" />
                  <div class="form-group">
                      <label for="verificationCode">Verification Code:</label>
                      <input type="text" id="verificationCode" name="verificationCode" required />
                  </div>
                  <div class="form-group">
                      <label for="newPassword">New Password:</label>
                      <input type="password" id="newPassword" name="newPassword" required />
                  </div>
                  <button type="submit" class="btn-change">Reset Password</button>
                  @if (!string.IsNullOrEmpty(ViewBag.ErrorMessage))
                  {
                      <div class="error">@ViewBag.ErrorMessage</div>
                  }
              </form>
          </div>
      </body>
      </html>
    
  • Add Logo to wwwroot:

    • Create wwwroot and add your logo:

    •     New-Item -Path "C:\Temp\PasswordPortal\wwwroot" -ItemType Directory -Force
      
    • Copy logo.png to C:\Temp\PasswordPortal\wwwroot\logo.png.

Step 3: Deploy to the DMZ Server
  • Prepare the Server:

  • Set Environment Variables:

    • Open PowerShell as Administrator:

    • ```powershell


    * * Replace \[your-service-account\] and \[your-secure-password\] with your AD service account details and adjust the FromAddress as needed.

        * Reboot to apply.

* **Create Deployment Script**:

    * Save as C:\\tools\\[DeployPasswordPortal.ps](http://DeployPasswordPortal.ps)1:

    * ```powershell
        # Log file
        $LogFile = "C:\ProgramData\PasswordPortalLog.txt"
        function Write-Log { param ([string]$msg); Add-Content -Path $LogFile -Value "$(Get-Date): $msg" }

        # Create app directory
        $appPath = "C:\inetpub\PasswordPortal"
        if (-not (Test-Path $appPath)) {
            New-Item -Path $appPath -ItemType Directory -Force
            Write-Log "Created app directory : $appPath"
        }

        # Set environment variables for the application pool
        $envName1 = "ServiceCredentials__Username"
        $envValue1 = "[your-service-account]"
        $envName2 = "ServiceCredentials__Password"
        $envValue2 = "[your-secure-password]"
        $envName3 = "EmailSettings__FromAddress"
        $envValue3 = "no-reply@company.com"
        [Environment]::SetEnvironmentVariable($envName1, $envValue1, [EnvironmentVariableTarget]::Machine)
        [Environment]::SetEnvironmentVariable($envName2, $envValue2, [EnvironmentVariableTarget]::Machine)
        [Environment]::SetEnvironmentVariable($envName3, $envValue3, [EnvironmentVariableTarget]::Machine)
        Write-Log "Set environment variables: $envName1, $envName2, and $envName3"

        # Create log directory and set permissions
        $logPath = "$appPath\logs"
        if (-not (Test-Path $logPath)) {
            New-Item -Path $logPath -ItemType Directory -Force
            Write-Log "Created log directory : $logPath"
        }
        $acl = Get-Acl $logPath
        $rule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS AppPool\PasswordResetPortal", "Modify", "ContainerInherit, ObjectInherit", "None", "Allow")
        $acl.AddAccessRule($rule)
        Set-Acl $logPath $acl
        Write-Log "Set Modify permissions for IIS AppPool\PasswordResetPortal on $logPath"

        # Prepare project
        Set-Location "C:\Temp\PasswordPortal"
        if (-not (Test-Path "PasswordPortal.csproj")) {
            Write-Log "Project file not found, manual copy required"
        } else {
            Write-Log "Project file exists, proceeding with publish"
        }

        dotnet publish -o $appPath -c Release
        Write-Log "Published ASP.NET Core app to $appPath"

        # Configure IIS site with HTTP and HTTPS bindings
        Import-Module WebAdministration -ErrorAction Stop
        if (-not (Get-Website -Name "PasswordPortal")) {
            New-Website -Name "PasswordPortal" -PhysicalPath $appPath -Port 8081 -HostHeader "localhost" -ApplicationPool "PasswordResetPortal"
            Write-Log "Created IIS site PasswordPortal : port 8081 with application pool PasswordResetPortal"
        }
        if (-not (Get-WebBinding -Name "PasswordPortal" -Protocol https -Port 8082 -ErrorAction SilentlyContinue)) {
            New-WebBinding -Name "PasswordPortal" -IPAddress "*" -Port 8082 -Protocol https
            $certThumbprint = "[your-thumbprint]"
            $binding = Get-WebBinding -Name "PasswordPortal" -Protocol https -Port 8082
            $binding.AddSslCertificate($certThumbprint, "my")
            Write-Log "Added HTTPS binding on port 8082 with SSL certificate thumbprint $certThumbprint"
        }

        Start-Website -Name "PasswordPortal" -ErrorAction SilentlyContinue
        Write-Log "Started IIS site : http://localhost:8081 and https://localhost:8082"
  • Deploy:

    • Run the script:

    •     C:\tools\DeployPasswordPortal.ps1
      
    • Check C:\ProgramData\PasswordPortalLog.txt.

  • Step 4: Secure Corporate Network Access
    • Import DMZ Certificates:

      • On the Corporate network, import the DMZ Trusted Root CA and Intermediate CA:

        • Open certmgr.msc.

        • Navigate to Trusted Root Certification Authorities > Certificates.

        • Import the DMZ Trusted Root CA certificate.

        • Repeat for Intermediate CA in Intermediate Certification Authorities.

    • Add DMZ IP to Trusted Sites:

      • Create a GPO:

        • Open Group Policy Management.

        • Create a new GPO (e.g., "DMZ Trusted Sites").

        • Edit: Computer Configuration > Policies > Administrative Templates > Windows Components > Internet Explorer > Security Page > Site to Zone Assignment List.

        • Enable and add:

          • Value Name: <dmz-ip> (e.g., 172.x.x.x)

          • Value: 2 (Trusted Sites zone).

      • Apply and run

      •     gpupdate /force
        
      • Create A Record:

        • On the Corporate side, create an A record mapping rds.company.com to a 172.x.x.x IP (e.g., the DMZ server IP) in your DNS server. This ensures seamless access from the Corporate network.
      • Access the Portal:

        • From a Corporate-side machine, navigate to https://rds.company.com:8082.

        • Test Password Change:

          • Username: A valid AD user.

          • Current Password: Existing password.

          • New Password: New password.

        • Test Forgot Password:

          • Click "Forgot Password?", enter a username, receive the email, and enter the code and new password.
        • Verify via AD login (e.g., RemoteApp).

Step 5: Finalize and Document
  • Verify Functionality:

    • Ensure the portal loads, displays branding, and resets passwords.
  • Document:

    • Create a file with:

      • URL: https://rds.company.com:8082

      • AD Requirements: Password policy (365-day maximum age, minimum length 15, complexity enabled, 24 history).

      • UI Branding: Colors (Orange: #f57f26, Blue: #233269, Gray: #73808a) and logo (company_logo.png).

      • Deployment: Script and prerequisites.

      • Corporate Access: Certificate import, Trusted Sites GPO, A record setup.

    • Development Notes:

      • Platform: The portal uses Windows-specific APIs (System.DirectoryServices.AccountManagement) and is designed to run on Windows Server with IIS. Platform checks (OperatingSystem.IsWindows()) are implemented to ensure compatibility.

      • SMTP Configuration: Email-based 2FA uses an IP-restricted SMTP server at 172.x.x.x:25 with no authentication. SSL hostname verification is bypassed due to a certificate mismatch (server cert for the DMZ hostname, connected via IP). Ensure the portal server is whitelisted and trusted within the DMZ.

      • Password Policy: Supports a 365-day maximum password age due to MFA usage, with resets for expired passwords using SetPassword.

      • Self-Service Reset: Users can reset forgotten passwords via email-based 2FA, requiring a valid AD EmailAddress attribute or a fallback email.

    • Document Any Deviations:

      • Note any site-specific adjustments (e.g., different server name, certificate thumbprint, or SMTP server IP).

Mission Debrief

Mission accomplished, agents! We’ve fortified a DMZ password change portal, accessible only from the Corporate network, with OT users relying on RemoteApp access to bridge the gap. Using ASP.NET Core, AD integration, and IIS, we’ve built a scalable solution that withstands enterprise scrutiny, now with self-service password resets via email 2FA. The key takeaway? Segment your networks, secure your creds, and always have a backup plan—OT’s RemoteApp lifeline proves it. This is Mainframe79 signing off—keep the firewalls burning bright!

0
Subscribe to my newsletter

Read articles from Mike Becker directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mike Becker
Mike Becker