Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tls): agent can pass TLS client auth cert #491

Merged
merged 16 commits into from
Oct 4, 2024

Conversation

andrewazores
Copy link
Member

@andrewazores andrewazores commented Sep 23, 2024

Fixes #490
See also cryostatio/cryostat-operator#957

See above PR for steps on retrieving tls.crt, tls.key, ca.crt from the Operator's generated Secret for the Agent to use.

Then, to test this combined with the above Operator PR:

  1. Patch the sample-app.yaml to deploy the container quay.io/andrewazores/quarkus-cryostat-agent:tls-client-auth-15 (an application containing the latest build of this PR)
  2. make sample_app
  3. export SAMPLE_POD="$(kubectl get pod -l app=quarkus-test -o jsonpath='{$.items[0].metadata.name}')" ; oc wait pod $SAMPLE_POD --for condition=ready; for i in tls.key tls.crt ca.crt; do oc cp $i $SAMPLE_POD:/tmp/ ; done to get the secret contents into the Agent application's Pod
  4. In another terminal oc wait pod -l app=quarkus-test --for condition=ready ; oc logs -f -l app=quarkus-test to watch the log output from the sample application Pod. Initially the Agent should start up but bail out because the baseuri property is empty. This is expected.
  5. In the original terminal, oc exec -it $SAMPLE_POD -- java -jar /deployments/app/cryostat-agent.jar -Dcryostat.agent.baseuri=https://cryostat-sample-agent.cryostat.svc:8282 -Dcryostat.agent.callback=http://quarkus-test:9977 -Dcryostat.agent.api.writes-enabled=true -Dcryostat.agent.webclient.tls.trust-all=true -Dcryostat.agent.webclient.tls.client-auth.cert.path=/tmp/tls.crt -Dcryostat.agent.webclient.tls.client-auth.key.path=/tmp/tls.key -Dcryostat.agent.webclient.tls.client-auth.keystore.pass=changeit1 -Dcryostat.agent.webclient.tls.client-auth.key.pass=changeit2 . This forks a process to invoke the Agent to dynamically attach itself to the sample application JVM, passing the various required configuration properties as late binding arguments. The important properties for this PR are the ones containing tls.client-auth: cryostat.agent.webclient.tls.client-auth.cert.path is analogous to curl --cert; cryostat.agent.webclient.tls.client-auth.key.path is analagous to curl --key. For the ease of testing, cryostat.agent.webclient.tls.trust-all is also passed here, which tells the client to trust any TLS certificate presented by the server. Patching sample-app-agent.yaml to deploy the same container image, then doing make sample_app_agent, should show that the current changes do not change the existing functionality for configuring the HTTP client to correctly trust the server certificate (in this case the OAuth proxy, not the Agent TLS proxy, but it's the same concept and code path, just a different cert and server URL).
  6. Repeat the previous test but remove the trust-all property. This should result in the Agent's HTTP client refusing to connect since it does not trust the ca.crt presented by the server.

@andrewazores
Copy link
Member Author

I think using the ca.crt is pretty simple actually. Extending the previous instructions to retrieve the tls.key, tls.crt, and ca.crt, as well as to export SAMPLE_POD, and copy the TLS files into the pod, then:

kubectl exec -it $SAMPLE_POD -- java -jar /deployments/app/cryostat-agent.jar \
  -Dcryostat.agent.baseuri=https://cryostat-sample-agent.cryostat.svc:8282 \
  -Dcryostat.agent.callback=http://quarkus-test:9977 \
  -Dcryostat.agent.api.writes-enabled=true \
  -Dcryostat.agent.webclient.tls.client-auth.cert.path=/tmp/tls.crt \
  -Dcryostat.agent.webclient.tls.client-auth.key.path=/tmp/tls.key \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].path=/tmp/ca.crt \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].type=X.509 \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].alias=cryostat

looks like it does the trick [0]. Worth noting that this only works when doing dynamic attach with late-binding configuration properties however, because of the cert[0] syntax - square brackets are not allowed in environment variable names in k8s. I remember having a conversation with Ming about supporting a CERT_0_ replacement syntax transformation for environment variables, but haven't checked if that actually got done yet.

I didn't have any luck trying to use these configuration properties to use the truststore.p12 available from the cryostat-sample-tls Secret - it seemed like a file format/encoding problem. Most likely I just didn't give it just the right combination of config properties.


[0] caveat: this deployed the latest main Cryostat container, ie 4.0.0-snapshot, which doesn't work with the sample application version that I deployed for this test because of the API V4 changes. I'll need to rebuild the sample application and test again. But, the request made it past the nginx auth proxy successfully, so the TLS setup worked - just Cryostat responded with the index.html contents and an HTTP 404 status code, because the Agent made a request to an old API V3 path that no longer exists.

@andrewazores
Copy link
Member Author

Using sample application quay.io/andrewazores/quarkus-cryostat-agent:tls-client-auth-16 makes some more progress, but now the setup is broken in a new way. All of the TLS stuff is working, but the Agent/Cryostat registration flow is somehow broken.


Agent:

2024-10-03 14:39:43:759 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Cryostat Agent starting...
2024-10-03 14:39:44:756 +0000 [cryostat-agent-main] INFO io.cryostat.agent.CryostatClient - Using Cryostat baseuri https://cryostat-sample-agent.cryostat.svc:8282
2024-10-03 14:39:44:765 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Registration - io.cryostat.agent.Registration started
2024-10-03 14:39:44:845 +0000 [cryostat-agent-registration] INFO io.cryostat.agent.Agent - Registration state: UNREGISTERED
2024-10-03 14:39:45:163 +0000 [cryostat-agent-main] INFO io.cryostat.agent.Agent - Startup complete
2024-10-03 14:39:45:471 +0000 [cryostat-agent-worker-1] INFO io.cryostat.agent.WebServer - Defined credentials with id 20
2024-10-03 14:39:45:473 +0000 [cryostat-agent-registration] INFO io.cryostat.agent.Agent - Registration state: REFRESHING
2024-10-03 14:39:46:560 +0000 [cryostat-agent-registration] ERROR io.cryostat.agent.Registration - Registration failure
java.util.concurrent.ExecutionException: io.cryostat.agent.RegistrationException: java.util.concurrent.CompletionException: java.util.concurrent.CompletionException: java.net.SocketTimeoutException: Read timed out
	at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
	at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073)
	at io.cryostat.agent.Registration.tryRegister(Registration.java:228)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: io.cryostat.agent.RegistrationException: java.util.concurrent.CompletionException: java.util.concurrent.CompletionException: java.net.SocketTimeoutException: Read timed out
	at io.cryostat.agent.Registration.lambda$tryRegister$6(Registration.java:223)
	at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934)
	at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911)
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1773)
	... 6 more
Caused by: java.util.concurrent.CompletionException: java.util.concurrent.CompletionException: java.net.SocketTimeoutException: Read timed out
	at io.cryostat.agent.CryostatClient.lambda$register$3(CryostatClient.java:148)
	... 10 more
Caused by: java.util.concurrent.CompletionException: java.net.SocketTimeoutException: Read timed out
	at io.cryostat.agent.CryostatClient.executeQuiet(CryostatClient.java:459)
	at io.cryostat.agent.CryostatClient.lambda$supply$36(CryostatClient.java:452)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
	... 6 more
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:288)
	at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:314)
	at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:355)
	at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:808)
	at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
	at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:484)
	at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:478)
	at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
	at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1465)
	at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1069)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
	at io.cryostat.agent.shaded.org.apache.shaded.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
	at io.cryostat.agent.shaded.org.apache.shaded.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
	at io.cryostat.agent.shaded.org.apache.shaded.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
	at io.cryostat.agent.CryostatClient.executeQuiet(CryostatClient.java:457)
	... 8 more
2024-10-03 14:39:46:561 +0000 [cryostat-agent-registration] INFO io.cryostat.agent.Registration - Registration retry period: PT5S

Cryostat:

2024-10-03 14:39:45,353 INFO  [io.qua.htt.access-log] (executor-thread-25) 127.0.0.1 - - [03/Oct/2024:14:39:45 +0000] "GET /api/v4/credentials HTTP/1.0" 200 923
2024-10-03 14:39:45,470 INFO  [io.qua.htt.access-log] (executor-thread-25) 127.0.0.1 - - [03/Oct/2024:14:39:45 +0000] "POST /api/v4/credentials HTTP/1.0" 201 171
2024-10-03 14:39:46,560 INFO  [io.qua.htt.access-log] (vert.x-eventloop-thread-5) 127.0.0.1 - - [03/Oct/2024:14:39:46 +0000] "POST /api/v4/discovery HTTP/1.0" 200 -
2024-10-03 14:39:51,396 INFO  [io.qua.htt.access-log] (executor-thread-28) 10.217.0.2 - - [03/Oct/2024:14:39:51 +0000] "GET /health/liveness HTTP/1.1" 204 -
2024-10-03 14:39:51,584 INFO  [io.qua.htt.access-log] (executor-thread-28) 127.0.0.1 - - [03/Oct/2024:14:39:51 +0000] "GET /api/v4/credentials HTTP/1.0" 200 1108
2024-10-03 14:39:52,592 INFO  [io.qua.htt.access-log] (vert.x-eventloop-thread-3) 127.0.0.1 - - [03/Oct/2024:14:39:52 +0000] "POST /api/v4/discovery HTTP/1.0" 200 -
2024-10-03 14:40:00,567 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-27) HTTP Request to /api/v4/discovery failed, error id: 84608d6b-4304-42c2-8901-fb279865355c-119: jakarta.persistence.EntityNotFoundException: Unable to find io.cryostat.discovery.DiscoveryPlugin with id 0eb02848-21bb-4eb0-b078-f921b8d531e1
	at io.quarkus.hibernate.orm.runtime.boot.FastBootEntityManagerFactoryBuilder$JpaEntityNotFoundDelegate.handleEntityNotFound(FastBootEntityManagerFactoryBuilder.java:249)
	at org.hibernate.persister.entity.AbstractEntityPersister.initializeEnhancedEntityUsedAsProxy(AbstractEntityPersister.java:3828)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:225)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.lambda$forceInitialize$1(EnhancementAsProxyLazinessInterceptor.java:195)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:206)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:193)
	at org.hibernate.engine.internal.StatefulPersistenceContext.unproxyAndReassociate(StatefulPersistenceContext.java:820)
	at org.hibernate.event.internal.DefaultDeleteEventListener.delete(DefaultDeleteEventListener.java:157)
	at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:99)
	at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:86)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.fireDelete(SessionImpl.java:967)
	at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:898)
	at org.hibernate.internal.SessionImpl.remove(SessionImpl.java:2412)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.remove(TransactionScopedSession.java:168)
	at org.hibernate.engine.spi.SessionLazyDelegator.remove(SessionLazyDelegator.java:352)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.remove(Unknown Source)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.delete(AbstractJpaOperations.java:128)
	at io.quarkus.hibernate.orm.panache.PanacheEntityBase.delete(PanacheEntityBase.java:92)
	at io.cryostat.discovery.DiscoveryPlugin$Listener.prePersist(DiscoveryPlugin.java:121)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass.prePersist$$superforward(Unknown Source)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass$$function$$1.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInCallerTx(TransactionalInterceptorBase.java:335)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:40)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass.prePersist(Unknown Source)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_ClientProxy.prePersist(Unknown Source)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.hibernate.jpa.event.internal.ListenerCallback.performCallback(ListenerCallback.java:55)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.callback(CallbackRegistryImpl.java:123)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.preCreate(CallbackRegistryImpl.java:72)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:198)
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:136)
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:177)
	at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:95)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:79)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:761)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:745)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.persist(TransactionScopedSession.java:146)
	at org.hibernate.engine.spi.SessionLazyDelegator.persist(SessionLazyDelegator.java:282)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.persist(Unknown Source)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.persist(AbstractJpaOperations.java:105)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.persist(AbstractJpaOperations.java:100)
	at io.quarkus.hibernate.orm.panache.PanacheEntityBase.persist(PanacheEntityBase.java:65)
	at io.cryostat.discovery.Discovery.register(Discovery.java:254)
	at io.cryostat.discovery.Discovery_Subclass.register$$superforward(Unknown Source)
	at io.cryostat.discovery.Discovery_Subclass$$function$$1.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:136)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:107)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:38)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
	at io.cryostat.discovery.Discovery_Subclass.register(Unknown Source)
	at io.cryostat.discovery.Discovery$quarkusrestinvoker$register_121ad36d63a7d07486382c062cb0b09b644a4ff2.invoke(Unknown Source)
	at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
	at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
	at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:635)
	at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1583)

2024-10-03 14:40:01,397 INFO  [io.qua.htt.access-log] (executor-thread-27) 10.217.0.2 - - [03/Oct/2024:14:40:01 +0000] "GET /health/liveness HTTP/1.1" 204 -
2024-10-03 14:40:06,599 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-28) HTTP Request to /api/v4/discovery failed, error id: 84608d6b-4304-42c2-8901-fb279865355c-120: jakarta.persistence.EntityNotFoundException: Unable to find io.cryostat.discovery.DiscoveryPlugin with id 2d973fcf-95d2-4946-9e3b-e38352d589b2
	at io.quarkus.hibernate.orm.runtime.boot.FastBootEntityManagerFactoryBuilder$JpaEntityNotFoundDelegate.handleEntityNotFound(FastBootEntityManagerFactoryBuilder.java:249)
	at org.hibernate.persister.entity.AbstractEntityPersister.initializeEnhancedEntityUsedAsProxy(AbstractEntityPersister.java:3828)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:225)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.lambda$forceInitialize$1(EnhancementAsProxyLazinessInterceptor.java:195)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper.performWork(EnhancementHelper.java:206)
	at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.forceInitialize(EnhancementAsProxyLazinessInterceptor.java:193)
	at org.hibernate.engine.internal.StatefulPersistenceContext.unproxyAndReassociate(StatefulPersistenceContext.java:820)
	at org.hibernate.event.internal.DefaultDeleteEventListener.delete(DefaultDeleteEventListener.java:157)
	at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:99)
	at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:86)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.fireDelete(SessionImpl.java:967)
	at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:898)
	at org.hibernate.internal.SessionImpl.remove(SessionImpl.java:2412)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.remove(TransactionScopedSession.java:168)
	at org.hibernate.engine.spi.SessionLazyDelegator.remove(SessionLazyDelegator.java:352)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.remove(Unknown Source)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.delete(AbstractJpaOperations.java:128)
	at io.quarkus.hibernate.orm.panache.PanacheEntityBase.delete(PanacheEntityBase.java:92)
	at io.cryostat.discovery.DiscoveryPlugin$Listener.prePersist(DiscoveryPlugin.java:121)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass.prePersist$$superforward(Unknown Source)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass$$function$$1.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInCallerTx(TransactionalInterceptorBase.java:335)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:40)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_Subclass.prePersist(Unknown Source)
	at io.cryostat.discovery.DiscoveryPlugin_Listener_ClientProxy.prePersist(Unknown Source)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.hibernate.jpa.event.internal.ListenerCallback.performCallback(ListenerCallback.java:55)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.callback(CallbackRegistryImpl.java:123)
	at org.hibernate.jpa.event.internal.CallbackRegistryImpl.preCreate(CallbackRegistryImpl.java:72)
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:198)
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:136)
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:177)
	at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:95)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:79)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:761)
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:745)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.persist(TransactionScopedSession.java:146)
	at org.hibernate.engine.spi.SessionLazyDelegator.persist(SessionLazyDelegator.java:282)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.persist(Unknown Source)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.persist(AbstractJpaOperations.java:105)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.persist(AbstractJpaOperations.java:100)
	at io.quarkus.hibernate.orm.panache.PanacheEntityBase.persist(PanacheEntityBase.java:65)
	at io.cryostat.discovery.Discovery.register(Discovery.java:254)
	at io.cryostat.discovery.Discovery_Subclass.register$$superforward(Unknown Source)
	at io.cryostat.discovery.Discovery_Subclass$$function$$1.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:136)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:107)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:38)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
	at io.cryostat.discovery.Discovery_Subclass.register(Unknown Source)
	at io.cryostat.discovery.Discovery$quarkusrestinvoker$register_121ad36d63a7d07486382c062cb0b09b644a4ff2.invoke(Unknown Source)
	at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
	at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
	at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:635)
	at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1583)

