Keycloak Email Configuration: SMTP Setup and Templates

Guilliano Molaire Guilliano Molaire Updated May 29, 2026 12 min read

Last updated: March 2026

Email is a critical component of any authentication system. Users need verification emails to confirm their accounts, password reset links when they forget credentials, and OTP codes for multi-factor authentication. If email does not work, your users cannot complete these flows, and your support team handles requests that should be automated.

This guide covers SMTP configuration for Keycloak with four popular email providers, custom email template creation using FreeMarker, and troubleshooting common issues.

SMTP Configuration via Admin Console

The quickest way to configure email is through the Keycloak admin console:

  1. Navigate to Realm Settings > Email.
  2. Fill in the SMTP settings.
  3. Click Test Connection to verify.

The fields map to these settings:

Field Description
From The sender email address (e.g., [email protected])
From Display Name The sender name shown in email clients
Reply To Where replies should go (optional)
Host SMTP server hostname
Port SMTP port (usually 587 for STARTTLS, 465 for SSL)
Enable SSL Use implicit SSL (port 465)
Enable StartTLS Use STARTTLS (port 587, recommended)
Username SMTP authentication username
Password SMTP authentication password

Provider-Specific Configuration

Amazon SES

Amazon Simple Email Service is the most cost-effective option for AWS-hosted Keycloak deployments.

Prerequisites:

  1. Verify your sending domain in SES.
  2. Create SMTP credentials in the SES console (these are different from IAM credentials).
  3. If your SES account is in sandbox mode, verify recipient email addresses for testing.

Keycloak configuration:

# Environment variables
[email protected]
KC_SPI_EMAIL_SENDER_DEFAULT_FROM_DISPLAY_NAME=Your Company
KC_SPI_EMAIL_SENDER_DEFAULT_HOST=email-smtp.us-east-1.amazonaws.com
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=587
KC_SPI_EMAIL_SENDER_DEFAULT_STARTTLS=true
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=true
KC_SPI_EMAIL_SENDER_DEFAULT_USER=AKIAXXXXXXXXXXXXXXXX
KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD=your-ses-smtp-password

Or via the Admin REST API:

curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  "https://keycloak.example.com/admin/realms/myrealm" 
  -d '{
    "smtpServer": {
      "from": "[email protected]",
      "fromDisplayName": "Your Company",
      "host": "email-smtp.us-east-1.amazonaws.com",
      "port": "587",
      "starttls": "true",
      "auth": "true",
      "user": "AKIAXXXXXXXXXXXXXXXX",
      "password": "your-ses-smtp-password"
    }
  }'

SES-specific considerations:

  • Use the SMTP endpoint for your SES region (e.g., email-smtp.us-east-1.amazonaws.com).
  • SMTP credentials are NOT IAM access keys. Generate them in the SES console under SMTP Settings.
  • Production sending requires moving out of SES sandbox mode. Request production access through the AWS console.
  • Set up SNS notifications for bounces and complaints to maintain sender reputation.

SendGrid

SendGrid (now part of Twilio) provides a dedicated SMTP relay with analytics.

Prerequisites:

  1. Create a SendGrid account and verify your sending domain.
  2. Generate an API key with Mail Send permissions.

Keycloak configuration:

KC_SPI_EMAIL_SENDER_DEFAULT_HOST=smtp.sendgrid.net
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=587
KC_SPI_EMAIL_SENDER_DEFAULT_STARTTLS=true
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=true
KC_SPI_EMAIL_SENDER_DEFAULT_USER=apikey
KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxx

Note: The username is literally apikey (not your account name). The password is your SendGrid API key.

Mailgun

Mailgun provides SMTP relay with email validation and analytics.

Prerequisites:

  1. Add and verify your sending domain in Mailgun.
  2. Get your SMTP credentials from the domain settings.

Keycloak configuration:

KC_SPI_EMAIL_SENDER_DEFAULT_HOST=smtp.mailgun.org
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=587
KC_SPI_EMAIL_SENDER_DEFAULT_STARTTLS=true
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=true
[email protected]
KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD=your-mailgun-smtp-password

Gmail (Development Only)

Gmail works for development and testing but has sending limits (500 emails/day) that make it unsuitable for production.

Prerequisites:

  1. Enable 2-Step Verification on your Google account.
  2. Generate an App Password (Google Account > Security > App passwords).

Keycloak configuration:

KC_SPI_EMAIL_SENDER_DEFAULT_HOST=smtp.gmail.com
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=587
KC_SPI_EMAIL_SENDER_DEFAULT_STARTTLS=true
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=true
[email protected]
KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD=your-app-password

