1. Introduction
One of the issues we all face in current times when developing software is the need and the will to monitor and keep applications in check. Instead of just using metrics to study and investigate our application, we can also use them for other means such as notifications and alerts. The Kong Gateway provides many free options that we can explore in order to achieve this. In this article, I will try to demonstrate a few essential aspects of the free Kong
Gateway
plugins that can help us detect issues with our Java/Kotlin
implementations. I will try to use the most advanced and complex ways of implementing algorithms in both languages and how Kong
can help us. For this article, our real live environment is cameras protecting a prized item. In particular, we will protect a plant of the species Peperomia Obtusifolia
. If you don’t know what that is, here is a photo of my own plant of this species:
So, why Kong
, and why trying to find what it can do? The Kong
framework
, albeit being a paid framework if we truly want to escalate well with our product (like any other framework
out there), does allow us to implement a lot in our application and provide security to it.
In the application for this example, we will also have a look at how we can easily protect, otherwise unsafe running applications with Kong. We’ll have a look at examples of how this may work without a gateway, when it’s a good idea to use it and when it potentially can be too much of a good thing.
But for now, I just want to ramble a bit about security
and why security
is important especially in current times for literally everyone, unless you don’t use the internet at all, have no email, no telephone
and don’t use electricity.
Please note, before you read onwards, that this article, will be subject to multiple updates in the future. The repo supporting this is located on GitHub.
1.2. Security in the eyes of a private user
If we take a look at today's times, we are living in an era, where it's almost impossible not to talk about cybersecurity. Everyone is in a way affected by this. The example where the discussion mostly starts is fraud. It can take many shapes or forms. We may receive emails that may tell some story about us, promise rewards, and prizes, or tell us that we have physical issues and that we need to click on some link to avoid fines, punishment, or whatever. The sky is the limit for fraudsters and even when they realize we took notice, they still keep sending emails using evasive techniques like creating multiple subdomains
, creating emails
with a hash, sending subdomains with a combination of a 6-digit prefix, pretending to be the real deal using strange characters, etc. The worst example is when one gets an email
that does come from a real domain, but it still is nothing but a fraud. Perhaps it came from an infiltrator in that organization, or perhaps it's a masked email. Unfortunately, when we get this kind of overflow of crazy emails, one can’t get rid of them at once just like that, because fraudsters are quite able to create anonymous emails as frequently as possible. We should keep reporting them as phishing attempts, report them to Google
when they come from Gmail
accounts, and block them. Sometimes though, it seems like you can’t block them, but you really always can. I have received all of the above and more but also mails without a sender or emails with a dot before the domain name. However, all emails have headers and in the headers, you can find a ton of information about where the email came from. Your email provider should provide you with an option to see the source headers. And there you’ll find where the email is coming from. An easy way to get rid of some of these emails is to let your email provider use your SPAM
folder, or create redirect rules. However, the problem with that, is that we are still receiving these emails and so the criminal entity has no reason to believe that you’ll never feel enticed to react to that specific email. And if you delete that email automatically, you are still not giving any signs that you understand what fake emails are. If you block the email or the whole domain, then it’s a different story. If your email provider allows you to block emails in such a way that they don’t even enter your email inbox, then that also means that the criminal entity will get a reply that the email wasn’t delivered. There will be no indication that your email account exists, is blocking emails, or anything else. The only data they will get is just that their hacking attempt didn’t find its way into your email. What you should NEVER EVER EVER do, is engaging with the sender, tell the sender off, or take any of this into your own hands. Never do this! Every time you send your frustrations in, you are just giving extra information to the criminal entity. Always read the headers, block the sender with the email in the sender field in the headers, and use at least the phishing option to report your email. Some providers have higher levels of reporting criminal emails. And sometimes it's also a good idea to report to third parties that are responsible for cybersecurity in your own country or internationally.
Will they ever give up? Unfortunately, it can be quite difficult to make them give up. Unfortunately, Cybercrime
are really on the rise, and in order to do something about this, we have to understand that this is part of our new life, accept it, and make a joint effort to stop this.
What does this have to do with this article? The underlying message is that in order to protect our application, we need to take into account that in the worst-case scenario, the threat actors can be very perseverant, and they will go down the rabbit hole and stretch everything they know, to reach their goal.
A few pieces of advice on fraudulent emails. To make sure you block the correct ones please look into the headers of your email. Your provider should have the option to do this and look for these headers in the very long text you get when you view them:
From: "possibly an actual email. this could be masked one so it's not always a good idea to block whatever is put here"
To: undisclosed-recipients:;
Subject: "whatever"
Reply-To: "this is the email you should block"
Mail-Reply-To: "this is the other email you should block"
In-Reply-To: "block this one too"
References: "whatever"
Message-ID: "whatever"
If you need to know from which country and location where this email was sent on the last point of its trip to you then look for these headers:
Authentication-Results: spf=pass (sender IP is "this is the IP you are looking for")
Or you can also look it up on this header:
X-Sender-IP: "this is the IP you are looking for"
You can then pick this IP and look it up in: IP WHOIS Lookup.
An IP WHOIS Lookup determines ownership information of any IP address. Search for IP WHOIS information using the IP…www.whatismyip.com.
1.2 Security in the eyes of a public service
As of today, there are several different algorithms that allow us to create very safe application accesses. We can have security systems purely based on username/password combinations, shared secrets, tokens, up-to-integrity checks, and third-party authenticators. For good measure, we can also monitor our applications using standards that can then be used in different monitoring applications. This means that on one hand, we have to be responsible for protecting our application and gateways to it. On the other hand, we have to make sure that we can detect whenever there is a security breach. In this article, we’ll have a look at both sides of this equation.
1.3. Case
We are going to have a look, as mentioned before at monitoring how is it going with a plant. For that, we have 6 cameras, or at least 6 simulated cameras, and each is implemented with its own security algorithm. Camera 1 is running a metric system known as StatsD. The cameras are numbered from 1 to 6 and their respective security implemented security algorithms are: Basic
, HMAC
, JWT
, ApiKey
, LDAP
, and OAuth2
. The way this works is by running a service that is completely unprotected behind the Kong Gateway. For the purpose of this article, we aren’t going to dive into the Ingress controller or other ways to isolate our domain from the outside world. For now, we just use Kong Gateway in its simplest form. In the end, we’ll have a quick look at how we handle data with StatsD.
2. Generic overview
In order to get our system started, we’ll simply create one Spring Boot service, where we will serve a welcoming message and an endpoint that will simply give out an image. The image we are giving out and the welcome end-point is not protected at all. For this article, we are simply going to have a look at how to protect them via Kong. The following is a visual representation of what we have implemented for our case:
In our system, we’ll have 6 cameras. They are all implemented in the exact same way. Because its important to understand what is happening, let’s first dive into the backend code for the services. For this, we’ll cover a few main sections: the StatsD custom metrics, the Rest services implementation, the reactive WebSockets implementation, and finally the security tags related to OAuth2.
3. StatsD
3.1. Custom StatsD tags
In order to get started with StatsD, we need to understand the basics of how it works. StatsD was originally developed by Etsy and is used for many application developments where we need to monitor our applications. Many frameworks provide standard StatsD metrics, which normally are things like the time it took to make a request, the request payload, response times, and the type of requests. We are usually concerned about how responsive our application truly is, its capacity, its resiliency, and how reactive it is. StatsD was designed precisely to allow us, using a standard, to perform these kinds of measurements. There are records on the web about it starting at around 2011, but odds, are that it was already in the making a few years prior to that. After that, major competitors like Kong and Datadog created plugins and adaptors in order to read this data and present it in the form of graphs. And this is also because, StatsD, being created as a standard, also allows us to create new metrics in a standard way. This is also a focal point in this article, before moving on to security. Kong provides two plugins in order to digest StatsD information. It has a StatsD regular plugin and it has a StatsD advanced plugin. The latter is used for more advanced enterprise applications. The first is the one we going to be looking at. But before any of that, let’s have look at the essential parts to make StatsD a reality purely via Spring and using Graphite as a service to listen to this data:
Graphite opens port 8085 for its web interface. It is not the most glamorous webpage ever, but the service has a great startup time and does not consume a lot of resources. So It works brilliantly for demonstration purposes. The flow starts on each individual camera, where we use a micrometer library to emit our Prometheus data in a StatsD-recognized format. The format we choose for our project is Etsy which is accepted by Graphite. So, how do we do this? First, we add the necessary libraries:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-statsd</artifactId>
</dependency>
This enables us to configure our SringBoot service:
management.endpoints.web.exposure.include=prometheus,health
management.endpoint.health.show-details=always
management.statsd.metrics.export.enabled=true
management.statsd.metrics.export.host=localhost
management.statsd.metrics.export.port=8125
management.statsd.metrics.export.flavor=etsy
In this configuration, we are saying SpringBoot
to open up the Prometheus
and the health endpoints. These two are very important. The Prometheus
endpoint is where we are going to expose our custom metrics. Showing health details can be important because the generic health actuator endpoint is usually a composition of different health endpoints. In our case, we also have the database endpoint and so it is always good to have a full description of what is happening should our service not become healthy. Further, we enable StatsD and what this does is immediately allow our SpringBoot
service to send StatsD data to a destination. We define that destination in the next 2 steps where we say that host is localhost and the port is 8125
. This port is not the GUI
port, which we have seen before to be 8085
. The field flavor is just a fun way to call the protocol which our metric data is being sent with. There are other flavors as alternatives to Etsy
. Finally, we define the frequency of our push to Graphite
and the polling period to any gauges we have implemented. Should any of the Gauges
present a different value than the previous reading, the StatsD exporter will resend all data back to Graphite
once again.
Now that we have everything configured, we can look at the code:
@Configuration
class MetricsConfiguration(
val cameraService: CameraService,
@Value("\${hc.camera.number}")
val cameraNumber: Long,
) {
private val last10FileDeltaNSReading: MutableList<Double> = mutableListOf()
/**
* You should see all fails happening every 10 seconds if your polling rate is every 10 seconds
* For every 10 seconds you'll get all fails in that period
*/
@Bean
@Qualifier("counterMetric")
fun counterMetric(meterRegistry: MeterRegistry) = Counter.builder("camera.fail.count")
.tag("type", "fail_count")
.description("The number of anomalies detected by the camera")
.register(meterRegistry)
/**
* You should see all fails happening every 10 seconds if your polling rate is every 10 seconds
* For every 10 seconds you'll get all heartbeats in the period
*/
@Bean
@Qualifier("heartBeats")
fun heartBeatCounter(meterRegistry: MeterRegistry) = Counter.builder("camera.heartbeats")
.tag("type", "heart_beats")
.description("The number of heartbeats measured per time and counts 1 per second")
.baseUnit("beats")
.register(meterRegistry)
/**
* Measures the time it takes to read an image.
* This is purely to illustrate how can we inject our own custom metrics in StatsD.
*/
@Bean
fun gaugeMetric(meterRegistry: MeterRegistry) =
Gauge.builder("camera.image.read.time", last10FileDeltaNSReading)
{ last10Records ->
logger.debug("$last10FileDeltaNSReading")
measureNanoTime { runBlocking { cameraService.getImageByteArrayByCameraNumber(cameraNumber) } }.toDouble()
.let { record ->
last10Records.add(record)
if (last10Records.size == 11) {
last10Records.removeFirst()
}
logger.info("Refreshed ${last10Records.size} metrics. Last value read is ${last10Records.last()} ns")
record
}
}
.tag("type", "image_read_ns")
.description("Time to read one image from camera in ns")
.baseUnit("ns")
.register(meterRegistry)
}
For this case, we are creating a bean called counterMetric of type Counter. The function of this type of metric is to simply count the occurrences of some event. We can do this programmatically, and we’ll see how this occurs later on. For now, we just need to bear in mind that bean counterMetric and bean heartBeatCounter
will respectively count the number of failed attempts to access an OAuth
endpoint with the wrong scope and the other will count 1 heartbeat per second. Finally, and purely because we want to make a strange metric in our own way, we create a gaugeMetric type Gauge. This gauge will just read the time it takes to read an image from a camera. Gauge is a type of metric, that, unlike counter, it measures the actual value read at one point in time. It is important to be conscient at this point that a Gauge processes information at specific points that we define. We will do this programmatically. So maybe to better make it clear the difference between counter and gauge, we can say that countermeasures the number of times an event has occurred between readings. So, in our specific case, we will register 10 heartbeats per second. Our counter only knows that it counts plus 1 every second. However, when sending this information to Graphite
, it will register the number of times it counted from the last point or up until that point if no reading has yet occurred. So if data is processed every 10 seconds in graphite, we’ll get 10 readings every 10 seconds. Sending data every 2 minutes does not change that. Gauge, however, registers a specific value for that occasion. It does this by keeping the data in a List. However, this list doesn’t really play a role in sending the data to StatsD. Instead, it is there if we want to use a common object to share data between readings. It works more like a bean, and we can insert whatever we want in or do whatever we want with it. The result of the function we pass in is what matters to our Gauge metric. Let’s zoom in on that:
@Bean
fun gaugeMetric(meterRegistry: MeterRegistry) =
Gauge.builder("camera.image.read.time", last10FileDeltaNSReading)
{ last10Records ->
logger.debug("{}", last10FileDeltaNSReading)
measureNanoTime { runBlocking { cameraService.getImageByteArrayByCameraNumber(cameraNumber) } }.toDouble()
.let { record ->
last10Records.add(record)
if (last10Records.size == 11) {
last10Records.removeAt(0)
}
logger.info("Refreshed ${last10Records.size} metrics. Last value read is ${last10Records.last()} ns")
record
}
}
.tag("type", "image_read_ns")
.description("Time to read one image from camera in ns")
.baseUnit("ns")
.register(meterRegistry)
So in our function’s receiver, we get a last10Records
object, which we’ve made into a list. Then we measure how long it takes to read a camera. We have to do it in a blocking way in this case purely because we are not in a coroutine context and because we want to read how long it takes to read the image. If we do it via something else like GlobalScope.launch
, we would not have a realistic idea of how long it took to read the image since, in that way, the coroutine would be spread out between threads, and it would be impossible to have a realistic idea of how bad it would get while reading an image. We add our record measurement to our list of 10 records, and we make sure that we always keep the last 10 records.
Now that we have the metrics in place, we need to see how the counters work and where we trigger the measurements. We first take a look at class CameraServiceApplication
:
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@EnableScheduling
@EnableWebFluxSecurity
@OpenAPIDefinition(
info = Info(title = "OpenAPI definition"),
servers = [Server(url = "\${hc.server.url}/api/v1/hc", description = "Server URL")]
)
@EnableReactiveMethodSecurity
class CameraServiceApplication(
private val webSocketHandler: WebSocketHandler,
@Qualifier("heartBeats")
private val heartBeats: Counter
) {
/**
* You should see 10 heartbeats as a max in graphite if graphite is configure to take samples every 10 seconds.
* For every 10 seconds you'll get 10 heart beats
*/
@Scheduled(cron = "*/1 * * * * *")
fun incrementHeartBeat() {
heartBeats.increment()
}
@Bean
fun webSocketHandlerMapping(): HandlerMapping? {
val map: MutableMap<String, WebSocketHandler> = HashMap()
map["/camera-states-emitter"] = webSocketHandler
val handlerMapping = SimpleUrlHandlerMapping()
handlerMapping.order = 1
handlerMapping.urlMap = map
return handlerMapping
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
runApplication<CameraServiceApplication>(*args)
}
}
}
What is important here to understand and that it really stands out is that we are using a Spring Scheduler to invoke the increment function of our heartBeats Counter.
So now we know what happens to the heartBeats
counter and the gaugeMetrics. So what about the fail counter? This is located at a service level. In our endpoint, we need to protect our entry points, and we do so like this:
@GetMapping(<value = ["/scopes/admin"])
@ResponseBody
@PreAuthorize("@securityService.checkAdminHeader(#scope)")
fun findScopeAdminInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeAdminInfo()
@GetMapping(value = ["/scopes/observer"])
@ResponseBody
@PreAuthorize("@securityService.checkObserverHeader(#scope)")
fun findScopeObserverInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeObserverInfo()
@GetMapping(value = ["/scopes/visitor"])
@ResponseBody
@PreAuthorize("@securityService.checkVisitorHeader(#scope)")
fun findScopeVisitorInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeVisitorInfo()
@GetMapping(value = ["/scopes/researcher"])
@ResponseBody
@PreAuthorize("@securityService.checkResearcherHeader(#scope)")
fun findScopeResearcherInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeResearcherI>nfo()
Here we are using PreAuthorize annotations to indicate an Expression Language (EL) condition to validate that we can make the rest call. These annotations are activated in the Application Running class:
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@EnableScheduling
@EnableWebFluxSecurity
@OpenAPIDefinition(
info = Info(title = "OpenAPI definition"),
servers = [Server(url = "\${hc.server.url}/api/v1/hc", description = "Server URL")]
)
@EnableReactiveMethodSecurity
class CameraServiceApplication(
private val webSocketHandler: WebSocketHandler,
@Qualifier("heartBeats")
private val heartBeats: Counter
)
So going back to our methods, we see that they all use a component called securityService. Essentially this is a component that is also a Service and we are simply calling a method with it called checkHeader. In this method we check that the header matches our scope expectation and then inside we make our counter go up 1 unit:
private fun checkHeader(scope: String, expectedScope: String) =
logger.info("Accessing with scope {}", scope).let {
when (scope == expectedScope) {
true -> true
else -> counterMetric.increment().let { false}
}
}
We will see further on that Gauges
are cumulative, but for now this is all the basic information we need to know about a possible custom StatsD configuration.
3.2. StatsD via Kong
Custom tags are a great way to send metrics to Graphite, and using micrometer like a mechanism to trigger that is quite easy to do. However, in most cases, and as I mentioned before, we don’t really need manually, or custom-created metrics. This is really where StatsD becomes so relevant. Kong provides many functions with the standard StatsD plugins. So let’s have a look at how that works. We’ll use the deck container to make the Kong update possible. Further down this article, we’ll look into the details of what the deck container is about. For now, let’s just bear in mind that the deck container allows reloading and updating our konh.yml file. We’ll only configure StatsD to work with camera 1. We first need to configure a route:
- host: camera-1-service
name: camera-1-service
path: /api/v1/hc
port: 8080
protocol: http
routes:
- name: camera-1-service-route
paths:
- /camera-1-service/api/v1/hc
strip_path: true
So this just means that we’ll be able to access a service running locally on host camera-1-service
, on port 8080, with path /api/v1/hc
. This location is then made accessible via port 8000 (the Kong gateway port for our example) on the path /camera-1-service/api/v1/hc
. This ensures that when we try to access our camera-1-service
, we will always have to go through the Kong gateway. This mechanism isn’t that much different than a simple proxy. However, because Kong is a gateway platform, it is highly manageable, and we can make changes in real-time. One of the things that we can do is directly add the StatsD plugin:
- config:
host: graphite
metrics:
- name: request_count
sample_rate: 1
stat_type: counter
- name: request_size
sample_rate: 1
stat_type: histogram
- name: response_size
sample_rate: 1
stat_type: histogram
port: 8125
prefix: statsd
enabled: true
name: statsd
service: camera-1-service
In this case, we are using out-of-the-box metrics. The request_count, the request_size, and finally the response_size. Data will be sent every second to Graphite. As we have seen before, this is equivalent to the polling interval, and it works in exactly the same way.
4. Common Camera Functions
As I mentioned before, all cameras are going to start out of the same deployment package. They have common functionalities and others that are specific to certain protocols. For now, let’s just have a look at the functionalities we can use for all our cameras.
4.1. Welcoming message
The welcoming message is just a text that is useful for testing what we are going to have a look at. It is as simple as a GET endpoint:
@GetMapping
fun welcome() = "Welcome to Healthy cameras!"
Next to it, we have the endpoint responsible for retrieving the camera image for a given time:
@GetMapping(value = ["/camera"], produces = [MediaType.IMAGE_JPEG_VALUE])
@ResponseBody
suspend fun getImageProtected(): ByteArray? = withContext(Dispatchers.IO) {
cameraService.getImageByteArrayByCameraNumber(cameraNumber)
}
And its implementation here:
@Service
class CameraService(
@Value("\${hc.camera.bank}")
val bank: String
) {
suspend fun getImageByteArrayByCameraNumber(cameraNumber: Long) =
Path.of(System.getProperty("user.dir"), "${bank}/camera$cameraNumber")
.takeIf { it.exists() }
?.let { resource ->
val allImages =
Files.walk(resource).use { paths ->
val filter = paths
.sorted()
.filter { it.name.endsWith("jpg") }
filter.toList()
}
if (allImages.size > 0) {
val countImages = allImages?.size ?: 0
val delta = (10 / countImages.toDouble())
val currentMinute = findCurrentMinute()
val index = (((currentMinute + 1) / delta).toInt()).absoluteValue - 1
allImages?.get(if (index == -1) 0 else index)?.let { resource.resolve(it.name) }
} else return null
}?.readBytes()
fun findCurrentMinute() = LocalDateTime.now().second.toString().last().digitToInt()
}
Generally speaking, once we are authenticated, we can get an image at a point in time. Since this exercise is just a simulation, we will get a new image according to the matching fraction of 10 seconds
. This way we get a feeling of movement when we refresh the front-end page.
The front end will connect to the backend regardless of whether the user is authenticated or not. Here we will be sending status data in real-time to the front end. This will be done via Websockets:
@Component
class ReactiveWebSocketHandler(
val healthIndicator: PingHealthIndicator
) : WebSocketHandler {
override fun handle(webSocketSession: WebSocketSession): Mono<Void> =
webSocketSession.send(fluxIteration.map { payload: String? -> webSocketSession.textMessage(payload ?: "") })
.and(webSocketSession.receive().map { obj: WebSocketMessage -> obj.payloadAsText }.log())
private val eventFlux = Flux.generate { sink: SynchronousSink<String?> ->
Event(
id = randomUUID().toString(),
dateTime = now().toString(),
status = healthIndicator.health().status
).let { event ->
try {
sink.next(objectMapper.writeValueAsString(event))
} catch (e: JsonProcessingException) {
sink.error(e)
}
}
}
private val fluxIteration = Flux.interval(Duration.ofMillis(1000L)).zipWith(
eventFlux
) { _: Long, event: String? -> event }
}
5. Authentication protocols
5.1. Basic Auth
Of all the 6 protocols to access the cameras we are going to have a look at, this is probably the most unsafe one to use. The way the message flow works can be seen in the following diagram:
In any case, we know that our camera isn’t originally protected and that we, for some odd reason, want to implement the most simple, and of course, most unsafe way to access it via a simple username
and password
. If your command line contains the base64
command, we can already try and do a simple test. Let’s say that I’m user Lucy
and my password is winkelallemaal
. We also assume that this credential is already configured to work in the system. With basic auth, all we have to do is to send Lucy:winkelallemaal
to the server with your request. However, we do not send it blank. We use the base64 encoding version of it. This would be done via the command line in the following way:
echo ‘Lucy:winkelallemaal’ | base64
The result will be something like this:
THVjeTp3aW5rZWxhbGxlbWFhbAo=
So now this just looks like some cyphered thing and we send it this way through the wire in the Authorization header:
Authorization: basic THVjeTp3aW5rZWxhbGxlbWFhbAo=
The issue with this security protocol arises when for one reason or another other someone grabs hold of the token. In that case, a simple decode operation will do to retrieve the username and password:
echo THVjeTp3aW5rZWxhbGxlbWFhbAo= | base64 — decode
And the result will be:
Lucy:winkelallemaal
But of course, we want to learn how this works with Kong
and this is actually a great start to learning at least about the existence of these 6 security protocols.
In our Kong.yaml file we need to add the route to our camera-1-service
and also create a plugin associated with it. For the path we can use the following:
- host: camera-1-service
name: camera-1-service
path: /api/v1/hc
port: 8080
protocol: http
routes:
- name: camera-1-service-route
paths:
- /camera-1-service/api/v1/hc
strip_path: true
- host: camera-1-service
name: camera-1-open-service
path: /api/v1/hc/actuator
port: 8080
protocol: http
routes:
- name: camera-1-service-open-route
paths:
- /camera-1-service/api/v1/hc/actuator
strip_path: true
- host: camera-1-service
name: camera-1-open-service-webjars
path: /api/v1/hc/webjars
port: 8080
protocol: http
routes:
- name: camera-1-service-open-webjars
paths:
- /camera-1-service/api/v1/hc/webjars
- host: camera-1-service
name: camera-1-open-service-v3
path: /api/v1/hc/v3
port: 8080
protocol: http
routes:
- name: camera-1-service-open-v3
paths:
- /camera-1-service/api/v1/hc/v3
strip_path: true
- host: camera-1-service
name: camera-1-socket-service
path: /api/v1/hc/camera-states-emitter
port: 8080
protocol: http
routes:
- name: camera-1-service-socket-route
paths:
- /camera-1-service/api/v1/hc/camera-states-emitter
strip_path: true
For camera 1 we define 3 routes. The first is unique to camera 1
. The other two are routes that we are going to replicate for all cameras. We are going to protect route 1 because that is where we’ll get the camera info and current image. The other 2 routes are the actuator and WebSockets routes. It is important that they stay open and unprotected so that we can check in real-time the state of the camera.
In order to protect the first route, we create a plugin configuration for the Basic Authentication
:
- config:
hide_credentials: true
enabled: true
name: basic-auth
service: camera-1-service
When we create this, we have already protected our route, but we still do not have any users. In order to make that configuration, we can simply run a few commands, which can run via a GUI or in our case, via cUrl commands:
curl -d "username=camera1&custom_id=CC1" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera1/basic-auth \
--data "username=cameraUser1" \
--data "password=administrator"
In the first command, we create a consumer. In the second, we define a username and password for that same user.
This is the simple way to configure Basic Auth with this plugin.
5.2. HMAC
HMAC is an extraordinary algorithm that is based on location, timestamp a secret, and a choice of protocol. Sending data via HMAC is as simple as described in the following diagram:
We can see that it follows exactly the same flow as the Basic Auth
protocol. However, its token is not possible to unencrypt. It is created in one way and furthermore, it depends on the location where it has been generated and the path for which it has been specifically generated. This means that if a hacker grabs the HMAC token, they won’t be able to use it anywhere even though it's a valid token. Using the same secret or private key on both ends is a concept known as symmetric key encryption. This algorithm is frequently used in M2M
(machine-to-machine) connections and file transfers. The idea is to authenticate and authorize data transfers between parties in a secure way. The more complicated the secret the more difficult it will be to guess which one we are using. As we have seen before, this is why in some cases, we use the PEM private key. For our case though, and to remain minimalistic in the use of HMAC, we’ll just use dragon as our secret.
To make this configuration in Kong we first, as always need to create the routes. For this case let’s just look at the camera 2 route:
- host: camera-2-service
name: camera-2-open-service-v3
path: /api/v1/hc/v3
port: 8080
protocol: http
routes:
- name: camera-2-service-open-v3
paths:
- /camera-2-service/api/v1/hc/v3
strip_path: true
And now let’s protect it with HMAC:
- config:
hide_credentials: true
enabled: true
name: hmac-auth
service: camera-2-service
With this configuration, we are protecting camera 2, but for now, it is not accessible. To make it accessible we need to issue a few commands:
curl -d "username=camera2&custom_id=CC2" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera2/hmac-auth \
--data "username=cameraUser2" \
--data "secret=dragon"
For this case, we also need a consumer. In this case, we also create a username for it, but instead of a password, we create a secret
We have not seen yet how exactly the algorithm works, but that is also not the purpose of this article. What actually is the purpose of this article is to show you what variables we need to create a minimally acceptable HMAC token. For this it is probably better to jump into an algorithm created with crypto:
createCamera2HmacHeaders = (method: string, path: string): Partial<any> => {
const username = 'cameraUser2', secret = 'dragon', algorithm = 'hmac-sha256';
const currentDateTimeUtc = new Date().toUTCString();
const digestBodyHeader = `SHA-256=${crypto.createHash('sha256').digest('base64')}`;
const signingString = `x-date: ${currentDateTimeUtc}\n${method} ${path} HTTP/1.1\ndigest: ${digestBodyHeader}`;
const signature = crypto.createHmac('sha256', secret).update(signingString).digest('base64');
const authorization = `hmac username="${username}", algorithm="${algorithm}", headers="x-date request-line digest", signature="${signature}"`;
return {
'Digest': digestBodyHeader,
'Authorization': authorization,
'X-Date': currentDateTimeUtc,
'Content-Type': 'application/json',
};
}
Let’s go through roughly what we intend here. When we try to connect to camera 2, we need to create an HMAC token. For that, we pass in the path we are trying to access and the method with which we want to perform the request. These are only GET requests in this project, but they can be applied to any method. We start out by initializing variables that are important for our algorithm: username, secret, algorithm, and current date. In our HMAC header, we need to include a Digest header. That header is created by providing a text composed of "SHA=256=base64Hash". For now, we just need to know that this is needed for the algorithm to work.
So essentially for this implementation of the HMAC algorithm, we need 4 basic elements in our header. The digest, which contains encrypted information about the algorithm we are using, authorization, which is an encrypted string containing different elements, the currentDate, and finally just a Content-Type header which determines the type of content we are going to send through the wire. All four headers are very important but the most extensive one is the Authorization, which has the following form:
Authorization : hmac username="cameraUser2",algorithm="hmac-sha256",headers="x-date request-line digest",signature="<signature>"
Sending requests through the wire this way means that each request can be sent with a new HMAC header. And the point is that, since we can do that, then why not do that? HMAC can be a heavy algorithm to process. We sacrifice performance in order to achieve optimal security.
For this camera, we’ve only scratched the surface of how HMAC works. We are only making get requests and none of them has a payload to encrypt. Although HMAC is specifically designed to protect outbound data, our goal in this article is simply to have it configured and understand a bit about how it works.
5.3. JWT
JWT
tokens are created usually as a token that the user can add to their requests in order to authenticate and authorize the use of certain methods. JWTs
can be created however we want. It relies on the principle of asymmetric key encryption. The encryption happens on the signature level. Normally with JWT
we only need a token. A token may have an expiry date and it contains all sorts of information about the user logged on and the user permissions. A JWT token, pretty much like basic authentication, does not hide a lot. It just Invalidates if the request is accompanied by an invalid token. A JWT
token is composed of a header, a payload, and a signature. The header and the payload are base64 encoded, while the signature is a function of the header and the payload. In this function, we use a private key to sign our token. If it's not correctly signed, then our application will reject it. The validation of the token needs a public key. No password is necessary for this process, although it may be used in order to request a new JWT
token. Essentially this is how it works:
Using the Kong plugin we are then able to implement this workflow:
This is a very simple workflow where we make requests to the Kong Gateway purely by adding a token like this one:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0.R4S8yV_CPJRvJ2dAX_56HfNUZrbKr7Jt4AxEmsqqx8w
Later on, we’ll see in our app how and when Kong can return these tokens to us. For now, let’s have a look at JWT.io and see what deciphering what can be decyphered
in this token can tell us:
If you are one of these people very sensitive to red-colored messages, the first thing you probably realize when checking this token is that JWT.io, is telling us that the token is invalid. We did not insert our signature into the Verify Signature box. Since we have not given a secret for JWT.io to be able to validate, it just says that the token is invalid. But let’s have a look at the header and at the payload. We were able to decipher these two and the reason being is that they are just base64 encrypted. Let’s decipher this. At this point, you already have seen how it works. We just issue a few commands:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 --decode
And the result is:
{"alg":"HS256","typ":"JWT"}
What this tells is that this is a basic JWT token implemented with the algorithm HS256, which is used to verify the signature we are sending.
Now let’s check the body:
echo "eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0" | base64 --decode
The result is:
{"iss":"nSD7ZSqSwSx207vR5XxiocTUZhsB2QxV","expiresIn":"12h","algorithm":"HS256"}
Iss is the issuer. This issue can be specified and if we don’t, Kong will give us a random one. The issuer is an essential part of the signature, although it is not protected. For every time we ask for a JWT a new secret is generated. The secret for this run was: xwRs1oR22OhzBeq2hWH4NnIxdF5Jr6jv
. If we input that in JWT.io we’ll be able to get validated and whatever operation we provide in the payload and in the request we are performing will be allowed to go through security. If then the authorization allows or further operations don’t matter. We are able to reach the system already, but only under the conditions we specified in the payload:
The secret is key here, and it's never shared. This is how we validate the request via JWT.io. But of course, here we are in possession of the private key. The algorithm on the Kong side, only validates if the request is valid with its public key.
To make this configurable in Kong we need to first configure our route:
- host: camera-3-service
name: camera-3-open-service-v3
path: /api/v1/hc/v3
port: 8080
protocol: http
routes:
- name: camera-3-service-open-v3
paths:
- /camera-3-service/api/v1/hc/v3
strip_path: true
And then just configure the plugin:
- enabled: true
name: jwt
service: camera-3-service
Once the service is enabled with camera 3 running we can then request our JWT token params:
curl -d "username=camera3&custom_id=CC3" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera3/jwt -H "Content-Type: application/x-www-form-urlencoded"
curl -X GET http://127.0.0.1:8001/consumers/camera3/jwt > ../e2e/cypress/fixtures/CC3KongToken.json
These commands create a consumer called camera3. Then we generate our token with the POST request. In order to be able to use a token in the website, I create a file called CC3KongToken.json, where we can then find the issuer and the secret. The latter is also what we have referenced as the private key. It is called private because is not shared. Another camera would accept a request with the same token should it be configured with the same public key.
If we perform that last GET request, we’ll get an answer similar to this one:
{
"data": [
{
"tags": null,
"rsa_public_key": null,
"consumer": {
"id": "137558e2-bcc0–4fc4-a423–33a37e2d9f6e"
},
"key": "nSD7ZSqSwSx207vR5XxiocTUZhsB2QxV",
"created_at": 1665153305,
"algorithm": "HS256",
"id": "9d6febc6–2602–42ae-a2b2–4ec3924e7bce",
"secret": "xwRs1oR22OhzBeq2hWH4NnIxdF5Jr6jv"
}
],
"next": null
}
To get the JWT token, we do it manually, and we have an example of that in the typescript code. We first encode the token:
encodeToken = (payload: any) => {
const header = {
"alg": "HS256",
"typ": "JWT"
};
const encodedHeader = this.encodeObject(header);
const encodedData = this.encodeObject(payload);
return `${encodedHeader}.${encodedData}`;
}
Then we sign the complete token:
signToken = (body: any, secret: string) => {
const jwtToken: any = this.encodeToken(body);
const signature = this.base64url(this.cryptoJS.HmacSHA256(jwtToken, secret));
return `${jwtToken}.${signature}`;
}
We can look at JWT on several levels. We can, in a way, consider it to be some sort of evolution to pure HMAC. Perhaps what’s not so fine with JWT is the fact that we do share some data anyways. Of course, this is nothing that a TLS tunnel cannot solve. But again, providing some information to a potential hacker is always worse than providing none. What JWT allows as an extra to the first two security protocols is the ability to attribute permissions and roles. Effectively Authorization. Perhaps one more difference we need to understand is that the token in this case is a Bearer token:
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0.R4S8yV_CPJRvJ2dAX_56HfNUZrbKr7Jt4AxEmsqqx8w"
}
The bearer token is actually part of the same problem that we’ll see with OAuth. If the token is valid then the official RFC6750 describes it as:
A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can
What this means is that, as long as the token is valid, anyone in possession of it can access the protected application
.
5.4. API Key
There isn’t really much to say about this type or form of authentication. API Key is a form of authentication that essentially allows a user to access an application purely on the bases of the possession of an API key. The apiKey is not sent via an Authorization header but via a header of the same name:
Camera 4 is protected with this security protocol. To configure it, we first configure the routes:
- host: camera-4-service
name: camera-4-open-service-v4
path: /api/v1/hc/v3
port: 8080
protocol: http
routes:
- name: camera-4-service-open-v4
paths:
- /camera-4-service/api/v1/hc/v3
strip_path: true
And then we activate the apiKey plugin:
- enabled: true
name: key-auth
service: camera-4-service
In this case, we also need a consumer. To create it, we can issue these commands:
curl -d "username=camera4&custom_id=CC4" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera4/key-auth
curl -X GET http://127.0.0.1:8001/key-auths > ../e2e/cypress/fixtures/CC4KongKeys.json
In short, we first create a username called camera4, and then we request an apiKey. With the key-auths endpoint, we are able to make a request and as a response we’ll get a request of this form that we then are able to find in the CC4KongKeys.json file:
{
"data": [
{
"ttl": null,
"consumer": {
"id": "99647244-bc39-4981-a187-00e2402be24b"
},
"created_at": 1665157122,
"id": "7a5a39f5-011d-4557-b021-d35e6fb60030",
"tags": null,
"key": "87FNNInZXturFdEsal3InGMu7Me3CX5C"
}
],
"next": null
}
In this case, the key is our apiKey. Let’s see how a request with the apiKey looks like in the typescript code:
retrieveWelcomeMessage(input: Map<string, string>): Observable<string> {
return this.httpClient.get(input.get("path") || "", {
headers: this.emit({
apiKey: input.get("key") || ""
}),
responseType: 'text'
})
}
So what’s important here to see that we are sending the apiKey in the clear through the wire:
{"apiKey":"87FNNInZXturFdEsal3InGMu7Me3CX5C"}
An apiKey
can be useful if we want to automate connections between applications. It is very lightweight and requires a few amounts of processing. However, if we choose to use this security protocol, we have to make sure that we are using it via a TLS/HTTPS
connection or otherwise, an encrypted connection.
5.5. LDAP
This security protocol is something that reminds us of windows, but it’s not necessarily related intrinsically to it. In this article it is not my goal to explain how it works, but how can we get it to be configured via the Kong gateway? We are going to abstract ourselves from how LDAP implements itself and focus on its actual implementation of it. The simplest form of an LDAP flow is the following:
First we implement the route:
- host: camera-5-service
name: camera-5-service
path: /api/v1/hc
port: 8080
protocol: http
routes:
- name: camera-5-service-route
paths:
- /camera-5-service/api/v1/hc
strip_path: true
Then we can configure the plugin:
- config:
attribute: cn
base_dn: dc=example,dc=org
header_type: ldap
hide_credentials: true
ldap_host: openldap
ldap_port: 1389
enabled: true
name: ldap-auth
service: camera-5-service
For LDAP, we have something a bit different than in the other protocols. LDAP needs an external LDAP service in order to be able to work. So, essentially, using LDAP via Kong, we are not only proxying our requests to the camera but also to the authentication LDAP service. The LDAP service is what we find in docker-compose to be the openldap service.
Once having all of this in place, then we only need to send our request to the service with the authentication header next to it. In this case, we will be sending a very similar header to the one we used for camera 1 during the basic-auth algorithm, but in this case, we call it ldap:
{ Authorization: ldap YWRtaW46cGFzc3dvcmQ= }
So this is the same principle and this is implemented in the same way as the basic-auth request:
retrieveWelcomeMessage = (input: Map<string, string>): Observable<string> => this.httpClient.get(input.get("path") || "", {
headers: this.emit({
"Authorization": `ldap ${(this.getCredentials(input))}`
}),
responseType: 'text'
})
5.6. OAuth2
Finally, we reach OAuth2
. This security
algorithm is a bit complicated because it involves an external party to our application. This external party is usually what we see when we try to log on to some applications and the applications ask us to log in via Google, Facebook, iCloud, GitHub, or any other authentication provider. When we log in through the authentication provider for the first time it may ask us if we want to provide our data to any of them like our name and email, when we say yes and log in, it only means that the authentication was successful and furthermore, our application has registered us with the data provided by the authentication provider. Following that, we can successfully log in. For this part of this article, we will skip the registration process and just use an authentication provider as if we were already registered. Authentication providers can be implemented in multiple ways and that alone is something way out of the scope of this article. However, I wanted to create an example with something so that we could see OAuth2 running locally. So I’ve created a simple SpringBoot service secured with Basic Auth. This, of course, is just used as a simulation of the real thing. The idea is that, after we authenticate, the service will look for the user in the database, fetch the user’s scope and use it in order to request a bearer authentication header to the camera-6-service. This bearer token can then be used to access the camera 6 service via HTTPS. The following data flow is an illustration of the whole process:
In order to configure this we first need to define the route for camera 6:
- host: camera-6-service
name: camera-6-service
path: /api/v1/hc
port: 8080
protocol: http
routes:
- name: camera-6-service-route
paths:
- /camera-6-service/api/v1/hc
protocols:
- https
strip_path: true
And then finally we can configure the OAuth2 plugin. For OAuth2 I’ve decided just protect the route via just curl
commands:
curl -X POST http://127.0.0.1:8001/consumers/ \
--data "username=camera6" \
--data "custom_id=CC6"
curl -X POST \
--url http://127.0.0.1:8001/services/camera-6-service/plugins/ \
--data "name=oauth2" \
--data "config.enable_password_grant=true" \
--data "config.enable_client_credentials=true" \
--data "config.scopes=admin" \
--data "config.scopes=observer" \
--data "config.scopes=visitor" \
--data "config.scopes=researcher" \
--data "config.mandatory_scope=true" \
--data "config.enable_authorization_code=true" > ../e2e/cypress/fixtures/CC6KongProvOauth2.json
cp ../e2e/cypress/fixtures/CC6KongProvOauth2.json ../cameras-auth-service/target/CC6KongProvOauth2.json
curl -X POST http://127.0.0.1:8001/consumers/camera6/oauth2 \
--data "name=Camera%20Application" \
--data "client_id=CAMERA06CLIENTID" \
--data "client_secret=CAMERA06CLIENTSECRET" \
--data "redirect_uris=https://127.0.0.1:8443/camera-6-service/api/v1/hc" \
--data "hash_secret=true" > ../e2e/cypress/fixtures/CC6KongOauth2.json
curl -X GET http://127.0.0.1:8001/oauth2_tokens/
So we first create a consumer
, then we configure the OAuth2
with scopes admin, observer, visitor, researcher, and then finally we configure the consumer with its own client_id, client_secret and redirect_uris. Files CC6KongProvOauth2.json
and CC6KongOauth2.json
are created because we need the data generated with these commands to run our tests.
Now we can have a look at our authentication
provider. In the project, it is called cameras-auth-service
. This service is connected to an SQL
database containing the following data:
INSERT INTO hc_user (username, password_hash, roles) VALUES ('admin', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'admin');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('officer', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'observer');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('edwin', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'visitor');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('johannes', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'researcher');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('lucy', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'visitor,researcher');
The field roles are actually the scope. All the users have the same password and the password is admin. It is never enough to keep reiterating that admin is ok to use as a password for demo purposes ONLY. Never use this kind of password in your own account.
To retrieve the authorization bearer token, there are two ways to do it. We are going to do it by first retrieving an access code and then using that to get the access token. This is implemented in this Kotlin code:
fun createToken(
principal: UsernamePasswordAuthenticationToken, ctr: ClientTokenRequest
): Mono<ResponseEntity<BearerTokenEnriched>> = principal.authorities.map { it.authority }[0].let { scope ->
webFluxClient.post().uri(authUrl).header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE)
.accept(APPLICATION_JSON).body(createAuthFormRequestBody(scope))
.retrieve().bodyToMono(ResAuthorizeBody::class.java).map { authorizeBody ->
logger.info("Response redirect uri: ${authorizeBody.redirectUri}")
logger.info("Input redirect uri: ${ctr.redirectUri}")
if (validate) {
authorizeBody.validate(ctr)
}
webFluxClient.post().uri(tokenUrl).body(
createTokenFormRequestBody(scope, authorizeBody)
).retrieve().bodyToMono(BearerToken::class.java).map { bearerToken ->
bearerToken.enrich(authorizeBody.redirectUri)
}
}
.flatMap { it }
.map {
ResponseEntity
.status(HttpStatus.OK)
.header("Authorization", "bearer ${it.accessToken}")
.location(URI.create(it.redirectUri))
.body(it)
}
}
If we zoom in on the way the first request is made, we can see that we need to create a special token, where we need the clientId, the user’s scope, the provisionKey (this is provided by Kong), the authenticatedUserId and the responseType :
private fun createAuthFormRequestBody(scope: String) = BodyInserters.fromFormData(
AuthorizeBody(
clientId = clientId,
scope = scope,
provisionKey = provisionKey,
authenticatedUserId = authenticatedUserid,
responseType = responseType
).toMultiValueMap()
)
This first request will provide us with the access code which we then can use to make the request that will give us the authorization bearer code
private fun createTokenFormRequestBody(
scope: String,
authorizeBody: ResAuthorizeBody
) = BodyInserters.fromFormData(
TokenRequest(
clientId = clientId,
clientSecret = clientSecret,
authenticatedUserid = authenticatedUserid,
scope = scope,
grantType = grantType,
code = authorizeBody.redirectUri.split("=")[1]
).toMultiValueMap()
)
The result of signing in to the auth app on http://localhost:8000/cameras-auth-service/api/v1/cameras/auth/login will be this:
{
"refresh_token": "zy2hwv8ov90e5XWRXmbNxZ9NiITMkRYG",
"access_token": "se4rpVnIofug611XgoqYwAmV7FSTwoMy",
"expires_in": "7200",
"token_type": "bearer",
"redirect_uri": "https://localhost:8443/camera-6-service/api/v1/hc?code=HGsfiB63XY2MyoX7YZk42ryLKNchucyP"
}
So this is a set of data necessary to perform our first OAuth2 request to our camera.
Sending the data with the bearer token will give us access to the camera:
{
"Content-Type": "application/text",
"Authorization": "Bearer kY0FMKviJgHQTuSjEE9ago1CSz5of8WA"
}
What’s inside the Bearer token is not readable to anyone except our camera. Passing this through to the back end will be easy to make. When we access our OAuth2 application we then get access to header x-authenticated-scope
. We know that when we pass through the Kong gateway, we are already in the clear to access our application. Kong provides us with not only this header but also x-authenticated-userid
, x-consumer-username
, and x-credential-identifier
. This is all of what we above close to the beginning of this article.
6. Running the demo
To run the demo please use the following command:
make dcup-full-action
When running, just go to http://localhost:8000
7. Conclusion
What we have seen in this article is that by using an application Gateway, we can control almost any aspect facing the application. In this article, we have only discussed the possibility of interpreting and reading metrics, but there are lots of other possibilities.
In any case, we also ended up seeing how Basic Auth
can work very well for simple demo applications. This is what we did for the Authentication
provider in the OAuth2
case for example. We now know that Basic-Auth can be very unsafe. We are essentially sending username and passwords through the wire without any active protocol to protect this pair. We could argue that a TLS tunnel would eventually solve this problem. But eavesdropping in a TLS tunnel has been proven to be possible and this is a risk we do not want to take. So Basic-Auth is something definitely not to use in production.
When we looked at HMAC
, we also saw a couple of fundamental benefits. It is highly encrypted and it is pretty much unbreakable even if someone gets their hand on the HMAC token gibberish. We know that HMAC is also based on the origin and from the token we can’t get the origin and so many aspects of the token aren’t really accessible to potential hackers. What can be argued as the downside of the HMAC algorithm is also that its secret is fixed and also that, it is used for permanent automated connections like M2M. We used this by providing and username and a secret only. We do not send in plain text through the wire and encryption is dependent on timestamp and location and of course the secret. But no matter how far we think this through, the backend will recognize this request, check that its integrity is ok and that the user is authorized purely based on that secret. Getting that secret from the token may be next to impossible, but via social engineering then it becomes less clear. This is part of the reason that instead of using something like dragon as a secret, people use something they can configure and cannot remember, like for example a PEM
private key. But another thing that may stand in the way of HMAC is the definition of profiles, roles, and access limitations to certain methods.
For JWT
, we also saw that all we need is a secret and an issuer. This algorithm is also pretty very powerful in protecting our applications. Perhaps where it fails is that the header and the payload are always in the clear as requests. This means that a hacker might not get a request through, but because the header and the body are always just Base64
encoded, the decode operation is always possible. The body is in any case very handy because there we can define, for example, roles that the user may have, but again, if the roles are exposed, then a hacker already has that piece of information. Is that enough for a hacker to be able to wreak havoc? That alone happens not because it does not necessarily compromise the application but because it is still a piece of information that helps to increase the surface area for a possible hacker attack
Using an APIKey
is an interesting concept that essentially doesn’t translate very well into attributing more security to our application key. Essentially, a consumer with an APIKey
is just another user with a username and password. Only that in this case the user cannot determine the password. It’s the API that gives the user an APIKey
to access the application. It can be used for pre-authentication purposes or to create another layer of deterring form of security.
LDAP
, is another form of security that introduces almost the same kind of paradigms, problems, and situations that the Basic Auth provides. The difference is that LDAP manages accesses on a network level. In our case, we did not explore how it really works, but it’s just important to bear in mind that it is more complex, and it provides Authentication and Authorization capabilities. Only that for the purposes of this article it doesn’t really make a difference in terms of making an application secure, because it is still based on the principle that the user sends his password in the clear.
Finally, we have learned how OAuth2
works. This is the most advanced algorithm of the six we’ve seen so far. If we have an authorization provider that allows for OTP
(One-Time-Password), MFA, SMS authentication, or any other form of safe login, then we can guarantee that the access to our application will be safe in the same way. Once we get a token from our authentication provider, we know that we can use it to access our application. Normally via OAuth
, we get a screen asking for permissions. This is part of what the scopes mean. Each scope means that the user can access certain parts of the application. On the other hand, it means also that the camera-6-service will be able to access the provided user information associated with that scope.
StatsD
is also a perfect example of how we can send standard statistics.
As I mentioned in the introduction, this article will be subject to more frequent reviews given its experimental nature.
I have placed all the source code of this application on GitHub.
I hope that you have enjoyed this article as much as I enjoyed writing it.
Thank you for reading!
8. References
- StatsD @ Etsy
- statsd / statsd @ GitHub
- What is StatsD exactly? @ Datadog
- Kubernetes Documentation / Concepts Services / Load Balancing / Networking Ingress
- A Deep Dive Into the Four Types of Prometheus Metrics
- Defining custom metrics in a Spring Boot application using Micrometer
- Kong Blog Posts
- Kong API Gateway – Observability with Prometheus, Grafana and OpsGenie by Daniel Kocot
- Defining custom metrics in a Spring Boot application using Micrometer
- Spring Boot app metrics - with Prometheus and Micrometer
- https://github.com/prometheus/statsd_exporter
- StatsD: What Is It and How To Monitor It
- Kong Gateway
- Kong Vitals
- Kong Plugin Hub
- Kong Lua Rules
Top comments (0)