Proxy:

10.217.0.139 - - [03/Oct/2024:14:39:45 +0000] "GET /api/v4/credentials HTTP/1.1" 200 923 "-" "Apache-HttpClient/4.5.14 (Java/17.0.12)" "-"
10.217.0.139 - - [03/Oct/2024:14:39:45 +0000] "POST /api/v4/credentials HTTP/1.1" 201 196 "-" "Apache-HttpClient/4.5.14 (Java/17.0.12)" "-"
10.217.0.139 - - [03/Oct/2024:14:39:46 +0000] "POST /api/v4/discovery HTTP/1.1" 499 25 "-" "Apache-HttpClient/4.5.14 (Java/17.0.12)" "-"
10.217.0.2 - - [03/Oct/2024:14:39:51 +0000] "GET /healthz HTTP/1.1" 200 0 "-" "kube-probe/1.29" "-"
10.217.0.139 - - [03/Oct/2024:14:39:51 +0000] "GET /api/v4/credentials HTTP/1.1" 200 1108 "-" "Apache-HttpClient/4.5.14 (Java/17.0.12)" "-"
10.217.0.139 - - [03/Oct/2024:14:39:52 +0000] "POST /api/v4/discovery HTTP/1.1" 499 25 "-" "Apache-HttpClient/4.5.14 (Java/17.0.12)" "-"

This Agent/Cryostat version cominbation works normally as expected in smoketest, so it seems like somehow something about the proxy being in the middle is what causes this change in behaviour.

Since this is broken after the V4 API changes, these are my suspects:

@andrewazores
Copy link
Member Author

"Fixed" by having the Agent specify its own Pod IP for the callback URL, rather than its app Service DNS name.

Apply this patch:

diff --git a/config/samples/sample-app.yaml b/config/samples/sample-app.yaml
index b9ba843..7d43926 100644
--- a/config/samples/sample-app.yaml
+++ b/config/samples/sample-app.yaml
@@ -16,7 +16,7 @@ spec:
         app: quarkus-test
     spec:
       containers:
-        - image: quay.io/andrewazores/quarkus-test:latest
+        - image: quay.io/andrewazores/quarkus-cryostat-agent:tls-client-auth-16
           imagePullPolicy: Always
           name: quarkus-test
           ports:
@@ -25,12 +25,9 @@ spec:
           - containerPort: 9097
             protocol: TCP
           resources:
-            requests:
-              cpu: 200m
-              memory: 96Mi
             limits:
               cpu: 500m
-              memory: 128Mi
+              memory: 256Mi
           securityContext:
             allowPrivilegeEscalation: false
             capabilities:

Then do:

$ make sample_app
$ SAMPLE_POD="$(oc get pod -l app=quarkus-test -o jsonpath='{$.items[0].metadata.name}')" ; \
  oc wait pod $SAMPLE_POD --for condition=ready; \
  for i in tls.key tls.crt ca.crt; do oc cp $i $SAMPLE_POD:/tmp/ \
  ; done
$ POD_IP="$(oc get pod -l app=quarkus-test -o jsonpath='{$.items[0].status.podIP}')"
$ oc exec -it $SAMPLE_POD -- java -jar /deployments/app/cryostat-agent.jar \
  -Dcryostat.agent.baseuri=https://cryostat-sample-agent.cryostat.svc:8282 \
  -Dcryostat.agent.callback=http://${POD_IP}:9977 \
  -Dcryostat.agent.api.writes-enabled=true \
  -Dcryostat.agent.webclient.tls.client-auth.cert.path=/tmp/tls.crt \
  -Dcryostat.agent.webclient.tls.client-auth.key.path=/tmp/tls.key \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].path=/tmp/ca.crt \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].type=X.509 \
  -Dcryostat.agent.webclient.tls.truststore.cert[0].alias=cryostat
