Skip to content

Generate admin password on first boot - #421

Merged
mgaffigan merged 1 commit into
OpenIntegrationEngine:mainfrom
mgaffigan:fix/no-default-admin-password
Aug 28, 2026
Merged

Generate admin password on first boot#421
mgaffigan merged 1 commit into
OpenIntegrationEngine:mainfrom
mgaffigan:fix/no-default-admin-password

Conversation

@mgaffigan

Copy link
Copy Markdown
Contributor

Adds log message with generated password instead of admin:admin matching MySQL, Jenkins, Gitlab, ESXi, ElasticSearch, KeyCloak, and other systems.

Allows manual configuration of generated password through new mirth.properties option.

Closes #227

Comment thread server/src/main/java/com/mirth/connect/server/migration/ServerMigrator.java Outdated
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Test Results

675 tests  ±0   675 ✅ ±0   2m 27s ⏱️ + 1m 7s
114 suites ±0     0 💤 ±0 
114 files   ±0     0 ❌ ±0 

Results for commit d45af87. ± Comparison against base commit e765919.

♻️ This comment has been updated with latest results.

@mgaffigan
mgaffigan force-pushed the fix/no-default-admin-password branch from d287c0c to 6efffc3 Compare August 26, 2026 22:27
@mgaffigan

Copy link
Copy Markdown
Contributor Author

Example output on first boot:

Info: Found suitable java version specified by the JAVA_HOME environment variable
Starting Open Integration Engine...
/Users/mgaffigan/dev/zulu17.60.17-ca-fx-jdk17.0.16-macosx_aarch64/bin/java -server -Xmx256m -Djava.awt.headless=true -Dapple.awt.UIElement=true --add-modules=java.sql.rowset --add-exports=java.base/com.sun.crypto.provider=ALL-UNNAMED --add-exports=java.base/sun.security.provider=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.security.cert=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/sun.security.pkcs=ALL-UNNAMED --add-opens=java.base/sun.security.rsa=ALL-UNNAMED --add-opens=java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED --add-opens=java.desktop/java.awt.color=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED -cp /Users/mgaffigan/dev/gitroot/oie-main/server/setup/mirth-server-launcher.jar com.mirth.connect.server.launcher.MirthLauncher
WARN  2026-08-26 17:26:14.550 [Main Server Thread] com.mirth.connect.server.Mirth: 
********************************************************************************
************   Initial admin password is Aa1!bKyDt8fzhUrznFmGaeGI   ************
********************************************************************************
INFO  2026-08-26 17:26:15.959 [Main Server Thread] com.mirth.connect.server.Mirth: Open Integration Engine 4.6.0 (Built on August 26, 2026) server successfully started.
INFO  2026-08-26 17:26:15.959 [Main Server Thread] com.mirth.connect.server.Mirth: This product was developed by NextGen Healthcare (https://www.nextgen.com) and its contributors (c)2005-2024.
INFO  2026-08-26 17:26:15.959 [Main Server Thread] com.mirth.connect.server.Mirth: Open Integration Engine contributors (c)2025.
INFO  2026-08-26 17:26:15.959 [Main Server Thread] com.mirth.connect.server.Mirth: Running OpenJDK 64-Bit Server VM 17.0.16 on Mac OS X (26.6, aarch64), derby, with charset UTF-8.
INFO  2026-08-26 17:26:15.962 [Main Server Thread] com.mirth.connect.server.Mirth: Web server running at http://10.37.129.2:8080/ and https://10.37.129.2:8443/

@mgaffigan
mgaffigan requested a review from jonbartels August 26, 2026 22:52
@pacmano1

Copy link
Copy Markdown
Contributor

I disagree with the approach here. This changes how the database gets created on five dialects in order to do something that doesn't need to touch database creation at all.

The install path is the least testable code in the product and the least forgiving, because a database that comes out wrong can't be repaired from inside the running product. As written, this deletes the seed row from five dialect scripts and adds a hand-written INSERT that runs during migration, and only Derby has actually been exercised.

None of that is necessary. Leave the scripts and the migrator exactly as they are, and check afterwards on every startup. Try to log in as admin/admin. If that fails, do nothing. If it succeeds and LAST_LOGIN is null, nobody has ever used this install, so generate a password, set it through userController.checkOrUpdateUserPassword, and log it. If it succeeds and LAST_LOGIN is set, this is a real install running on the default, so warn on every boot and change nothing.

The database then gets created exactly the way it is today, by code that already works on all five, and the new code only reads state and reacts to it. If it has a bug, the install is still fine and the fix is an ordinary patch.

It also widens what this fixes. Inside the tableExists(CONFIGURATION) branch, the code can only ever help databases created after the release. Every server already running admin/admin stays that way, and that is most of what #227 is describing. Checking at startup is what lets you say anything at all about those.

For the login attempt, the part you want is the credential check inside authorizeUser, lines 322 to 335, without the strike counter, the MFA hop and the audit event around it. Extract it and call it from both places. Calling authorizeUser directly books a login strike against admin on every boot of an already-secured server.

Warning rather than rotating on an existing install is deliberate. A rotation would write a live admin credential into mirth.log and its archives on a running server, and a loud warning every boot is the better trade.

Three smaller things are worth picking up. ./gradlew :server:createDerbyDb now produces an admin with no password row and CONFIGURATION already present, so startup skips the block and nothing ever creates it; I ran it to confirm. server.initialadminpassword skips PasswordRequirementsChecker, so an operator can seed a password that violates their own configured rules. And server/docs/README.txt:49 still tells people the password is admin, which is the line the issue cites.

@jonbartels this was force-pushed past your RandomStringUtils comment, so it needs a re-review.

@mgaffigan
mgaffigan force-pushed the fix/no-default-admin-password branch from 6efffc3 to 3cd6b4f Compare August 27, 2026 01:50
@mgaffigan

mgaffigan commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@pacmano1

I disagree with the approach here. This changes how the database gets created on five dialects in order to do something that doesn't need to touch database creation at all.

I disagree with your disagreement. The "admin" hashed password in the seed script is the issue. Anything that leaves a valid password in the DB for any period is a problem.

The install path is the least testable code in the product and the least forgiving, because a database that comes out wrong can't be repaired from inside the running product. As written, this deletes the seed row from five dialect scripts and adds a hand-written INSERT that runs during migration, and only Derby has actually been exercised.

None of that is necessary. Leave the scripts and the migrator exactly as they are, and check afterwards on every startup. Try to log in as admin/admin. If that fails, do nothing. If it succeeds and LAST_LOGIN is null, nobody has ever used this install, so generate a password, set it through userController.checkOrUpdateUserPassword, and log it. If it succeeds and LAST_LOGIN is set, this is a real install running on the default, so warn on every boot and change nothing.

The database then gets created exactly the way it is today, by code that already works on all five, and the new code only reads state and reacts to it. If it has a bug, the install is still fine and the fix is an ordinary patch.

It also widens what this fixes. Inside the tableExists(CONFIGURATION) branch, the code can only ever help databases created after the release. Every server already running admin/admin stays that way, and that is most of what #227 is describing. Checking at startup is what lets you say anything at all about those.

The problem is not someone who knowingly configures the system to use the password "admin", the problem is that the prompt to change it is enforced on the client side, and that it is a well-known value for any period of time. The installer and first run cannot allow a well-known password to create an auth session. The server must authenticate the admin performing initial setup.

For the login attempt, the part you want is the credential check inside authorizeUser, lines 322 to 335, without the strike counter, the MFA hop and the audit event around it. Extract it and call it from both places. Calling authorizeUser directly books a login strike against admin on every boot of an already-secured server.

My goal (and the vulnerability) have nothing to do with a user being configured to a particular value. The vuln is of the setup procedure leaving the system where a network attacker is able to take over control before the actual admin can complete setup.

Warning rather than rotating on an existing install is deliberate. A rotation would write a live admin credential into mirth.log and its archives on a running server, and a loud warning every boot is the better trade.

Again: the bug is with the setup process, not with an upgrade or migration.

Three smaller things are worth picking up. ./gradlew :server:createDerbyDb now produces an admin with no password row and CONFIGURATION already present, so startup skips the block and nothing ever creates it; I ran it to confirm.

I don't understand this comment.

server.initialadminpassword skips PasswordRequirementsChecker, so an operator can seed a password that violates their own configured rules.

This is the status quo. I do not view this as an issue.

And server/docs/README.txt:49 still tells people the password is admin, which is the line the issue cites.

Fixed.

@mgaffigan

Copy link
Copy Markdown
Contributor Author

@pacmano1

The install path is the least testable code in the product and the least forgiving, because a database that comes out wrong can't be repaired from inside the running product. As written, this deletes the seed row from five dialect scripts and adds a hand-written INSERT that runs during migration, and only Derby has actually been exercised.

Also: I had tested this with #294, which exercises this on each run against every database. It passed. It would be nice if this were the default for all PR's to be tested so.

I agree it would be nice to avoid the direct write, but short of adding another operation, I did not see a clean way to do so, and this is very mundane SQL.

@pacmano1

pacmano1 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor
  1. engine starts
  2. engine attempts to login as admin/admin whenever the engine is ready.

If this succeeds and there is no prior login generate a conformant password. So there's a vulnerability that is available on an initial engine for split second while a new password is generated by the engine itself.

If admin/admin succeeds and there is a prior login, write a conspicuous log message.

That's the plain version of what I'm saying.

it works on new installs and it warns current installs. And I'm pretty sure it's less code. I didn't want to create a competing PR. I can to demonstrate the concept.

To be clear, I'm trying to make the assessment continuous not just on first time run.

@mgaffigan
mgaffigan force-pushed the fix/no-default-admin-password branch from 3cd6b4f to 6f37ed2 Compare August 27, 2026 04:51
@pacmano1

Copy link
Copy Markdown
Contributor

I will back off most of my comments - but I would urge the PR to address current admin/admin creds with a loud error on startup.

@mgaffigan

Copy link
Copy Markdown
Contributor Author

@pacmano1

I would urge the PR to address current admin/admin creds with a loud error on startup.

Password scanner added as #424

@jonbartels jonbartels left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed in teams. I read the code. Seems correct to me.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
@mgaffigan
mgaffigan force-pushed the fix/no-default-admin-password branch from 6f37ed2 to d45af87 Compare August 28, 2026 18:48
@mgaffigan
mgaffigan merged commit 423df18 into OpenIntegrationEngine:main Aug 28, 2026
6 checks passed
@mgaffigan
mgaffigan deleted the fix/no-default-admin-password branch August 28, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY] Default administrative credentials (admin:admin) enabled by default

5 participants