For local development, consider using Mailpit or MailHog to capture emails without sending them:

# Run Mailpit locally
docker run -p 1025:1025 -p 8025:8025 axllent/mailpit

# Configure Keycloak to use Mailpit
KC_SPI_EMAIL_SENDER_DEFAULT_HOST=localhost
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=1025
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=false

Access the Mailpit web UI at http://localhost:8025 to view captured emails. For Docker-based local development, use the Keycloak Docker Compose Generator and add Mailpit to the compose file.

Environment Variable Configuration

For production deployments, configure SMTP via environment variables rather than the admin console. This keeps credentials out of the database and enables secrets management integration.

All SMTP settings can be set via environment variables with the prefix KC_SPI_EMAIL_SENDER_DEFAULT_:

# Complete environment variable configuration
[email protected]
KC_SPI_EMAIL_SENDER_DEFAULT_FROM_DISPLAY_NAME=Your Company
[email protected]
KC_SPI_EMAIL_SENDER_DEFAULT_REPLY_TO_DISPLAY_NAME=Support Team
KC_SPI_EMAIL_SENDER_DEFAULT_HOST=smtp.provider.com
KC_SPI_EMAIL_SENDER_DEFAULT_PORT=587
KC_SPI_EMAIL_SENDER_DEFAULT_STARTTLS=true
KC_SPI_EMAIL_SENDER_DEFAULT_SSL=false
KC_SPI_EMAIL_SENDER_DEFAULT_AUTH=true
KC_SPI_EMAIL_SENDER_DEFAULT_USER=smtp-username
KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD=smtp-password
[email protected]

In Kubernetes, store the SMTP password in a Secret:

apiVersion: v1
kind: Secret
metadata:
  name: keycloak-smtp
type: Opaque
stringData:
  SMTP_PASSWORD: "your-smtp-password"

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
spec:
  template:
    spec:
      containers:
      - name: keycloak
        env:
        - name: KC_SPI_EMAIL_SENDER_DEFAULT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: keycloak-smtp
              key: SMTP_PASSWORD

Email Template Customization

Keycloak uses FreeMarker templates for email generation. The default templates are functional but generic. Customizing them reinforces your brand and improves user trust.

Template Types

Keycloak includes these email templates:

Template Trigger
email-verification.ftl User registers or admin triggers verification
email-verification-with-code.ftl Verification using an OTP code instead of a link
password-reset.ftl User requests a password reset
executeActions.ftl Admin triggers required actions (verify email, update password, etc.)
email-update-confirmation.ftl User changes their email address
event-login_error.ftl Login error notification
event-remove_totp.ftl TOTP removal notification
event-update_password.ftl Password change notification
event-update_totp.ftl TOTP setup notification
identity-provider-link.ftl Identity provider account linking
org-invite.ftl Organization invitation

Creating Custom Templates

Create a custom email theme directory. See Keycloak Theme Customization for the full theme structure.

Here is a custom email-verification.ftl with branded styling:

