Skip to content

Latest commit

 

History

1,294 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GopherPass 🐹

Self-service password change & reset for Active Directory and LDAP — written in Go.

Give users a simple, secure web interface to change or reset their directory passwords — no tickets, no scripts.

GopherPass Logo

CI CodeQL Template Drift codecov managed by netresearch/.github templates License: MIT WCAG 2.2 AAA

Features

  • Active Directory & LDAP Support: Works with both AD and OpenLDAP directory services
  • Dual Mode Operation: Self-service password change (authenticated) and email-based password reset
  • Configurable Password Policies: Enforce minimum length, numbers, symbols, uppercase, lowercase requirements
  • Security First: LDAPS support, cryptographic token generation, rate limiting, no password storage
  • Real-Time Validation: Client-side validation with immediate feedback on password requirements
  • Accessibility Excellence: WCAG 2.2 Level AAA compliant with full keyboard navigation and screen reader support
  • Modern UX: Dark mode support, adaptive density, responsive design, optimized for password managers
  • Production Ready: Single binary deployment, Docker support, comprehensive logging
  • Developer Friendly: Go backend, TypeScript frontend, Tailwind CSS, embedded assets

Quick Start

Docker (Recommended)

docker run -d -p 3000:3000 \
  -e LDAP_SERVER=ldaps://dc1.example.com:636 \
  -e LDAP_IS_AD=true \
  -e LDAP_BASE_DN=DC=example,DC=com \
  -e LDAP_READONLY_USER=readonly \
  -e LDAP_READONLY_PASSWORD=readonly \
  -e PASSWORD_RESET_ENABLED=true \
  -e SMTP_HOST=smtp.gmail.com \
  -e SMTP_PORT=587 \
  -e SMTP_USERNAME=notifications@example.com \
  -e SMTP_PASSWORD=your_app_password \
  -e SMTP_FROM_ADDRESS=noreply@example.com \
  -e APP_BASE_URL=https://password.example.com \
  ghcr.io/netresearch/ldap-selfservice-password-changer

Access at http://localhost:3000

Native Installation

Prerequisites:

# Clone and build
git clone https://github.com/netresearch/ldap-selfservice-password-changer
cd ldap-selfservice-password-changer
bun install
bun run build

# Configure (create .env.local or use flags)
cp .env.local.example .env.local
# Edit .env.local with your directory server details

# Run
./ldap-selfservice-password-changer

Configuration

GopherPass is configured via environment variables or command-line flags. Key settings:

