This demo shows an example using a SPIFFE based KeyStore implemented in the JAVA-SPIFFE library to authenticate Tomcat's workloads.
The SPIFFE KeyStore interacts with the Workload API to fetch asynchronously the SVIDs and handle the certificates in memory. Certificates updates are pushed by the Workload API before expiration.
mTLS connections are handled by the Tomcat, that has a connector configured to use a Spiffe
KeyStoreType.
One of the Tomcats is configured with a Java Servlet Filter to grant access to APIs based on the SPIFFE ID of the client.
It also demonstrates that a standalone Java Application can use the SPIFFE Provider and interact with both a Java App running on Tomcat and a SPIFFE Nginx.
This demo is based on SPIRE version 0.6.0.
This demo is composed of 4 containers as seen in the following diagram:
Tomcats have a connector configured to listen on port 8443, to connect with mTLS, using the custom KeyStoreType Spiffe
.
That KeyStore connects with the Workload API to receive automatically the SVID updates that are used during the handshake
when establishing a TLS connection. It also validates the Peer's SPIFFE ID.
Tomcat connector:
<Connector
protocol="org.apache.coyote.http11.Http11NioProtocol"
port="8443" maxThreads="200"
scheme="https" secure="true" SSLEnabled="true"
keystoreFile="" keystorePass=""
keystoreType="Spiffe"
clientAuth="true" sslProtocol="TLS"/>
The trusted SPIFFE ID is configured in the java.security
file through the ssl.spiffe.accept
. In the Backend it looks as follows:
ssl.spiffe.accept=spiffe://example.org/front-end
The Standalone Java App in the diagram is a simple cURL app that takes an URL as a parameter and connects over mTLS using
the SPIFFE Provider. It's used to access an API
running on the Backend Tomcat and to get data from Users Service
connecting
to the NGINX Proxy.
The following diagram shows a simplified version of the workloads only, with their SPIFFE IDs and the interactions between each other:
The WEB-APP
that gets the SPIFFE ID spiffe://example.org/front-end1
is allowed to access the API /tasks
on the Backend,
whereas SSL-CURL-JAVA
that gets the SPIFFE ID spiffe://example.org/front-end2
is allowed to access the API /projects
.
That is configured through the file <tomcat>/conf/web.xml
:
<filter>
<filter-name>SpiffeFilter1</filter-name>
<filter-class>spiffe.filter.SpiffeFilter</filter-class>
<init-param>
<param-name>accept-spiffe-id</param-name>
<param-value>spiffe://example.org/front-end1</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SpiffeFilter1</filter-name>
<url-pattern>/tasks/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>SpiffeFilter2</filter-name>
<filter-class>spiffe.filter.SpiffeFilter</filter-class>
<init-param>
<param-name>accept-spiffe-id</param-name>
<param-value>spiffe://example.org/front-end2</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SpiffeFilter2</filter-name>
<url-pattern>/projects/*</url-pattern>
</filter-mapping>
SpiffeFilter
is a simple Java Servlet Filter example to show how SPIFFE ID based filtering to multiple APIs can be implemented.
The code is available in this repo.
Workload | Selector | SPIFFE ID | Parent ID |
---|---|---|---|
Back-end | unix:uid:1000 | spiffe://example.org/back-end | spiffe://example.org/host1 |
Front-end 1 | unix:uid:1000 | spiffe://example.org/front-end1 | spiffe://example.org/host2 |
Front-end 2 | unix:uid:1001 | spiffe://example.org/front-end2 | spiffe://example.org/host2 |
Proxy-Service | unix:uid:1000 | spiffe://example.org/proxy-service | spiffe://example.org/host3 |
- Linux or macOS
- Docker
- Docker Compose
$ make build
Successfully built
Run the containers:
$ make run
docker-compose up -d
Creating network "spiffeenvoydemo_default" with the default driver
Creating spiffeenvoydemo_spire-server_1 ...
Creating spiffeenvoydemo_spire-server_1 ... done
Creating spiffeenvoydemo_backend_1 ...
Creating javakeystoretomcatdemo_users-service_1
Creating javakeystoretomcatdemo_users-service_1 ... done
Creating spiffeenvoydemo_backend_1 ... done
Creating spiffeenvoydemo_frontend_1 ...
Creating spiffeenvoydemo_frontend_1 ... done
On a console run:
$ docker-compose exec spire-server ./spire-server run
INFO[0000] plugins started
INFO[0000] Starting gRPC server subsystem_name=endpoints
INFO[0000] Starting HTTP server subsystem_name=endpoints
On a console run:
$ docker-compose exec spire-server ./create-entries.sh
+ ./spire-server entry create -parentID spiffe://example.org/host1 -spiffeID spiffe://example.org/back-end -selector unix:uid:1000 -ttl 120
Entry ID: aed86831-38ec-40f9-bbe5-7bf88f84f788
SPIFFE ID: spiffe://example.org/back-end
Parent ID: spiffe://example.org/host1
TTL: 120
Selector: unix:uid:1000
+ ./spire-server entry create -parentID spiffe://example.org/host2 -spiffeID spiffe://example.org/front-end -selector unix:uid:1000 -ttl 120
Entry ID: 3c1ffb41-cb2f-4393-9eb5-52b17e9cc28e
SPIFFE ID: spiffe://example.org/front-end
Parent ID: spiffe://example.org/host2
TTL: 120
Selector: unix:uid:1000
+ ./spire-server entry create -parentID spiffe://example.org/host2 -spiffeID spiffe://example.org/front-end2 -selector unix:uid:1001 -ttl 120
Entry ID: 6c5c0e0d-e4c1-4022-9c29-1d4a8fc6693c
SPIFFE ID: spiffe://example.org/front-end2
Parent ID: spiffe://example.org/host2
TTL: 120
Selector: unix:uid:1001
+ ./spire-server entry create -parentID spiffe://example.org/host3 -spiffeID spiffe://example.org/proxy-service -selector unix:uid:1000 -ttl 120
Entry ID: dabc746b-3a68-40ca-893d-0aa062d125a6
SPIFFE ID: spiffe://example.org/proxy-service
Parent ID: spiffe://example.org/host3
TTL: 120
Selector: unix:uid:1000
On the console run:
$ docker-compose exec spire-server ./spire-server token generate -spiffeID spiffe://example.org/host1
Token: b0cab4ab-ddcd-4403-862c-325fe599f661
Copy the token and run:
# docker-compose exec backend ./spire-agent run -joinToken {token}
DEBU[0000] Requesting SVID for spiffe://example.org/back-end subsystem_name=manager
DEBU[0000] Requesting SVID for spiffe://example.org/host1 subsystem_name=manager
INFO[0000] Starting workload API subsystem_name=endpoints
Replace {token}
by the generated token.
On the console run:
$ docker-compose exec spire-server ./spire-server token generate -spiffeID spiffe://example.org/host2
Token: 81d70337-b255-446e-bb70-5eae5655a876
Copy the token and run:
# docker-compose exec frontend ./spire-agent run -joinToken {token}
DEBU[0000] Requesting SVID for spiffe://example.org/back-end subsystem_name=manager
DEBU[0000] Requesting SVID for spiffe://example.org/host2 subsystem_name=manager
INFO[0000] Starting workload API subsystem_name=endpoints
Replace {token}
by the generated token.
On the console run:
$ docker-compose exec spire-server ./spire-server token generate -spiffeID spiffe://example.org/host3
Token: aed63640-d5d4-4d0a-afd5-60617498adde
Copy the token and run:
# docker-compose exec users-service ./spire-agent run -joinToken {token}
DEBU[0000] Requesting SVID for spiffe://example.org/back-end subsystem_name=manager
DEBU[0000] Requesting SVID for spiffe://example.org/host2 subsystem_name=manager
INFO[0000] Starting workload API subsystem_name=endpoints
Replace {token}
by the generated token.
On a console run:
$ docker-compose exec backend /opt/back-end/start-tomcat.sh
INFO [main] org.apache.catalina.startup.Catalina.start Server startup
On a console run:
$ docker-compose exec frontend /opt/front-end/start-tomcat.sh
INFO [main] org.apache.catalina.startup.Catalina.start Server startup
On a console run:
$ docker-compose exec users-service ./init.sh
2018/08/22 12:14:51 Listening on port 8000...
2018/08/22 12:14:51 [debug] 52#0: bind() 0.0.0.0:8443 #6
Open a browser an go to http://localhost:9000/tasks
Then go to http://localhost:9000/users
The pages should be displayed without errors.
The page /tasks
displays data that is fetched from the service running on the Backend
The page /users
displays data that is fetched from the Users service
Stop Front-end Tomcat and start it with frontend2
user:
$ docker-compose exec frontend chown frontend3 -R /opt/tomcat
$ docker-compose exec frontend /opt/front-end/start-tomcat.sh frontend3
Open a browser an go to http://localhost:9000/tasks
As the frontend3
is not mapped in Spire Server registry, no SVID is issued and therefore the KeyStore doesn't have a
certificate to present during the handshake.
In the console there will be an error log:
2018-08-06 12:25:18.863 ERROR 206 --- [nio-9000-exec-1] s.api.examples.demo.TasksController : I/O error on GET request for "https://backend:8443/tasks/": java.security.cert.CertificateException: No trusted Certs; nested exception is javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No trusted Certs
06-Aug-2018 12:25:26.674 SEVERE [grpc-default-executor-0] spiffe.api.svid.X509SVIDFetcher$1.onError Could not get SVID
PERMISSION_DENIED: no identity issued
Now add a workload entry with a SPIFFE ID spiffe://example.org/front-end3
:
$ docker-compose exec spire-server ./spire-server entry create -parentID spiffe://example.org/host2 -spiffeID spiffe://example.org/front-end3 -selector unix:uid:1002 -ttl 120
After a while, try again http://localhost:9000/tasks.
This time the Backend has rejected the connection since the SPIFFE ID of the Frontend is not trusted:
2018-08-06 12:29:42.416 ERROR 206 --- [nio-9000-exec-6] s.api.examples.demo.TasksController : I/O error on GET request for "https://backend:8443/tasks/": Received fatal alert: certificate_unknown; nested exception is javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
Connect to Backend container and edit /opt/back-end/java.security
:
$ docker-compose exec backend nano /opt/back-end/java.security
Edit the trusted SPPIFE ID:
# The spiffeID that will be trusted
ssl.spiffe.accept=spiffe://example.org/front-end3
Restart Backend Tomcat and try again http://localhost:9000/tasks.
This time it should work.
sslCurl
is a standalone java app that uses the SPIFFE Provider to interact with services that require SVID to establish a mTLS connection.
Connect to the Frontend container:
$ docker-compose exec frontend bash
# cd /opt/sslCurl
Execute the script as ./sshCurl.sh {URL}
# ./sslCurl.sh https://users-service:8443/users
[{"id":1,"username":"user1@example.org"},{"id":2,"username":"user2@example.org"}]
# ./sslCurl.sh https://backend:8443/projects/
[{"id":1,"name":"SPIFFE"},{"id":2,"name":"SPIRE"},{"id":3,"name":"NGINX"}]
If you try to access the /tasks
API on the Backend, you will not be authorized:
# ./sslCurl.sh https://backend:8443/tasks/
HTTP Status 403 ? Forbidden
sslCurl
has the SPIFFE ID spiffe://example.org/front-end2
that is not authorized in the Tomcat Filter.
Stop the docker containers:
$ make clean
docker-compose down
Stopping spiffeenvoysidecar_frontend_1 ... done
Stopping spiffeenvoysidecar_backend_1 ... done
Stopping spiffeenvoysidecar_spire-server_1 ... done
Removing spiffeenvoysidecar_frontend_1 ... done
Removing spiffeenvoysidecar_backend_1 ... done
Removing spiffeenvoysidecar_spire-server_1 ... done
Removing network spiffeenvoysidecar_default