<html>
<head>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
                         Helvetica, Arial, sans-serif;
            background-color: #f1f5f9;
            margin: 0;
            padding: 20px;
            color: #1e293b;
        }
        .email-container {
            max-width: 560px;
            margin: 0 auto;
            background: #ffffff;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }
        .email-header {
            background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
            padding: 32px;
            text-align: center;
        }
        .email-header img {
            height: 36px;
        }
        .email-body {
            padding: 32px;
            line-height: 1.6;
        }
        .email-body h2 {
            margin-top: 0;
            color: #0f172a;
            font-size: 22px;
        }
        .cta-button {
            display: inline-block;
            padding: 14px 36px;
            background: linear-gradient(135deg, #3b82f6, #1e40af);
            color: #ffffff !important;
            text-decoration: none;
            border-radius: 8px;
            font-weight: 600;
            font-size: 15px;
            margin: 24px 0;
        }
        .link-text {
            word-break: break-all;
            color: #3b82f6;
            font-size: 13px;
        }
        .email-footer {
            padding: 24px 32px;
            border-top: 1px solid #e2e8f0;
            color: #64748b;
            font-size: 13px;
            line-height: 1.5;
        }
        .expiry-note {
            background: #f8fafc;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 12px 16px;
            font-size: 13px;
            color: #64748b;
            margin-top: 24px;
        }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="email-header">
            <img src="https://your-domain.com/images/logo-white.svg"
                 alt="${realmName}" />
        </div>
        <div class="email-body">
            <h2>${msg("emailVerificationSubject")}</h2>
            <p>Hi ${user.firstName!"there"},</p>
            <p>Thanks for creating your account. To get started, please verify
               your email address by clicking the button below.</p>
            <p style="text-align: center;">
                <a href="${link}" class="cta-button">Verify Email Address</a>
            </p>
            <p>Or copy and paste this link into your browser:</p>
            <p class="link-text">${link}</p>
            <div class="expiry-note">
                This link will expire in ${linkExpirationFormatter(linkExpiration)}.
                If you did not create an account, you can safely ignore this email.
            </div>
        </div>
        <div class="email-footer">
            <p>This email was sent by ${realmName}. If you have questions,
               contact our support team.</p>
        </div>
    </div>
</body>
</html>

Password Reset Template

Create password-reset.ftl:

<html>
<head>
    <!-- Use the same style block as email-verification.ftl -->
    <style>
        /* ... same styles as above ... */
    </style>
</head>
<body>
    <div class="email-container">
        <div class="email-header">
            <img src="https://your-domain.com/images/logo-white.svg"
                 alt="${realmName}" />
        </div>
        <div class="email-body">
            <h2>Reset Your Password</h2>
            <p>Hi ${user.firstName!"there"},</p>
            <p>We received a request to reset the password for your account.
               Click the button below to choose a new password.</p>
            <p style="text-align: center;">
                <a href="${link}" class="cta-button">Reset Password</a>
            </p>
            <p>Or copy and paste this link into your browser:</p>
            <p class="link-text">${link}</p>
            <div class="expiry-note">
                This link will expire in ${linkExpirationFormatter(linkExpiration)}.
                If you did not request a password reset, your account is safe and
                you can ignore this email. Someone may have entered your email
                address by mistake.
            </div>
        </div>
        <div class="email-footer">
            <p>For security, this link can only be used once. If you need to
               reset your password again, visit the login page and click
               "Forgot Password."</p>
        </div>
    </div>
</body>
</html>

OTP Email Template

If you use email-based OTP for multi-factor authentication, customize email-verification-with-code.ftl:

<html>
<head>
    <style>
        /* ... same base styles ... */
        .otp-code {
            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
            font-size: 36px;
            font-weight: 700;
            letter-spacing: 8px;
            color: #0f172a;
            background: #f1f5f9;
            border: 2px solid #e2e8f0;
            border-radius: 12px;
            padding: 20px 32px;
            display: inline-block;
            margin: 24px 0;
        }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="email-header">
            <img src="https://your-domain.com/images/logo-white.svg"
                 alt="${realmName}" />
        </div>
        <div class="email-body">
            <h2>Your Verification Code</h2>
            <p>Hi ${user.firstName!"there"},</p>
            <p>Use the following code to verify your identity:</p>
            <p style="text-align: center;">
                <span class="otp-code">${code}</span>
            </p>
            <div class="expiry-note">
                This code will expire in ${linkExpirationFormatter(linkExpiration)}.
                If you did not request this code, please secure your account
                immediately.
            </div>
        </div>
        <div class="email-footer">
            <p>Never share this code with anyone. ${realmName} will never
               ask you for this code via phone or chat.</p>
        </div>
    </div>
</body>
</html>

For using email OTP with Skycloak’s extensions, see Using Email OTP Keycloak Extension with Skycloak.

Custom Message Strings

Override email subject lines and text in messages/messages_en.properties:

# Subject lines
emailVerificationSubject=Verify your email for {0}
passwordResetSubject=Reset your {0} password
emailUpdateConfirmationSubject=Confirm your new email for {0}
executeActionsSubject=Action required for your {0} account

# Common strings
emailVerificationBody=Please verify your email address.
passwordResetBody=Click the link below to reset your password.

Configuring Email-Related Realm Settings

Beyond SMTP configuration, several realm settings affect email behavior:

Verification Email Settings

curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  "https://keycloak.example.com/admin/realms/myrealm" 
  -d '{
    "verifyEmail": true,
    "loginWithEmailAllowed": true,
    "registrationEmailAsUsername": true,
    "actionTokenGeneratedByUserLifespan": 300,
    "actionTokenGeneratedByAdminLifespan": 43200
  }'
Setting Description Recommended
verifyEmail Require email verification on registration true
loginWithEmailAllowed Allow login with email address true
registrationEmailAsUsername Use email as the username true for most apps
actionTokenGeneratedByUserLifespan Link expiration for user-triggered emails (seconds) 300 (5 minutes)
actionTokenGeneratedByAdminLifespan Link expiration for admin-triggered emails (seconds) 43200 (12 hours)

Required Actions Configuration

Control which actions trigger emails:

  • Verify Email: Sends verification email on registration.
  • Update Password: Sends password reset email with link.
  • Configure OTP: Does not send email (user configures OTP in-session).

Admin-triggered required actions (via the admin console or API) use the executeActions email template:

# Trigger email verification and password update for a user
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" 
  "https://keycloak.example.com/admin/realms/myrealm/users/$USER_ID/execute-actions-email" 
  -H "Content-Type: application/json" 
  -d '["VERIFY_EMAIL", "UPDATE_PASSWORD"]'

Deploying Custom Email Templates

Volume Mount (Development)

docker run -p 8080:8080 
  -v /path/to/themes/my-company:/opt/keycloak/themes/my-company 
  quay.io/keycloak/keycloak:26.1.0 start-dev

Docker Build (Production)

FROM quay.io/keycloak/keycloak:26.1.0 AS builder

COPY themes/my-company /opt/keycloak/themes/my-company

ENV KC_DB=postgres
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.1.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
CMD ["start", "--optimized"]

Activate the Email Theme

In the admin console, navigate to Realm Settings > Themes and set Email Theme to my-company.

Or via the API:

curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  "https://keycloak.example.com/admin/realms/myrealm" 
  -d '{"emailTheme": "my-company"}'

Troubleshooting

Connection Refused or Timeout

ERROR: Could not send email: javax.mail.MessagingException: Could not connect to SMTP host

Causes:

  • SMTP host or port is incorrect.
  • Firewall blocks outbound traffic on port 587 or 465.
  • TLS/STARTTLS mismatch (e.g., using STARTTLS on port 465 which expects implicit SSL).

Fix:

# Test SMTP connectivity from the Keycloak container
docker exec keycloak-container nc -zv smtp.provider.com 587

Authentication Failed

ERROR: javax.mail.AuthenticationFailedException: 535 Authentication failed

Causes:

  • Wrong username or password.
  • For SES: Using IAM credentials instead of SMTP credentials.
  • For Gmail: App password not generated or 2-Step Verification not enabled.
  • For SendGrid: Using account password instead of API key, or username is not apikey.

Emails Go to Spam

Causes and fixes:

  1. SPF record missing. Add an SPF record to your domain’s DNS that includes your SMTP provider.
  2. DKIM not configured. Most providers (SES, SendGrid, Mailgun) require DKIM setup.
  3. DMARC not configured. Add a DMARC record after SPF and DKIM are in place.
  4. “From” address mismatch. The from address must be from a verified domain.
  5. Generic content. The default Keycloak templates look automated. Custom templates with your brand improve deliverability.
# Check DNS records for your domain
dig TXT example.com +short  # SPF
dig TXT default._domainkey.example.com +short  # DKIM
dig TXT _dmarc.example.com +short  # DMARC

Link Expired Before User Clicks

The default token lifespan may be too short. Increase it:

curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  "https://keycloak.example.com/admin/realms/myrealm" 
  -d '{"actionTokenGeneratedByUserLifespan": 900}'

This sets the lifespan to 15 minutes (900 seconds). Do not set it longer than necessary — these links are single-use security tokens.

Emails Not Sent for Specific Events

Ensure events are enabled in Realm Settings > Events > Event Listeners. The email event listener must be enabled for event-triggered emails (login errors, password changes, TOTP changes).

For monitoring email-related events, configure audit logging and review the SEND_VERIFY_EMAIL, SEND_RESET_PASSWORD, and SEND_IDENTITY_PROVIDER_LINK event types.

Email Best Practices

  1. Use a dedicated sending domain (e.g., auth.example.com) separate from your marketing email domain. This protects your marketing email reputation if auth emails trigger spam reports.

  2. Configure SPF, DKIM, and DMARC before going to production. All major email providers require these for reliable delivery.

  3. Set reasonable token lifespans. 5-15 minutes for user-triggered actions (password reset). 12-24 hours for admin-triggered actions (account setup).

  4. Monitor bounce rates. High bounce rates degrade your sender reputation. Integrate with your SMTP provider’s bounce/complaint notifications.

  5. Test across email clients. Email rendering varies across Gmail, Outlook, Apple Mail, and mobile clients. Test your templates with tools like Litmus or Email on Acid.

  6. Include plain-text alternatives. Create a text/ directory alongside html/ with plain-text versions of each template for clients that do not render HTML.

  7. Localize templates. Create messages_xx.properties files for each language your users need. Keycloak selects the right template based on the user’s locale.

Next Steps


Want managed email configuration with verified sending domains and template deployment handled for you? Skycloak provides managed Keycloak with email infrastructure included. Check our pricing to learn more.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman