Migrating to Microservices: Securely & Safely
Microservices allow you to build your applications as services that are deployed and maintained independently. While many software organizations have been using microservices and containers for years, a considerable amount are still in the early phases of adopting and migrating their legacy architectures heading into 2018. Microservices have a lot in common with Service-Oriented Architectures (SOA), but have their own unique properties too. Compared to traditional monolithic software development, microservices speed up our deployments, let us iterate faster, and take full advantage of modern computing platforms. There are great benefits to using microservices, but there are also many architectural complexities to consider as well as cultural and procedural issues to solve. Keeping your architecture secure with decentralized governance can be challenging and requires us to think carefully upfront about how to scaffold security within our core design and habits.
Because microservices promote polyglot programming and decoupled systems, this can be more challenging than having all of our developers use the same base Java Spring project with security baselines built in. As we migrate our security controls from a monolithic world to microservices, we need to make sure we’ve translated them correctly.
Modern computing tools, such as containers, serverless runtimes, and horizontally-scalable cloud services, make it attractive to build our software in a way that leverages these features. With monolithic applications, we typically scale up by running the application on additional servers. This has traditionally been adding additional physical servers or virtual machines. With microservices, we distribute and scale our services horizontally rather than vertically. We can independently run our services and deploy them using containers and serverless runtimes. A small change to a monolithic application’s code often requires redeploying your entire application, whereas a change to a single microservice does not require redeploying all services and minimizes any downtime.
With microservices, you’re not just shackled into using Java. Since your services are independently deployed and maintained, you can use a variety of programming languages, frameworks, and runtime environments. You can use Java where you need to, but you can also use Go, Scala, Rust, Python, or whatever you need. Microservices using serverless functions and containers billed by the second, (such as Azure Container Instances), offer compelling usage and pricing models to keep in mind while you design. However, we do need to consider the complexity we’re adding in exchange for flexibility.
Technologies Changing the Way We Build Software
Microservices are frequently deployed using containers and serverless runtimes. With containers, we have Docker,then an increasing number of other options and runtimes to provide flexibility:
- Docker - Everyone’s favorite container platform.
- CRI-O - Interface for using any OCI-compliant container runtime with Kubernetes.
- Rkt - Container engine by CoreOS.
When running containers, you’re generally using a system to scale them and manage them. These container orchestration systems and distributed operating systems provide robust APIs to manage and secure your services. They also provide an additional layer of complexity where we need to insert security around deployment capabilities (deploy, kill, and modify services) as well as general infrastructure hardening.
Serverless runtimes and Functions as a Service (FaaS) are supported natively within several major cloud platforms, and also are available as frameworks within container orchestration systems. It is even possible to run containers on Lambda using tools like Scar.
- AWS Lambda
- Azure Cloud Functions
- Serverless Frameworks & Tools
Web frameworks that play nicely with microservices promote building loosely coupled systems and provide tools and patterns that best support microservice development. Here are some of the languages and frameworks we see in many microservice shops, including our own:
Challenges With Building and Maintaining Secure Microservices
In a monolithic system, your teams, data, and assets move in the same direction, for the most part, and workflows are fairly linear. In microservice development organizations, many development teams may be working on different services at any given time. Making security a focal and repeatable part of development is a challenge because services are being built and deployed within their own lifecycle and on their own release schedules. While we don’t want to slow our developers down, we need to make sure we are providing some form of “lowest common denominator” with regards to security. To be successful, consider some of the following:
- Provide security through architecture. Implementing the API Gateway pattern allows you to abstract authentication away from your services and allows them to focus on entitlements and access control decisions. There are many good options to choose from, including AWS API Gateway, Azure API Management Gateway, and Kong. JWT works well for this purpose in conjunction with a good API Gateway solution.
- Identify any shared components and harden those interfaces to prevent data exposure or unauthorized access between systems. If you are using Apache Kafka, Kinesis, or another method of passing events between systems, ensure that you’ve restricted who can produce and consume sensitive data.
- Build security into your development pipelines and ensure you have adequate automation to catch the routine things or drift from a secure state.
- Harden your underlying platform with policies that define secure default policies for new services and required security settings. This can be achieved with AWS, Azure, GCP and other platforms, as well as using resources such as Kubernetes’ PodSecurityPolicy.
In a monolithic architecture, sensitive production data is typically less spread out and within a finite set of data stores, such as a database, backups, and maybe (cringe) developer laptops. In a microservice environment, we often have a database (or a few database types) per service. As an example, at nVisium we have some services that use Postgres, Cassandra, and Redis. This requires additional considerations around data access patterns, persistence, and how we protect the data as it traverses various tiers of the architecture. Managing and distributing secrets across many different teams also requires careful consideration in order to maintain separation of concerns and other practices. Fortunately, cloud and container orchestration systems have first-class support for secret and key management in distributed systems. Thinking about these things as early as possible ensures your architecture will grow in a way that does not introduce friction later or piles on an immense amount of security technical debt early on.
Microservices are prone to issues related to eventual consistency. In a monolithic system, data flows in a more linear and predictable way through defined paths in a specified order. With microservices and distributed systems in general, we need to anticipate that events may happen out of order and we need to defensively code against these considerations. These issues may expose your systems to race conditions and various timing attacks.
Asset Inventory and Data Classification
With the ability to dynamically provision resources on demand and stand up or deprecate new services at lightspeed, it becomes incredibly hard to understand our architecture in real time and see where our data is moving to. Traditional snapshot-in-time architecture reviews get stale quickly, as our architecture is changing faster than we can draw our data flow diagrams. We need to consider the tools and APIs our platforms provide us with in order to keep tabs on how things are changing and where the risks are moving.
Tools, such as CloudFormation and Terraform, give us the ability to define our infrastructure as code. This allows us to automate more of our infrastructure and deployment than before and replaces the work of many. While many view this as a concern initially, it is also an opportunity to embrace improved auditability of your infrastructure and simplified management of security.
Many of our modern development systems support rich integrations with our build tools and container orchestration platforms. They also support Webhooks that allow us to build event-driven security systems into our DevOps pipelines. It is important to take advantage of every tool we have at our disposal to analyze events and data and model them for security as they occur.
To Secure and Beyond
Starting on the journey towards microservices can be a dramatic shift for many development organizations, but that doesn’t mean it has to be painful. Automation is essential to being successful at making security scale within modern development organizations, but so is a well-thought out security architecture. Focus on the opportunities to build your systems securely out of the gate and take advantage of the tools at your disposal. By inherently building security into the evolving architecture, you will set the foundation for a secure, scalable platform for many years.