Directory Connection

  • LDAP_SERVER - Directory server URI (ldaps://server:636)
  • LDAP_IS_AD - Set to true for Active Directory
  • LDAP_BASE_DN - Base DN for user searches
  • LDAP_READONLY_USER - Service account with read access
  • LDAP_READONLY_PASSWORD - Service account password

Password Policy

  • MIN_LENGTH - Minimum password length (default: 8)
  • MIN_NUMBERS - Required numeric characters (default: 1)
  • MIN_SYMBOLS - Required special characters (default: 1)
  • MIN_UPPERCASE - Required uppercase letters (default: 1)
  • MIN_LOWERCASE - Required lowercase letters (default: 1)

Password Reset Feature

  • PASSWORD_RESET_ENABLED - Enable email-based password reset
  • SMTP_HOST / SMTP_PORT - Mail server configuration
  • SMTP_USERNAME / SMTP_PASSWORD - SMTP authentication
  • SMTP_FROM_ADDRESS - Sender email address
  • APP_BASE_URL - Base URL for reset links
  • RESET_IDENTIFIER_MODE - Identifier the reset form accepts: email, username, or both (default: email)
  • RESET_TOKEN_EXPIRY_MINUTES - Token validity (default: 15)
  • RESET_RATE_LIMIT_REQUESTS - Max reset requests per window (default: 3)
  • RESET_RATE_LIMIT_WINDOW_MINUTES - Length of that window (default: 60)

Rate Limiting

Two independent in-memory limiters apply. They are not the same thing and only one of them is configurable.

Limiter Limit Applies to Configurable
Per IP 10 requests / 60 minutes, at most 1000 tracked addresses change-password and request-password-reset No — hardcoded in internal/ratelimit/ip_limiter.go
Per identifier RESET_RATE_LIMIT_REQUESTS per RESET_RATE_LIMIT_WINDOW_MINUTES request-password-reset only Yes

The per-identifier limiter is keyed twice — once by the identifier as typed and again by the account it resolves to — so requesting a reset for the same account under different spellings does not multiply the allowance.

There is no RATE_LIMIT_* variable prefix. If users report being blocked without having hit the reset limit, the per-IP limiter is the likely cause, and changing it requires a code change. Both limiters hold state in memory only, so a restart clears them and a multi-instance deployment limits per instance rather than globally.

Branding (optional)

Every value is optional and the defaults reproduce the stock appearance, so an existing deployment looks unchanged after upgrading.

  • BRANDING_PRODUCT_NAME - Wordmark next to the logo (default: GopherPass)
  • BRANDING_PAGE_TITLE - Browser tab title of the start page (default: derived from the product name)
  • BRANDING_LOGO_ALT - Alternative text for the logo (default: empty, see below)
  • BRANDING_SHOW_ATTRIBUTION - Show the "Built by Netresearch" footer line (default: true)
  • BRANDING_DIR - Directory whose files replace the built-in assets

Replacing assets. Mount a directory and drop in only the files you want to change:

docker run -v /srv/branding:/branding:ro -e BRANDING_DIR=/branding ...
File Purpose
logo.webp Logo in the page header
logo-dark.webp Optional dark-mode variant — see below
favicon.ico, favicon-16x16.png, favicon-32x32.png, apple-touch-icon.png, android-chrome-192x192.png, android-chrome-512x512.png, mstile-150x150.png, safari-pinned-tab.svg Browser and home-screen icons
site.webmanifest Installed-app name and colours
browserconfig.xml Windows tile — see the caveat below

Anything else in the directory aborts startup rather than being ignored, so a typo surfaces immediately. styles.css and js/ are deliberately not replaceable: overriding them would let a deployment silently break the accessibility guarantees the templates rely on. Files must be regular files of at most 2 MiB, and must not be symbolic links pointing outside the directory. Assets you do not supply keep their built-in version.

The directory must be owned by the operator and not writable by anything less privileged — mount it read-only, as above. A file placed there is served publicly and unauthenticated under /static/.

The page's theme-color and msapplication-TileColor are set by <meta> tags in the template and take precedence over the manifest for the document itself; browserconfig.xml is not referenced by any page at all. Overriding site.webmanifest therefore affects the installed-app entry, not the browser chrome, and overriding browserconfig.xml currently has no effect. The manifest's name is also not derived from BRANDING_PRODUCT_NAME — a rebrand has to repeat it in the file.

Kubernetes ConfigMap and Secret volumes work: entries whose name begins with a dot (..data and the timestamped directory kubelet creates) are skipped rather than rejected.

Logo shape and size. The header renders the logo in a square box with object-contain, so a wide wordmark is letterboxed rather than stretched, but it will be small. A roughly square asset around 256×256 gives the best result. Both variants are downloaded on every page load — keep them well under the 2 MiB ceiling.

Dark mode. The app has a light/dark switch, and a dark corporate logo disappears on a dark background. Supply logo-dark.webp and it is shown in dark mode. The switch is class-based, so the variant follows the in-page toggle rather than only the operating-system setting. Note that dark mode requires JavaScript; without it the light logo is always shown. If you remove logo-dark.webp from a running deployment the light logo is served in its place, so the page never shows a broken image — but adding one needs a restart, since the pages are rendered at startup.

Accessibility. The logo is decorative (alt="") as long as the wordmark beside it carries the name; setting BRANDING_LOGO_ALT as well is accepted, but the alt text is ignored while a wordmark is shown, because screen readers would otherwise announce the name twice. If you clear BRANDING_PRODUCT_NAME to show only a logo, you must set BRANDING_LOGO_ALT — otherwise the header consists of a decorative image alone and the brand reaches sighted users only. That combination aborts startup.

The footer link keeps the name GopherPass even on a rebranded deployment: it points at the upstream project, not at your installation.

Hiding the attribution is permitted — the MIT licence does not require it in the UI. The licence and copyright notice in the source still apply.

Email Templates and Headers (optional)

Unset values fall back to the built-in defaults. A bad header name or value, or a SMTP_FROM_NAME containing control characters, aborts startup unconditionally. A bad address (SMTP_FROM_ADDRESS, EMAIL_REPLY_TO) or a bad template (missing file, parse error, undefined field) aborts startup only when password reset is enabled, since neither is used otherwise. Addresses are checked with the RFC parser, so internal senders such as noreply@localhost are accepted.

  • SMTP_FROM_NAME - Display name for the sender (encoded per RFC 2047)
  • EMAIL_REPLY_TO - Reply-To address (validated at startup)
  • EMAIL_TEMPLATE_SUBJECT - Subject line, itself a Go template
  • EMAIL_TEMPLATE_HTML - Path to a Go template for the HTML body
  • EMAIL_TEMPLATE_TEXT - Path to a Go template for the plain-text body
  • SMTP_HEADER_OVERRIDE_* - Raw header injection, one variable per header; the suffix maps _ to - (SMTP_HEADER_OVERRIDE_X_HELPDESK_TOPIC sets X-Helpdesk-Topic). Header names go on the wire in canonical (Go net/textproto) casing — first letter and each letter after a - upper-cased, the rest lower-cased — no matter how the variable was written. Values must not contain control characters: CR, LF, NUL, any other C0 control and DEL are rejected (HTAB is allowed). MIME-Version / Content-Type / Content-Transfer-Encoding cannot be overridden.

Template fields: {{.ResetLink}}, {{.Token}}, {{.BaseURL}}, {{.Recipient}}, {{.ExpiryMinutes}}.

Delivery semantics: To/Cc/Bcc overrides are display-only — the SMTP envelope recipient is always the reset requester, so SMTP_HEADER_OVERRIDE_BCC does not add a delivery target. It is still written as a real, visible Bcc: header line in the message the reset requester receives, so never put an address there that is meant to stay hidden — the override adds no recipient and discloses the address to the user. A cross-domain From-header override creates a From vs envelope MAIL FROM mismatch that can break SPF/DKIM/DMARC alignment and hurt deliverability.

For complete configuration options, run ./ldap-selfservice-password-changer --help

Password Reset Feature

The password reset feature allows users to reset forgotten passwords via secure email-based token verification.

Key Features

  • Email-Based Verification: Secure tokens sent via SMTP (Google Workspace supported)
  • Cryptographic Security: 32-byte tokens generated with crypto/rand
  • Rate Limiting: 3 reset requests per account per hour by default, plus a separate hardcoded per-IP limit — see Rate Limiting
  • Token Expiration: Tokens expire after 15 minutes (configurable)
  • Single-Use Tokens: Tokens cannot be reused after password reset
  • No User Enumeration: Generic responses prevent account discovery
  • Directory Integration: Automatic identifier-to-account lookup (email and/or username, see RESET_IDENTIFIER_MODE)

How It Works

  1. User navigates to /forgot-password and enters their email address or username (per RESET_IDENTIFIER_MODE; the reset link always goes to the account's registered email address)
  2. System looks up user in directory and generates secure token
  3. Reset email sent with link: https://your-domain.com/reset-password?token=XXX
  4. User clicks link, enters new password with real-time validation
  5. Password updated in directory, token marked as used

LDAP/AD Permissions

Security Best Practice: Use dedicated service accounts with minimal permissions.

For Active Directory:

  • Read-only account (LDAP_READONLY_USER): Default Users group permissions
  • Reset account (LDAP_RESET_USER, optional): Grant "Reset password" permission on user OU

For OpenLDAP:

# Read-only access
access to dn.subtree="ou=users,dc=example,dc=com"
    by dn="cn=readonly,dc=example,dc=com" read

# Password reset access (optional dedicated account)
access to attrs=userPassword
    by dn="cn=password-reset,dc=example,dc=com" write
    by self write
    by * auth

Security Recommendations

LDAPS (LDAP over TLS)

Strongly Recommended for Production: Use ldaps:// instead of ldap:// to encrypt credentials in transit.

# ✅ Recommended: Encrypted connection
LDAP_SERVER=ldaps://dc1.example.com:636

# ⚠️  Not recommended: Unencrypted connection (passwords visible on network)
LDAP_SERVER=ldap://dc1.example.com:389

When LDAPS is Required:

  • Active Directory password changes (AD protocol requirement)
  • Production deployments accessible over untrusted networks
  • Compliance requirements (HIPAA, PCI-DSS, SOC 2)

When plain LDAP may be acceptable:

  • Internal trusted networks with network-level encryption (VPN, WireGuard)
  • Development/testing environments
  • Legacy systems where LDAPS deployment is not feasible

GopherPass Behavior:

  • Accepts both ldap:// and ldaps:// connections (non-blocking)
  • Logs warning at startup when using unencrypted connections
  • Provides visibility for security monitoring and compliance auditing

Setting up LDAPS:

  • Ensure your LDAP/AD server has valid TLS certificates configured
  • Use port 636 for LDAPS (vs port 389 for plain LDAP)
  • Test connection: openssl s_client -connect dc1.example.com:636

Documentation

Comprehensive documentation is available in the docs/ directory:

For a complete overview, see the Documentation Index.

Development

Docker Compose (Recommended)

# Copy example environment
cp .env.local.example .env.local
# Edit .env.local with your directory server details

# Start development environment with Mailhog
docker compose --profile dev up

# Application: http://localhost:3000
# Mailhog UI: http://localhost:8025 (view password reset emails)

Native Development

bun install
cp .env.local.example .env.local

# Edit .env.local with your settings

# Run with hot-reload
bun run dev

Project Background

GopherPass was originally developed by Netresearch DTT GmbH for Active Directory environments and later expanded to support OpenLDAP. The project emphasizes security, accessibility, and user experience while maintaining simplicity and ease of deployment.

Contributing

Contributions are welcome! Please:

  • Follow Conventional Commits for commit messages
  • Use gofmt and prettier formatting standards
  • Ensure tests pass and add new tests for features
  • Update documentation for significant changes

License

GopherPass is licensed under the MIT License.


Built with ❤️ by Netresearch DTT GmbH

DocumentationIssuesContributing

About

A simple and fast self-service LDAP (Lightweight Directory Access Protocol) / AD (Active Directory) password changer written in Golang.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

32 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages