4.12.0 can no longer log in - WeakKeyException
Current Behavior
The upgrade to 4.12.0 effectively bricked my DT instance for I can no longer log in. It 500s with:
2024-10-16 07:57:44,031 ERROR [GlobalExceptionHandler] Uncaught internal server error [requestId=a470b97a-7f76-4dbb-87e3-bc516362c4ee]
io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 160 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.
at io.jsonwebtoken.security.Keys.hmacShaKeyFor(Keys.java:83)
at alpine.server.auth.JsonWebToken.<init>(JsonWebToken.java:86)
at alpine.server.auth.JsonWebToken.<init>(JsonWebToken.java:97)
at org.dependencytrack.resources.v1.UserResource.validateCredentials(UserResource.java:112)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:146)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:189)
at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$ResponseOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:176)
at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:93)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:478)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:400)
at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:274)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:266)
at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:253)
at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:696)
at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:397)
at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:349)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:358)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:312)
at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
at org.eclipse.jetty.ee10.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1379)
at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java:736)
at org.eclipse.jetty.ee10.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1614)
at alpine.server.filters.ContentSecurityPolicyFilter.doFilter(ContentSecurityPolicyFilter.java:225)
at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
at alpine.server.filters.ClickjackingFilter.doFilter(ClickjackingFilter.java:93)
at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
at alpine.server.filters.WhitelistUrlFilter.doFilter(WhitelistUrlFilter.java:166)
at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:208)
at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1547)
at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:824)
at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:436)
at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:464)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:575)
at org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:703)
at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060)
at org.eclipse.jetty.server.Server.handle(Server.java:181)
at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:661)
at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:406)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:478)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:441)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:201)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
at java.base/java.lang.Thread.run(Unknown Source)
That happens with OID as well as with username+password.
The error message ain't that helpful and if some security default was raised it should rather warn about it than just stop working. Seems to be an issue with jsonwebtoken that just bubbles up.
Steps to Reproduce
Not quite sure what key this is about so I'm not sure how to reproduce.
- Install 4.11.x
- Set up secret according to https://docs.dependencytrack.org/getting-started/configuration/#secret-key
- Set up data dir according to https://docs.dependencytrack.org/getting-started/data-directory/
- Upgrade to 4.12.0
- Try to log in
Expected Behavior
I should still be able to log in.
Dependency-Track Version
4.12.0
Dependency-Track Distribution
Container Image
Database Server
PostgreSQL
Database Server Version
13.x
Browser
Mozilla Firefox
Checklist
- [x] I have read and understand the contributing guidelines
- [x] I have checked the existing issues for whether this defect was already reported
I suppose that https://docs.dependencytrack.org/getting-started/configuration/#secret-key is the issue and that 32 is now "too short". But I cannot just set a new key, can I?
/edit: Luckily, a downgrade to 4.11 worked.
openssl rand 32 generates a secure random of 32 bytes, which is 256 bits, which in turn should satisfy the library's check.
Can you double-check if your key is indeed 32 bytes long?
In any case I'll check if we can do something to make this a warning log rather than an exception.
I cannot reproduce this with a key generated by openssl rand 32:
openssl rand 32 > secret.key
docker run \
-v "$(pwd)/secret.key:/var/run/secrets/secret.key:ro" \
-e 'ALPINE_SECRET_KEY_PATH=/var/run/secrets/secret.key' \
-p '127.0.0.1:8080:8080' dependencytrack/bundled:4.12.0
Change password of initial admin user:
curl 'http://localhost:8080/api/v1/user/forceChangePassword' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin&newPassword=admin123&confirmPassword=admin123'
Login as admin, yielding the properly signed JWT:
curl 'http://localhost:8080/api/v1/user/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin123'
eyJhbGciOiJIUzI1NiJ9.eyJ...
Thanks for the quick reply @nscuro !
Weird things are going on. I dropped into the api container and was looking for /secret/secret.key (value of ALPINE_SECRET_KEY_PATH) and found ... nothing.
A quick find yielded: /secret/..2024_10_16_08_08_54.351889796/.secret-key
That's pretty weird. Does DT do some kind of backup when upgrading to 4.12.x?
Disregarding the weird location the actual contents match with my actual key which seems to be ... 20 bytes long? Did the docs change? I'm close to 100% sure I copy pasted the generation. Is there a sane way to ... update the key?
The odd location is due to how k8s mounts ConfigMaps and Secrets - it mounts them to those seemingly random paths and then symlinks those to your desired path. IIRC that's what enables updating of mounts at runtime.
Our docs always mentioned a key length of 32. It's also the length of the key generated by DT in case you don't provide a custom one.
The good news is that changing the key is not overly painful. You can just generate a new one. What will happen is:
- Any active login session will expire since JWT validation will fail. Users merely need to login again to get a JWT signed with the new key.
- If you have configured any API tokens for external services (OSS Index, GitHub, VulnDB etc.), you need to re-enter them so they get encrypted with the new key.
Argh, thanks. Still weird tho, the /secret/secret.key was a symlink but it pointed into the void and nothing pointed to the weird path. Gonna check again tomorrow.
Thanks for the clarification regarding changing the key! Should probably go into the docs, too (unless I've missed it). I'll give it a shot tomorrow and report back.
Still, changing the error to a warning seems reasonable. Erroring out on the very first run would make sense as to prevent the user from doing potentially stupid things, but on consecutive boots it should just warn. If there is a cheap way to check at that point if it's a fresh install it might make sense to differentiate between the two use cases, imho.
Unfortunately there's no way to prevent the JWT library from failing if the provided key is too short. It has added multiple checks that cannot be bypassed, and likely for good reason.
We could pad the key to reach the >=256bit requirement, but I fear that would lead to users never getting to know that their key is too short. Also, in cases similar like yours, it would invalidate existing encrypted values too, so there's no real benefit.
Thank you very much for looking into this. I regenerated the key and everything seems fine.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.