Download a PDF of this article
This article’s headline mentions hosting a dark service. Huh? Why would you want your application services to be dark?
Open ports are everywhere. You need open ports, right? How are your users and applications going to connect to your services if there are no open ports? Well, they can still connect—but only if they are trusted.
A dark service is a service that’s had zero trust (ZT) principles applied across the board and, therefore, not only to the network layer. Indeed, the principles of ZT access are baked directly into the application itself.
Dark services have no listening ports, taking them off the internet (and even off the local network) where port scanners and nefarious actors are only an IP hop away.
The zero trust phrase has been thrown around a lot lately, but what does it really mean? Here’s how the concept is defined by NIST’s Computer Security Resource Center.
Zero trust (ZT) is the term for an evolving set of cybersecurity paradigms that move defenses from static, network-based perimeters to focus on users, assets, and resources. A zero trust architecture (ZTA) uses zero trust principles to plan industrial and enterprise infrastructure and workflows. Zero trust assumes there is no implicit trust granted to assets or user accounts based solely on their physical or network location (i.e., local area networks versus the internet) or based on asset ownership (enterprise or personally owned). Authentication and authorization (both subject and device) are discrete functions performed before a session to an enterprise resource is established. Zero trust is a response to enterprise network trends that include remote users, bring your own device (BYOD), and cloud-based assets that are not located within an enterprise-owned network boundary. Zero trust focuses on protecting resources (assets, services, workflows, network accounts, etc.), not network segments, as the network location is no longer seen as the prime component to the security posture of the resource.
In practice, you can boil this down to five basic tenants that apply to everything on your network, including both human users and automated processes.
You can apply all the ZT principles to your network, but even after that your application is still listening on a port after that. If there is a port open, your application can be attacked. That’s why the concept of app embedded zero trust moves the edge of the network into the application. Bringing ZT into your app programmatically via an SDK has several benefits.
In terms of portability, embedding ZT into an application can help in common use cases. If you want to change clouds or deploy your application into a new data center, no changes need to be made. Simply develop your app once and deploy it anywhere, and it just works. What about mobile clients that want access through a secure shell (ssh
) from home, a client site, or the airport? No problem! It doesn’t matter where the client is; trusted connectivity works from anywhere.
How is this achieved? When using OpenZiti, every endpoint (SDK for in-app access, tunnelers for the operating system or edge routers for the network) must have an identity with provisioned certificates. The certificates allow Ziti to perform authentication and authorization before any data flows across secure communications channels. These endpoints reach out of the private network to talk to the controller (control plane) and make connections to join the network fabric mesh (data plane). Therefore, services and endpoints in your private networks only make outbound connections—and thus, no holes are opened for inbound traffic.
This example will use OpenZiti to provide the ZT overlay network and application SDKs. Spring Boot and Tomcat will host the service. The project will take about half an hour. You will need the following:
bash
shell—or, for Windows users, a VM or Windows Subsystem for Linux 2 (WSL 2)If you don’t have access to a Linux environment but wish to use it, you can grab a Linux VM from the Oracle Cloud free tier.
What is OpenZiti? It’s an open source project sponsored by NetFoundry. Oracle embraces open source and zero trust security, so Oracle partnered with NetFoundry to provide zero trust connections to applications, including Java applications, running in Oracle Cloud Infrastructure (OCI). NetFoundry’s Edge Router software is staged within the OCI Marketplace for deployment within any OCI region.
Get the code. The example code can be downloaded from here. Alternatively, you can clone it using Git with the following shell command:
git clone https://github.com/netfoundry/openziti-spring-boot
As with most Spring guides, you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.
Create the test network. This example will use a very simple OpenZiti network, shown in Figure 1.
Figure 1. A simple OpenZiti network
For this article, it isn’t important for you to fully understand the components of the OpenZiti network; however, there are two important things to know.
To explore the architecture a little deeper, see “Overview of a Ziti Network” or watch this 54-minute video on YouTube.
OpenZiti provides a script that contains set of shell functions that bootstrap the OpenZiti client and network. As with any script, it is a good idea to download it and look it over before adding it to your shell. After running the following instructions, leave this terminal window open, because you’ll need it to configure the network.
# Pull the shell extensions
wget -q https://raw.githubusercontent.com/openziti/ziti/release-next/quickstart/docker/image/ziti-cli-functions.sh
# Source the shell extensions
. ziti-cli-functions.sh
# Pull the latest Ziti CLI and put it on your shell's classpath
getLatestZiti yes
The shell script above includes a few functions to initialize a network. To start the OpenZiti network overlay, run the following in the same terminal window:
expressInstall
startZitiController
waitForController
startExpressEdgeRouter
What do those functions do?
expressInstall
creates cryptographic material and configuration files required to run an OpenZiti network.startZitiController
starts the network controller.startExpressEdgerouter
starts the edge router.Log in to the new network. The OpenZiti network is now up and running. The next step is to log in to the controller and establish the administrative session that you will use to configure the example services and identities. ziti-cli-functions
has the following function to do that:
zitiLogin
Configure the new network. Use a script to configure the OpenZiti network. The code for the example contains a network directory. To configure the network, run the following command in the same terminal you used to start the OpenZiti network:
./express-network-config.sh
If the script produces errors with a lot of ziti: command not found
statements, run the following shell command to put ziti
in your terminal path:
getLatestZiti yes
The script will write out the two identity files (client.json
and private-service.json
) needed for the Java code you’ll write shortly. Note: The repository includes a file called NETWORK-SETUP.md
that explains what the script is doing and why.
Reset the Ziti demo network. If you wish to start over, these are the commands that need to be run to stop the Ziti network and clean up.
stopAllEdgeRouters
stopZitiController
unsetZitiEnv
rm -rf ~/.ziti/quickstart
Now, you’re at the good part! There are three things that need to be done to host an OpenZiti service in a Spring Boot application.
The example code contains an initial/server project. Pull that up in your favorite editor and follow along.
Add the OpenZiti Spring Boot dependency. The OpenZiti Spring Boot dependency is hosted on Maven Central.
If you are using Gradle, add the following to build.gradle
:
implementation 'org.openziti:ziti-springboot:0.23.12'
If you prefer Maven, add the following to pom.xml
:
<dependency>
<groupId>org.openziti</groupId>
<artifactId>ziti-springboot</artifactId>
<version>0.23.12</version>
</dependency>
Add application properties. Open the application properties file: src/main/resources/application.properties
. The Tomcat customizer provided by OpenZiti needs an identity and the name of the service that the identity will bind. If you followed along with the network setup above, the values will be the following:
ziti.id = ../../network/private-service.json
ziti.serviceName = demo-service
Configure the OpenZiti Tomcat customizer. The Tomcat customizer replaces the standard socket protocol with an OpenZiti protocol that knows how to bind a service to accept connections over the Ziti network. To enable this adapter, open the main application class: com.example.restservice.RestServiceApplication
. Then replace
@SpringBootApplication
with
@SpringBootApplication (scanBasePackageClasses = {ZitiTomcatCustomizer.class, GreetingController.class})
Run the application. The OpenZiti Java SDK will connect to the test network, authenticate, and bind your service so that other OpenZiti overlay network clients can connect to it.
If you use Gradle, enter the following in a terminal window in your project directory:
./gradlew bootRun
If you use Maven, run the following in a terminal window in your project directory:
./mvnw spring-boot:run
Test the new Spring Boot service. The Spring Boot service you just created is now totally dark, with no listening ports. You can verify this by using the following command in a terminal window:
netstat -anp | grep 8080
You should find nothing marked as LISTENING
. Now, the only way to access the service is via the OpenZiti network. Let’s write a simple client to connect to the service and check that everything is working correctly.
This section will use the OpenZiti Java SDK to connect to the OpenZiti network. The example source code includes a project and a class that takes care of the boilerplate stuff for you.
Connect to OpenZiti. The Java SDK needs to be initialized with an OpenZiti identity. It is polite to destroy the context once the code is done, so you will wrap it up in a try-catch
construct with a finally
block. Here is the code.
ZitiContext zitiContext = null;
try {
zitiContext = Ziti.newContext(identityFile, "".toCharArray());
long end = System.currentTimeMillis() + 10000;
while (null == zitiContext.getService(serviceName) && System.currentTimeMillis() < end) {
log.info("Waiting for {} to become available", serviceName);
Thread.sleep(200);
}
if (null == zitiContext.getService(serviceName)) {
throw new IllegalArgumentException(String.format("Service %s is not available on the OpenZiti network",serviceName));
}
} catch (Throwable t) {
log.error("OpenZiti network test failed", t);
}
finally {
if( null != zitiContext ) zitiContext.destroy();
}
What’s going on here?
Ziti.newContext
loads the OpenZiti identity and starts the connection process.while()
inserts a delay. It can take a little while to establish the connection with the OpenZiti network fabric. For long-running applications, this is typically not a problem, but for this little client you need to give the network some time to get everything ready.zitiContext.destroy()
disposes of the context and cleans up resources locally and on the OpenZiti network.Send a request to the service. The client now has a connection to the test OpenZiti network. Now the client can ask OpenZiti to dial the service and send some data.
Important: This client is for demonstration purposes only! You should never, ever write a raw HTTP request like this in a real app. OpenZiti has a couple of examples on GitHub that use OKHttp
and Netty
if you want to work up this code using a real HTTP client.
log.info("Dialing service");
ZitiConnection conn = zitiContext.dial(serviceName);
String request = "GET /greeting?name=MyName HTTP/1.1\n" +
"Accept: */*\n" +
"Host: example.web\n" +
"\n";
log.info("Sending request");
conn.write(request.getBytes(StandardCharsets.UTF_8));
Here’s an explanation of what the code does.
ZitiConnection
is a socket connection over the OpenZiti network fabric that can be used to exchange data with a Ziti service.zitiContext.dial
opens a connection through the OpenZiti network to the service.request
is used because the connection is essentially a plain socket. The request string is a plain HTTP GET command to the greeting endpoint in the Spring Boot app.conn.write
sends the request over the OpenZiti network.Read the service response. The service will respond to the request with a JSON greeting. Read the greeting and write it to the log.
byte[] buff = new byte[1024];
int i;
log.info("Reading response");
while (0 < (i = conn.read(buff,0, buff.length))) {
log.info("=== " + new String(buff, 0, i) );
}
What’s happening? conn.read
reads the data sent back from the Spring Boot service via the OpenZiti connection.
Run the client. If you use Gradle, run the following in a terminal window in the client project:
./gradlew build run
If you use Maven, run the following in a terminal window in the client project:
./mvnw package exec:java
In this article, you’ve seen how the concept of zero trust enables secure computing and how embedding ZT into an application improves the application’s protection by letting it run as a dark service.
Tod Burtchell is the Zero Trust advocate and associate director of software engineering at NetFoundry. Burtchell has more than 20 years of experience designing and building Java applications; currently, he is focused on building software-defined and programmable ZT networks.
Previous Post
Next Post