12-Factor Apps have become a popular model of application development. This model became extremely popular with more and more organizations adopting public cloud since it enabled portability and agility in a multi-environment setup. In this blog post, I’ll walk through the 12 factors of a 12-Factor App. The aim is to understand how you can shift left with security and build 12-Factor Apps securely.
Security is often left as a catch up step in application development life cycle. Retrofitting security into workflows can and almost always leaves loose ends. By shifting left with security, product teams can incorporate security best practices during the application development process.
As the name suggests, a 12-Factor App complies with 12 factors. These applications have the following differentiating features:
- They are portable between environments,
- They have clean interactions through contracts materialising as APIs,
- They use automation for operational setup, minimising onboarding time,
- They are more suitable for cloud platforms,
- They aim at minimising divergence in a multi environment setup,
- They are CI/CD ready,
- They are easily scalable, which means they do not need significant changes to tooling, architecture, workflows etc. to scale.
Let’s take a dive into how we can build maturity in security through each factor using the Security Pillar of AWS Well-Architected Framework. The Security Pillar has the following 5 important areas:
A Secure 12-Factor App
1. Codebase
- A 12-Factor App must have a version controlled codebase to track all changes. This helps track
who changed what and when
.- There should be a 1:1 codebase:app co-relation - A single codebase but many deploys.
The second mandate becomes a controversial especially as teams move towards a monorepo based micro-services model where each application is a subdirectory in the same repository as opposed to separate repository entirely. My take on the same is that the 1:1 codebase:app correlation still stands true since your application code is encapsulated in the one directory, irrespective of the fact the there are multiple other applications in their own directories within the same repository. A fair few CI/CD tools have plugins that enable deploying multiple applications from the same repository.
Identity and Access Management shines when considering security at the codebase level.
- Use least privileged principle to grant access to the codebase. You can segregate based on role like Admin, Read, Write etc.
- Code review model can be very powerful in ensuring that a single individual cannot make changes on their own. This can be enabled with either a Pull Request mechanism or a pairing workflow such as in Xtreme Programming.
Incident Response benefits from access control and audit capability.
- The audit information from the version controlled codebase proves vital during post event root cause analysis.
2. Dependencies
- The crux of this factor is to never assume anything.
- Explicitly declare and isolate dependencies.
The beauty of this factor is that it enhances the portability of the application. As long as you have the application runtime
and the codebase, you can use the dependency manager to install all dependencies. For example rubygems
for Ruby via Gemfiles/ npm
for Node.js using package.json/pip
and requirements.txt for python etc. In case there is a need to have any underlying packages on the host like development packages, use tools such as Amazon Machine Images (AMIs), Chef, Ansible, Puppet etc. to produce automated copies/snapshots of the host OS rather than relying on manual setup.
Target Infrastructure Protection and Detection.
- With dependency isolation, it is easier to identify and remove vulnerabilities with specific versions by upgrading in isolation. This way you don’t end up impacting any other services running on the system.
3. Configuration
- The only part of the application that has environment specific information.
- Always store separate from codebase.
- This does not mean application internal configuration like routes, initialisation etc.
Configuration often contains database credentials, third party integration API keys. Storing variables/constants within the code based on environments is considered a violation of this factor. Instead rely on environment specific configuration files or environment variables based on the framework/programming language in use. Dependency isolation becomes more important when we consider attached services configuration across environments like databases, caches, queues etc.
Enter Data Protection.
- Consider using a secure secret store to ensure credential security.
- Configuration can also be stored as files, where you can use access control policies to restrict access.
- Consider encrypting the configuration files with only the application having the decrypt permissions.
- ALWAYS keep configuration segregated from the codebase.
4. Backing Services
- Also known as Attached Services.
- All interactions to databases, caches, queues etc should occur through contracts like a URL/credentials.
A 12-Factor App does not discriminate between local/third party services. Since all interactions are through contracts it makes it fairly easy to restore services like databases from backup and connect back with the application.
When we consider Infrastructure Security, URL based access helps enable security at a network level. You can easily configure network level controls like firewalls to secure connectivity.
Data Protection plays a critical role in ensuring secrets are secured in a similar way as in the configuration factor. An authorization mechanism helps build access based on the principle of least privilege.
5. Build, Release and Run
There should a clear segregation of the different phases of the applications
- Build: where the code is complied, dependencies vendored within the application and packaged.
- Release: where packaged code from the build phase is deployed along with environment specific configuration ready for execution.
- Runtime: when the application is executing in the environment.
It is very important to consider application level versioning (semantic versioning) which can be tied in with the version of the codebase deployed across environments.
Segregation of phases and their access control ties in with Data Protection and Identity and Access Management.
Restrict access level based on phase. For example,
- build phase should have nothing to do with production data access or
- release phase should not concern itself with the ability to access code repository.
6. Process
- The application should be executed as one or more processes to enable fast scaling.
- The application should not assume that data would persist on disk or in memory.
- Any data that needs to persist must be stored in backing services like database, cache, queues etc.
This is another factor that ensures that development teams drop any assumptions. This helps build portability and agility in the application. You can deploy with ease, without having to care about the underlying system configuration. For example webhooks, sticky sessions are a violation to the 12-Factor App.
Using mechanisms like encryption in transit and at rest secures the data in line with the Data Protection.
- Since the application is executed as one or more process, each process can have its own mechanism of securing data at rest and transit.
This also ties in with Infrastructure Protection
- The underlying systems/backing services can be patched in isolation to get mitigate vulnerabilities with minimal impact.
7. Port Bindings
- Use port bindings/URLs to expose services.
This factor emphasises on building self contained applications. This effectively means that the application itself can behave as a backing service. For example, the application becomes an attached service for the web server that essentially is only behaving as a reverse proxy.
In line with Data Protection, you can configure protecting data in transit.
- Enforce end-to-end encryption needs with the TLS offloading at the application URL.
- Another option is to configure mTLS or token based authorization to services in a micro-services environment.
A beauty of enforcing encryption in transit is the alignment with Infrastructure Protection.
- Configuring host level firewalls enables securing the network and allowing interactions on certain ports only.
8. Concurrency
- Processes are first class citizens.
- Split the application into processes like say scheduler, worker, web etc.
- Maximise robustness with fast startup and graceful shutdown.
If you comply with the principle of self contained apps sharing nothing then you can configure each process to scale individually. This helps add reliability and concurrency to operations. Instead of building runtime specific start-up and shut-down configuration, consider using OS level process managers like systemd to execute applications.
Containers shine in this space adding to the isolation and self contained nature.
Consider Infrastructure Protection and Detection through
- isolation of processes,
- building granular controls to assist with incident response, apart from improving the reliability of the system.
9. Disposability
- Processes should be built in a manner to ensure graceful shutdown and fast startup.
- Applications are stateless and ensure idempotent transactions.
Think of this in terms of a consumer model. Say your application is reading from a queue or an event stream, as soon as the job is completed, the consumer should gracefully shutdown till its next invocation. The concept of transactions comes into play here which is needed to make an operation idempotent.
This is very critical to the Recovery aspect of Infrastructure Protection since processes are disposable and can recover from where they left off in events of sudden death.
10. Dev/Prod Parity
- Build like for like environments.
- Vendorise dependencies.
Ensure that system configuration is the same across environments like runtime version, database, cache configuration etc. Dependencies should be vendorised and packaged with application code to enable version parity across environments.
Essentially 12-Factor Apps should be designed for continuous deployment so that the deployment gap between Dev, test and production environments are minimal. The power of automation enables reducing lead times in building identical environments. Using automation and CI/CD tools help ensure identical build and release environment.
- A multi-environment parity helps test out environmental changes in isolation especially inline with Infrastructure Protection while changing/updating network and host level boundaries.
- Multi-environment parity ensures Detective Controls can be tested and applied across environments without any exceptions.
11. Logs
- Logs are event streams.
- A 12-Factor App should not manage logs.
- A 12-Factor App should output the logs as an event stream to standard output.
No matter how much I stress about logging, it is always less. Logs are the core of observability.
Using tools such as fluentd
, awslogs
help channel log streams to Security Information and Event Management (SIEM) tools to run analysis.
- Logs are critical to Detective Controls workflow.
- Granularity in logging enables appropriate detail during post event analysis, which is critical for Incident Response.
12. Admin Processes
- Run admin/management tasks as one-off processes.
- Automate admin tasks with access control.
A 12-Factor App should need no human intervention for it’s operational needs. Admin tasks should be run as a on-off and that too through automated scripts rather than relying on human source of truth. Running migrations should be considered to be added to the release phase and assets compilation as a part of build phase rather than as admin tasks.
Quite obviously, Identity and Access Management plays an important role here.
- Ensure Access control when assigning administrative privileges.