$ oc wait pod -l app=quarkus-test --for condition=ready ; oc logs -f -l app=quarkus-test

I'll update cryostatio/cryostat-operator#928 once this PR and cryostatio/cryostat-operator#957 are in to accommodate for these further changes.

ebaron
ebaron previously approved these changes Oct 3, 2024
Copy link
Member

@ebaron ebaron left a comment

Choose a reason for hiding this comment

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

I tested successfully combined with cryostatio/cryostat-operator#957 and the following deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: quarkus-test-agent
  name: quarkus-test-agent
  namespace: cryostat-operator-system
spec:
  selector:
    matchLabels:
      app: quarkus-test-agent
  template:
    metadata:
      labels:
        app: quarkus-test-agent
    spec:
      containers:
      - env:
        - name: CRYOSTAT_AGENT_APP_NAME
          value: agent-test
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
        - name: CRYOSTAT_AGENT_API_WRITES_ENABLED
          value: "true"
        - name: CRYOSTAT_AGENT_BASEURI
          value: https://cryostat-sample-agent.$(NAMESPACE).svc:8282
        - name: POD_IP
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: status.podIP
        - name: CRYOSTAT_AGENT_CALLBACK
          value: http://$(POD_IP):9977
        - name: JAVA_OPTS_APPEND
          value: |-
            -Dquarkus.http.host=0.0.0.0
            -Djava.util.logging.manager=org.jboss.logmanager.LogManager
            -javaagent:/deployments/app/cryostat-agent.jar
            -Dcryostat.agent.webclient.tls.client-auth.cert.path=/var/run/secrets/io.cryostat/cryostat-agent/tls.crt
            -Dcryostat.agent.webclient.tls.client-auth.key.path=/var/run/secrets/io.cryostat/cryostat-agent/tls.key
            -Dcryostat.agent.webclient.tls.truststore.cert[0].path=/var/run/secrets/io.cryostat/cryostat-agent/ca.crt
            -Dcryostat.agent.webclient.tls.truststore.cert[0].type=X.509
            -Dcryostat.agent.webclient.tls.truststore.cert[0].alias=cryostat
        image: quay.io/andrewazores/quarkus-cryostat-agent:tls-client-auth-16
        imagePullPolicy: Always
        name: quarkus-test-agent
        ports:
        - containerPort: 10010
          protocol: TCP
        - containerPort: 9097
          protocol: TCP
        resources:
          limits:
            cpu: 500m
            memory: 192Mi
          requests:
            cpu: 200m
            memory: 96Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
        volumeMounts:
        - mountPath: /var/run/secrets/io.cryostat/cryostat-agent
          name: agent-tls
      securityContext:
        runAsNonRoot: true
      volumes:
      - name: agent-tls
        secret:
          defaultMode: 420
          secretName: cryostat-agent-c44e3ce7d8452282f4cf1ab14d08cfda2875fa727912e41595c6979bffe0f693

On a side note, would it be possible to add a key options for the web server in addition to the existing keystore options, similar to what you've done here for client-auth? It would make it easier to configure HTTPS for the server.

@andrewazores
Copy link
Member Author

On a side note, would it be possible to add a key options for the web server in addition to the existing keystore options, similar to what you've done here for client-auth? It would make it easier to configure HTTPS for the server.

I'll open a follow-up PR with these changes in a moment, just to avoid potentially screwing up something in here.

@andrewazores andrewazores merged commit 1ccd7ed into cryostatio:main Oct 4, 2024
9 checks passed
@andrewazores andrewazores deleted the tls-client-auth branch October 4, 2024 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat New feature or request safe-to-test
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Task] Agent should be able to pass TLS client certificate for authentication to server (proxy)
2 participants