🛡️ Securing Your Traefik Services with a Custom Token Checker Middleware

Ajinkya KumbharAjinkya Kumbhar
5 min read

When managing multiple microservices behind Traefik, security is a top priority. One of the most critical challenges is ensuring that only valid requests (those containing valid tokens) are allowed to reach your services. In multi-tenant environments or when handling sensitive APIs, it’s crucial to immediately block revoked or expired tokens to avoid security breaches.

In this post, we’ll walk through how we built a custom Traefik middleware plugin that blocks blacklisted or expired tokens using Redis. We'll explore the challenges we faced, the limitations of developing custom plugins for Traefik, and how we implemented workarounds to get around these challenges.

🛑 The Problem: Dealing with Expired and Blacklisted Tokens

In a typical microservice architecture, when a token (like a JWT or developer token) is revoked, it’s added to a blacklist, often stored in a high-performance database like Redis. The idea is simple: whenever a request is made with a token, we check if it exists in the blacklist. If it does, the request should be blocked right away.

However, Traefik doesn’t natively support dynamic token revocation checks or blacklist middleware. This means that even if a token is revoked, Traefik cannot block it before it reaches your backend services, leaving a potential security hole and causing unnecessary load on your backend services.

🛠️ Developing Traefik Plugins: The Power and the Pitfalls

While Traefik’s extensibility through plugins is a powerful feature, it comes with some significant challenges. Let’s look at the primary approaches for extending Traefik to handle dynamic token revocation:

đź§© WebAssembly (WASM)-Based Plugin Limitations

Traefik allows you to write custom middleware using WebAssembly (WASM), which is a great option for languages like Rust or AssemblyScript. However, this approach presents a few key limitations:

  • Network Limitations: WASM is sandboxed and doesn’t support low-level network operations like connecting to Redis via TCP. This is a dealbreaker when you need to perform real-time checks against an external database.

  • Debugging Challenges: Debugging WASM plugins is more challenging than debugging Go-based plugins due to limited debugging tools within the WASM runtime.

🔄 Adding Code to Traefik Directly

An alternative approach is to contribute directly to the Traefik source code. This approach allows you to deeply integrate with Traefik’s core, but it comes with its own issues:

  • Cumbersome Maintenance: Modifying Traefik’s source code means you have to build Traefik from source and maintain your own fork. You must also ensure that your changes don’t conflict with future updates.

  • Scalability Issues: If you modify Traefik itself, you’ll face challenges in scaling your solution across multiple instances or projects, as you’d need to manually propagate any code changes.

🛠️ Developing Custom Traefik Plugins

The best approach for most scenarios is to develop a custom plugin. Here’s why:

  • Flexibility: Custom plugins allow you to extend Traefik without modifying its core code. You can write middleware in Go (Traefik's default language for plugins) or use the WASM interface for other languages.

  • Easier Maintenance: You won’t need to worry about keeping up with internal Traefik code changes, as your plugin is a separate entity from Traefik itself.

  • Seamless Integration: Custom plugins integrate easily into Traefik’s configuration. You don’t have to rebuild Traefik every time you make a change, which saves time.

🚀 Solution: Using Yaegi and Workarounds for Redis Communication

Since Traefik plugins can be written in Go or WebAssembly, we initially considered using WebAssembly (WASM). However, WASM posed limitations for our use case, particularly in making network calls to Redis.

We decided to bypass the WASM limitations by opting for a Yaegi-based solution, a Go interpreter that allows for scripting directly within Go code. This provided us with the flexibility to use direct TCP communication with Redis for token checks.

📡 Creating a TCP Client to Redis Using Yaegi

Yaegi allows you to write Go code and execute it dynamically at runtime. We created a workaround for Redis communication by utilizing Yaegi’s capabilities to connect to Redis via TCP, even though direct Redis access is limited in WASM. Here’s how we implemented it:

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

// connectToRedis creates a TCP connection to Redis
func connectToRedis(redisURL string) (net.Conn, error) {
    conn, err := net.DialTimeout("tcp", redisURL, 5*time.Second)
    if err != nil {
        return nil, fmt.Errorf("failed to connect to Redis: %w", err)
    }
    return conn, nil
}

// checkTokenInRedis checks if a token is blacklisted in Redis
func checkTokenInRedis(conn net.Conn, token string) bool {
    command := fmt.Sprintf("EXISTS %s\\r\\n", token)
    _, err := conn.Write([]byte(command))
    if err != nil {
        log.Println("Error writing to Redis:", err)
        return false
    }

    buffer := make([]byte, 1024)
    _, err = conn.Read(buffer)
    if err != nil {
        log.Println("Error reading from Redis:", err)
        return false
    }

    return string(buffer) == "1"
}

🎯 Key Takeaways

  1. WASM Limitations and Workaround: We faced significant challenges with WASM, particularly its inability to handle network operations like Redis communication. Our workaround was to use Yaegi, which allowed us to implement TCP communication with Redis in a way that WASM couldn’t.

  2. Direct Redis Integration: By using Yaegi and directly communicating with Redis over TCP, we circumvented WASM’s limitations, allowing us to perform real-time checks for blacklisted tokens without sacrificing performance.

  3. Improved Debugging: Working with Yaegi in Go made debugging much easier than working with WASM, providing us with the flexibility to quickly test and iterate on our solution.

  4. Custom Middleware: This solution allowed us to develop a custom middleware that integrates seamlessly with Traefik. It avoids the need to modify Traefik's core or rebuild it after every change.

🎉 Conclusion

Developing a custom Traefik middleware to check blacklisted tokens using Redis was both challenging and rewarding. By leveraging Yaegi, we were able to work around the limitations of WASM and build a solution that meets our performance and security needs.

Traefik’s plugin system is a powerful tool, but it requires careful consideration of the trade-offs involved. Whether you're working with WASM, adding code to Traefik directly, or developing custom plugins, the key is to choose the right approach based on your specific requirements. Happy coding, and stay secure!

đź”® Future Optimisation: Redis Connection Pooling

While current solution works efficiently, we can further optimise it by implementing Redis connection pooling. By reusing Redis connections rather than creating a new connection for every token check, we can significantly reduce the overhead and improve performance, especially in high-throughput environments.

0
Subscribe to my newsletter

Read articles from Ajinkya Kumbhar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ajinkya Kumbhar
Ajinkya Kumbhar

Enthusiastically exploring the world of DevOps