Large amounts of Zimbra LDAP ephemeral data can cause LDAP replica servers to fall out of sync with LDAP MMR servers, backups to fail and other issues. Very large Zimbra installations often migrate Zimbra LDAP ephemeral data to an SSDB or Redis cluster to avoid these issues. While Zimbra provides a migration tool to do this, such SSDB or Redis clusters are not covered by Zimbra Support. Further, if the cluster fails, every user is immediately logged out of Zimbra; no one can log back in until Redis or SSDB is back up and running.
mailboxd is supposed to remove CSRF and Auth tokens when they expire, but in our experience Zimbra doesn’t always do so, though it seems to do a better job of pruning expired zimbraCsrfTokenData entries than it does pruning expired zimbraAuthTokens entries.
We have found multiple smaller systems of even just a few hundred mailboxes with user accounts having thousands of expired zimbraAuthTokens entries (rarely have we found an excess of zimbraCsrfTokenData entries).
This blog post will help you understand what’s going on, and includes two bash scripts you can run: one to identify how big (or not) a problem you may have, and a second script to clean up expired zimbraAuthTokens entries from accounts identified by the first script having large numbers of such expired tokens.
Understanding zimbraAuthTokens and zimbraCsrfTokenData Creation
A new zimbraAuthTokens entry is added each time a user successfully authenticates to Zimbra via:
- Web Client (Webmail) Login
- Modern/Classic/Mobile web interface login
- Each new browser session creates a new token
- IMAP Authentication
- Each IMAP connection that authenticates creates a new auth token – for example, when configuring Apple Mail with IMAP, multiple authentication events occur, each creating hundreds tokens on mailboxes with lots of folders
- POP3 Authentication
- Each successful POP3 login creates a token
- SMTP Authentication
- When users authenticate to send mail via SMTP (authenticated submission)
- ActiveSync/EAS (Exchange ActiveSync)
- Mobile device synchronization via ActiveSync protocol
- CalDAV/CardDAV Authentication
- Calendar and contact synchronization clients
- Zimbra Connector for Outlook (ZCO)
- EWS (Exchange Web Services)
- Outlook for Mac and other EWS clients
- API/SOAP Requests
- Programmatic authentication via Zimbra’s SOAP API
- Pre-authentication
- Single sign-on integrations that create auth tokens
Key Points
- Each successful authentication creates a new token, even if the user already has active sessions.
- Tokens have a default lifetime of 2 days.
- Tokens should be automatically cleaned up when they expire, but this cleanup mechanism can fail.
IMAP clients in particular can create many tokens quickly – a single Mail.app configuration created multiple authentication events in rapid succession.
This explains why accounts with heavy IMAP usage (like users with multiple email clients or mobile devices) can accumulate hundreds or thousands of tokens if the automatic cleanup fails.
This article explains why having large numbers of expired tokens can cause issues, and lists several kinds of issues.
How Do I Know If I Have A Problem With Excessive zimbraAuthTokens Entries?
Simple! Run the script below on any Zimbra mailstore, as the Zimbra user. The script makes no changes to your system, runs “nicely” and generates a report.
#!/bin/bash # # Zimbra Auth Token Inventory Script # This script scans all accounts and reports zimbraAuthTokens counts # set -e # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Step 1: Check that script is being run as zimbra user if [ "$(whoami)" != "zimbra" ]; then echo -e "${RED}ERROR: This script must be run as the 'zimbra' user.${NC}" echo "Please run: su - zimbra -c '$0'" exit 1 fi echo -e "${GREEN}✓ Running as zimbra user${NC}" # Step 2: Set nice priority for CPU and IO # Renice the current process to run with low priority renice -n 19 -p $$ > /dev/null 2>&1 # Set IO priority to idle class if ionice is available if command -v ionice &> /dev/null; then ionice -c 3 -p $$ > /dev/null 2>&1 echo -e "${GREEN}✓ Running with low CPU and IO priority${NC}" else echo -e "${YELLOW}⚠ ionice not available, running with low CPU priority only${NC}" fi # Generate timestamp for output file DATE=$(date +%Y%m%d) TIME=$(date +%H%M%S) OUTPUT_FILE="/tmp/AuthTokenInventory-${DATE}_${TIME}.txt" TEMP_FILE=$(mktemp) ZMPROV_CMD_FILE=$(mktemp) ZMPROV_OUTPUT_FILE=$(mktemp) # Cleanup temp files on exit trap "rm -f $TEMP_FILE $ZMPROV_CMD_FILE $ZMPROV_OUTPUT_FILE" EXIT echo "" echo -e "${BLUE}=====================================================${NC}" echo -e "${BLUE} Zimbra Auth Token Inventory Script${NC}" echo -e "${BLUE}=====================================================${NC}" echo "" echo "Output file: $OUTPUT_FILE" echo "" # Step 3: Get all accounts (excluding system accounts) echo -e "${YELLOW}Fetching all accounts...${NC}" ACCOUNTS=$(zmprov -l gaa | grep -v "spam.\|ham.\|virus-\|galsync") if [ -z "$ACCOUNTS" ]; then echo -e "${RED}ERROR: No accounts found${NC}" exit 1 fi ACCOUNT_COUNT=$(echo "$ACCOUNTS" | wc -l) echo -e "${GREEN}✓ Found $ACCOUNT_COUNT accounts${NC}" echo "" # Step 4: Create bulk command file for zmprov echo -e "${YELLOW}Creating bulk provisioning command file...${NC}" while IFS= read -r ACCOUNT; do echo "ga $ACCOUNT zimbraAuthTokens" >> "$ZMPROV_CMD_FILE" done <<< "$ACCOUNTS" echo -e "${GREEN}✓ Command file created with $ACCOUNT_COUNT queries${NC}" echo "" # Execute bulk zmprov command echo -e "${YELLOW}Executing bulk account query...${NC}" echo "This may take a while for large deployments." echo "" zmprov -f "$ZMPROV_CMD_FILE" > "$ZMPROV_OUTPUT_FILE" 2>&1 echo -e "${GREEN}✓ Bulk query complete${NC}" echo "" # Parse the output and count tokens per account echo -e "${YELLOW}Parsing results and counting tokens...${NC}" # Use awk for more robust parsing awk ' BEGIN { account = "" count = 0 } /^prov> # name / { # Save previous account if exists if (account != "") { print count "|" account } # Extract new account name (everything after "# name ") sub(/^prov> # name /, "") account = $0 count = 0 next } /^zimbraAuthTokens:/ { count++ next } /^prov> $/ { # Empty prov> line indicates end of account with no tokens if (account != "") { print count "|" account account = "" count = 0 } next } END { # Print last account if exists if (account != "") { print count "|" account } } ' "$ZMPROV_OUTPUT_FILE" > "$TEMP_FILE" echo -e "${GREEN}✓ Parsing complete${NC}" echo "" # Step 5 & 6: Sort results and write output echo -e "${YELLOW}Sorting results...${NC}" # Write header to output file { echo "======================================================" echo "Zimbra Auth Token Inventory Report" echo "======================================================" echo "Generated: $(date '+%Y-%m-%d %H:%M:%S')" echo "Total Accounts Scanned: $ACCOUNT_COUNT" echo "======================================================" echo "" printf "%-10s %s\n" "TOKENS" "ACCOUNT" printf "%-10s %s\n" "----------" "----------------------------------------" } > "$OUTPUT_FILE" # Sort by token count (numeric, descending) and format output sort -t'|' -k1 -nr "$TEMP_FILE" | while IFS='|' read -r COUNT ACCOUNT; do printf "%-10s %s\n" "$COUNT" "$ACCOUNT" done >> "$OUTPUT_FILE" # Calculate cutoff timestamp (48 hours ago in milliseconds) CURRENT_TIME_SEC=$(date +%s) CUTOFF_TIME_MS=$(( (CURRENT_TIME_SEC - 172800) * 1000 )) # 172800 seconds = 48 hours echo -e "${YELLOW}Analyzing token ages (48 hour expiration threshold)...${NC}" # Create temp file for expired token counts EXPIRED_TEMP=$(mktemp) trap "rm -f $TEMP_FILE $ZMPROV_CMD_FILE $ZMPROV_OUTPUT_FILE $EXPIRED_TEMP" EXIT # Parse tokens again to count expired ones per account CURRENT_ACCOUNT="" TOTAL_COUNT=0 EXPIRED_COUNT=0 while IFS= read -r line; do # Check if this is an account name line if [[ "$line" =~ ^prov\>\ \#\ name\ (.+)$ ]]; then # Save previous account's counts if we have one if [ -n "$CURRENT_ACCOUNT" ]; then echo "$TOTAL_COUNT|$EXPIRED_COUNT|$CURRENT_ACCOUNT" >> "$EXPIRED_TEMP" fi # Start tracking new account CURRENT_ACCOUNT="${BASH_REMATCH[1]}" TOTAL_COUNT=0 EXPIRED_COUNT=0 # Check if this is a token line elif [[ "$line" =~ ^zimbraAuthTokens:\ (.+)$ ]]; then TOTAL_COUNT=$((TOTAL_COUNT + 1)) TOKEN="${BASH_REMATCH[1]}" # Extract timestamp (2nd field) TIMESTAMP=$(echo "$TOKEN" | cut -d'|' -f2) if [ "$TIMESTAMP" -lt "$CUTOFF_TIME_MS" ]; then EXPIRED_COUNT=$((EXPIRED_COUNT + 1)) fi fi done < "$ZMPROV_OUTPUT_FILE" # Don't forget the last account if [ -n "$CURRENT_ACCOUNT" ]; then echo "$TOTAL_COUNT|$EXPIRED_COUNT|$CURRENT_ACCOUNT" >> "$EXPIRED_TEMP" fi echo -e "${GREEN}✓ Age analysis complete${NC}" echo "" # Add summary statistics echo "" >> "$OUTPUT_FILE" echo "=====================================================" >> "$OUTPUT_FILE" echo "Summary Statistics:" >> "$OUTPUT_FILE" echo "=====================================================" >> "$OUTPUT_FILE" # Calculate statistics TOTAL_TOKENS=$(awk -F'|' '{sum+=$1} END {print sum}' "$TEMP_FILE") TOTAL_EXPIRED=$(awk -F'|' '{sum+=$2} END {print sum}' "$EXPIRED_TEMP") TOTAL_VALID=$((TOTAL_TOKENS - TOTAL_EXPIRED)) MAX_TOKENS=$(sort -t'|' -k1 -nr "$TEMP_FILE" | head -1 | cut -d'|' -f1) ACCOUNTS_WITH_TOKENS=$(awk -F'|' '$1 > 0 {count++} END {print count}' "$TEMP_FILE") ACCOUNTS_WITH_EXPIRED=$(awk -F'|' '$2 > 0 {count++} END {print count}' "$EXPIRED_TEMP") ACCOUNTS_OVER_100=$(awk -F'|' '$1 > 100 {count++} END {print count}' "$TEMP_FILE") ACCOUNTS_OVER_1000=$(awk -F'|' '$1 > 1000 {count++} END {print count}' "$TEMP_FILE") ACCOUNTS_EXPIRED_OVER_100=$(awk -F'|' '$2 > 100 {count++} END {print count}' "$EXPIRED_TEMP") { echo "Cutoff timestamp: $CUTOFF_TIME_MS (48 hours ago)" echo "Current time: $(date)" echo "" echo "Total Auth Tokens (all accounts): $TOTAL_TOKENS" echo " Valid tokens (< 48 hours): $TOTAL_VALID" echo " Expired tokens (>= 48 hours): $TOTAL_EXPIRED" echo "" echo "Maximum Tokens (single account): $MAX_TOKENS" echo "Accounts with tokens: $ACCOUNTS_WITH_TOKENS" echo "Accounts with expired tokens: $ACCOUNTS_WITH_EXPIRED" echo "Accounts with >100 total tokens: $ACCOUNTS_OVER_100" echo "Accounts with >100 expired tokens: $ACCOUNTS_EXPIRED_OVER_100" echo "Accounts with >1000 total tokens: $ACCOUNTS_OVER_1000" echo "" echo "Top 10 accounts by total token count:" echo "--------------------------------------" } >> "$OUTPUT_FILE" sort -t'|' -k1 -nr "$TEMP_FILE" | head -10 | while IFS='|' read -r COUNT ACCOUNT; do printf " %6s tokens: %s\n" "$COUNT" "$ACCOUNT" done >> "$OUTPUT_FILE" { echo "" echo "Top 10 accounts by expired token count:" echo "----------------------------------------" } >> "$OUTPUT_FILE" sort -t'|' -k2 -nr "$EXPIRED_TEMP" | head -10 | while IFS='|' read -r TOTAL EXPIRED ACCOUNT; do printf " %6s expired (%s total): %s\n" "$EXPIRED" "$TOTAL" "$ACCOUNT" done >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE" echo "=====================================================" >> "$OUTPUT_FILE" echo "End of Report" >> "$OUTPUT_FILE" echo "=====================================================" >> "$OUTPUT_FILE" # Display results echo -e "${GREEN}✓ Results written to: $OUTPUT_FILE${NC}" echo "" echo -e "${BLUE}Summary:${NC}" echo " Total Accounts: $ACCOUNT_COUNT" echo " Accounts with tokens: $ACCOUNTS_WITH_TOKENS" echo " Accounts with expired tokens: $ACCOUNTS_WITH_EXPIRED" echo " Accounts with >100 total tokens: $ACCOUNTS_OVER_100" echo " Accounts with >100 expired tokens: $ACCOUNTS_EXPIRED_OVER_100" echo " Accounts with >1000 total tokens: $ACCOUNTS_OVER_1000" echo "" echo -e "${BLUE}Token Statistics:${NC}" echo " Total tokens: $TOTAL_TOKENS" echo " Valid tokens (< 48 hours): $TOTAL_VALID" echo " Expired tokens (>= 48 hours): $TOTAL_EXPIRED" echo "" echo -e "${BLUE}Top 5 accounts by expired token count:${NC}" sort -t'|' -k2 -nr "$EXPIRED_TEMP" | head -5 | while IFS='|' read -r TOTAL EXPIRED ACCOUNT; do echo " $EXPIRED expired ($TOTAL total): $ACCOUNT" done echo "" echo -e "${GREEN}Full report available at: $OUTPUT_FILE${NC}"
OK, So I Have A Gazillion Expired Tokens; How Can I fix the problem?
Simple! Run the script below on any Zimbra mailstore, as the Zimbra user. Input the email address of a user identified by the report from the script above. This script will re-identify expired auth token entries, and then ask you to confirm that you want the script to delete them.
#!/bin/bash # # Zimbra Auth Token Cleanup Script # This script removes expired zimbraAuthTokens (older than 48 hours) # set -e # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Step 1: Check that script is being run as zimbra user if [ "$(whoami)" != "zimbra" ]; then echo -e "${RED}ERROR: This script must be run as the 'zimbra' user.${NC}" echo "Please run: su - zimbra -c '$0'" exit 1 fi echo -e "${GREEN}✓ Running as zimbra user${NC}" # Step 2: Set nice priority for CPU and IO # Renice the current process to run with low priority renice -n 19 -p $$ > /dev/null 2>&1 # Set IO priority to idle class if ionice is available if command -v ionice &> /dev/null; then ionice -c 3 -p $$ > /dev/null 2>&1 echo -e "${GREEN}✓ Running with low CPU and IO priority${NC}" else echo -e "${YELLOW}⚠ ionice not available, running with low CPU priority only${NC}" fi # Step 3: Ask for the account to scan echo "" read -p "Enter the email account to clean (user@domain format): " ACCOUNT # Validate email format if [[ ! "$ACCOUNT" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then echo -e "${RED}ERROR: Invalid email format${NC}" exit 1 fi # Check if account exists if ! zmprov ga "$ACCOUNT" &>/dev/null; then echo -e "${RED}ERROR: Account '$ACCOUNT' not found${NC}" exit 1 fi echo -e "${GREEN}✓ Account found: $ACCOUNT${NC}" echo "" # Step 4: Execute zmprov ga and get auth tokens echo "Fetching auth tokens..." TOKENS=$(zmprov ga "$ACCOUNT" zimbraAuthTokens | grep "^zimbraAuthTokens:" | sed 's/^zimbraAuthTokens: //') # Count tokens TOKEN_COUNT=$(echo "$TOKENS" | wc -l) if [ -z "$TOKENS" ] || [ "$TOKEN_COUNT" -eq 0 ]; then echo -e "${YELLOW}No auth tokens found for this account.${NC}" exit 0 fi echo -e "${YELLOW}Found $TOKEN_COUNT auth tokens${NC}" # Step 5: Calculate cutoff timestamp (48 hours ago in milliseconds) CURRENT_TIME_SEC=$(date +%s) CUTOFF_TIME_MS=$(( (CURRENT_TIME_SEC - 172800) * 1000 )) # 172800 seconds = 48 hours echo "Current time: $(date)" echo "Cutoff time: $(date -d @$((CURRENT_TIME_SEC - 172800)) 2>/dev/null || date -r $((CURRENT_TIME_SEC - 172800)))" echo "Cutoff timestamp (ms): $CUTOFF_TIME_MS" echo "" # Create temporary file to store valid (non-expired) tokens TEMP_FILE=$(mktemp) trap "rm -f $TEMP_FILE" EXIT # Filter tokens - keep only those with timestamp >= cutoff echo "Analyzing tokens..." VALID_COUNT=0 EXPIRED_COUNT=0 # Show first few tokens for debugging echo "" echo "Sample of first 3 tokens with timestamps:" echo "$TOKENS" | head -3 | while IFS= read -r line; do TIMESTAMP=$(echo "$line" | cut -d'|' -f2) TOKEN_DATE=$(date -d @$((TIMESTAMP / 1000)) 2>/dev/null || date -r $((TIMESTAMP / 1000)) 2>/dev/null || echo "Unable to parse") echo " Timestamp: $TIMESTAMP => $TOKEN_DATE" if [ "$TIMESTAMP" -ge "$CUTOFF_TIME_MS" ]; then echo " Status: VALID (>= $CUTOFF_TIME_MS)" else echo " Status: EXPIRED (< $CUTOFF_TIME_MS)" fi done echo "" while IFS= read -r line; do # Extract timestamp (2nd field, separated by |) TIMESTAMP=$(echo "$line" | cut -d'|' -f2) # Compare timestamp with cutoff if [ "$TIMESTAMP" -ge "$CUTOFF_TIME_MS" ]; then echo "$line" >> "$TEMP_FILE" VALID_COUNT=$((VALID_COUNT + 1)) else EXPIRED_COUNT=$((EXPIRED_COUNT + 1)) fi done <<< "$TOKENS" echo -e "${GREEN}✓ Analysis complete${NC}" echo " Valid tokens (< 48 hours old): $VALID_COUNT" echo " Expired tokens (>= 48 hours old): $EXPIRED_COUNT" echo "" # If no expired tokens, nothing to do if [ "$EXPIRED_COUNT" -eq 0 ]; then echo -e "${GREEN}No expired tokens found. All tokens are valid.${NC}" exit 0 fi # If all tokens are expired, warn user if [ "$VALID_COUNT" -eq 0 ]; then echo -e "${YELLOW}WARNING: All tokens are expired!${NC}" echo "This will remove all expired tokens." echo "" fi # Show sample of tokens to be kept echo "Sample of tokens to be preserved (newest first):" sort -t'|' -k2 -nr "$TEMP_FILE" | head -5 | while IFS='|' read -r token_id timestamp version; do TOKEN_DATE=$(date -d @$((timestamp / 1000)) 2>/dev/null || date -r $((timestamp / 1000))) echo " $TOKEN_DATE (timestamp: $timestamp)" done echo "" # Confirm before proceeding echo -e "${YELLOW}This will DELETE $EXPIRED_COUNT expired tokens and keep $VALID_COUNT valid tokens.${NC}" read -p "Do you want to proceed? (yes/no): " CONFIRM if [ "$CONFIRM" != "yes" ]; then echo "Operation cancelled." exit 0 fi # Step 6 & 7: Replace tokens with valid ones only echo "" echo "Updating auth tokens..." if [ "$VALID_COUNT" -eq 0 ]; then # Remove all tokens by setting a single dummy token, then removing it echo "Removing all expired tokens..." # First, get one existing token to use as a template SAMPLE_TOKEN=$(echo "$TOKENS" | head -1) # Set to just one token if ! zmprov ma "$ACCOUNT" zimbraAuthTokens "$SAMPLE_TOKEN"; then echo -e "${RED}ERROR: Failed to set temporary token${NC}" exit 1 fi # Now remove that token if ! zmprov ma "$ACCOUNT" -zimbraAuthTokens "$SAMPLE_TOKEN"; then echo -e "${RED}ERROR: Failed to remove token${NC}" exit 1 fi else # Read tokens into array mapfile -t TOKEN_ARRAY < "$TEMP_FILE" # First token: replace (ma) to clear all existing tokens FIRST_TOKEN="${TOKEN_ARRAY[0]}" echo "Setting first token (replacing all)..." if ! zmprov ma "$ACCOUNT" zimbraAuthTokens "$FIRST_TOKEN"; then echo -e "${RED}ERROR: Failed to set first token${NC}" exit 1 fi # Remaining tokens: append (+zimbraAuthTokens) for i in $(seq 1 $((VALID_COUNT - 1))); do TOKEN="${TOKEN_ARRAY[$i]}" echo "Adding token $((i + 1))/$VALID_COUNT..." if ! zmprov ma "$ACCOUNT" +zimbraAuthTokens "$TOKEN"; then echo -e "${RED}ERROR: Failed to add token $((i + 1))${NC}" exit 1 fi done fi # Flush account cache echo "" echo "Flushing account cache..." if ! zmprov fc account "$ACCOUNT"; then echo -e "${YELLOW}WARNING: Failed to flush cache, but tokens were updated${NC}" fi echo "" echo -e "${GREEN}✓ SUCCESS: Auth tokens cleaned up successfully!${NC}" echo -e "${GREEN} Deleted: $EXPIRED_COUNT expired tokens${NC}" echo -e "${GREEN} Kept: $VALID_COUNT valid tokens${NC}" echo "" # Show final count FINAL_COUNT=$(zmprov ga "$ACCOUNT" zimbraAuthTokens | grep -c "^zimbraAuthTokens:" || true) echo "Final token count: $FINAL_COUNT"
Am I Done?
Maybe not. As explained at the end of the Github article, just deleting expired tokens does not reduce Free Pages. You may need to dump and restore the LDAP databases to fix that issue. This wiki article can show you how — but we STRONGLY recommend you open a Zimbra Support Case first if you feel that an LDAP export/import is justified.
If you need help with this, or any other Zimbra LDAP issue, just fill out this form and we’ll be back to you!
Your message has been sent
Hope that helps,
L. Mark Stone
Mission Critical Email LLC
1 October 2025
The information provided in this blog is intended for informational and educational purposes only. The views expressed herein are those of Mr. Stone personally. The contents of this site are not intended as advice for any purpose and are subject to change without notice. Mission Critical Email makes no warranties of any kind regarding the accuracy or completeness of any information on this site, and we make no representations regarding whether such information is up-to-date or applicable to any particular situation. All copyrights are reserved by Mr. Stone. Any portion of the material on this site may be used for personal or educational purposes provided appropriate attribution is given to Mr. Stone and this blog.