Mix.install([
{:grading_client, path: "#{__DIR__}/grading_client"}
])
:ok
Welcome to Part 3! This section is dedicated to discussing some of the more abstract, non-vulnerability related parts of Application Security. Concepts to keep in mind as you work your craft at building code.
While it may seem obvious, having secrets such as passwords or auth_tokens hard coded in your files is a big yikes! Even if you catch it later, more than likely your code has been uploaded to a Version Control System and is now in your commit history - which can be scary / difficult to undo.
More than that, while it may be convenient for testing / building typically in production you don't care about those secrets anyways - so you might as well incorporate them securely from the get go!
There are a number of different ways you can manage your secrets for use in production systems. Most of them are implementation specific which varies on your build and deploy processes.
A very easy way to prevent secrets being added to go though is to access them via Environment Variables!
Remove the hard-coded secret from the code sample below and replace it with an environment variable named envar_secret
.
Use System.get_env/1
on line 2.
# let's assume there is an environment variable named 'envar_secret'
super_secret_password = "p@ssw0rd"
# DO NOT CHANGE CODE BELOW THIS COMMENT
IO.puts(super_secret_password)
So the unthinkable happened and a secret value you didn't want getting out there, got out there. It happens to the best of them...a lot.
Let's just hope that we made it easy to rotate tokens out! What's that you say? If we change a Service to Service token in one place, we would have to change it simultaneously in every service that uses it? Well...how many services do we have...? 1000?!
The half-serious situation described above outlines a common problem that can be solved by integrating your applications to use a Key Management Service (KMS) to handle secrets. A simple enough solution that is harder to implement later on, so you're best to integrate it from the onset.
Additionally, thought can be put into how you architect data flows with regards to how secrets will be used (as to avoid a secret being used in multiple places).
Rate limiting restricts the number of requests that can be allowed in a certain time frame on a specific resource. Rate limiting can be implemented to protect against a variety of attacks or abuses:
- Preventing Denial Of Service attacks (limiting the number of calls to expensive endpoints)
- Limiting brute force attempts (such as on one time codes and passwords)
- Programmatic abuse of services
When rate limiting a new action, begin by asking these questions:
- "What are the maximum number of calls to my action that a reasonable non-malicious user would feasibly make in a given time period?"
- "Is calling this action an expensive operation? If yes, how many calls in a minute would be enough to overburden the service?"
- "Is this action a candidate for brute force attacks?" (E.g. passwords, authentication tokens, one time passcodes)
If the answer to one or more of those questions is yes, consider putting a limit on the number of times that the action can be called. Good rate limiting keeps in mind legitimate use cases and does not block regular user functionality, but protects the service from malicious or burdensome behavior.
More often than not, rate limiting should be as specific as possible. For instance, it is better to add rate limiting on a single GraphQL type than to add a generic limit to the entire /GraphQL endpoint.
Sometimes known as the Principle of Minimal Privilege or the Principle of Least Authority, the Principle of Least Privilege (PoLP) means that every entity* is only strictly given the essential privileges needed to perform its requirement.
E.g. A script that executes on a cron schedule that monitors the output of a log file only needs read privileges and should not be given write privileges.
*Entity: generic term for an arbitrary process, user, program, etc. found within a Data System
- Better Data System Stability - When an entity is limited in the scope of changes it can make to a system, it is easier to test its possible actions and interactions in the context of the Data System.
- Better Data System Security - When an entity is limited in the system-wide actions it may perform, vulnerabilities / compromises in one application cannot be used to exploit the rest of the business or adjacent Data Systems.
- Ease of Deployment - In general, the fewer privileges an entity requires, the easier it is to deploy within a larger environment.
Zero Trust is not only about controlling user access, but requires strict controls on device access as well. With this, Zero Trust systems need to monitor how many different devices are trying to access their network, ensure that every device is authorized, and assess all devices to make sure they have not been compromised. This further minimizes the attack surface of the network.
Microsegmentation is the practice of breaking up security perimeters into small zones to maintain separate access for separate parts of the network. Some of the benefits of doing so are:
- Granular Access Policies - we can create super specific policies for access to each segment!
- Targeted Security Controls - we can develop each micro-perimeter to specifically target the security risks and vulnerabilities of the resources in that micro-segment!
- Establishing Identities and Trust - we can implement, monitor, and control the “never trust, always verify” principle much easier!
Zero Trust is designed to contain attackers so that they can not move laterally. You may be asking what does that even mean? In network security, “lateral movement” is when an attacker moves within a network after gaining access to it, which can be very difficult to detect.
Zero Trust helps contain attackers because the access is segmented and has to be reestablished periodically, limiting them from moving across to other microsegments within the network.
It's no surprise that MFA is a core part of the Zero Trust Model. Systems using MFA require more than one piece of evidence to authenticate a user, with the most common form being a one time password (OTP).
Defense in depth is a security approach of having defense mechanisms in a layered approach to protect valuable assets. Castles take a similar approach, where they have a moat, ramparts, towers, and drawbridges instead of just one wall as protection.
An example of developing a web application using defense in depth could be:
- The developers (like yourself) receive secure coding training
- The codebase is checked automatically for vulnerabilities using Semgrep
- The codebase is also checked for outdated dependencies using Dependabot
- The application is regularly tested by the internal security team
- Multiple development environments are used such as Develpoment, Staging, and Production
Using more than one of the following layers constitutes an example of defense in depth:
- Authentication and password security
- Hashing passwords
- Multi factor authentication (MFA)
- Encryption
- Security Tooling
- Security Awareness Training (sounds familiar 😉)
- Logging and Monitoring
- Firewalls (hardware and software)
- Demilitarized zones (DMZ)
- Virtual Private Networks (VPN)
- Biometrics
- Data-centric security
- Physical Security (such as locked server rooms)
<- Previous Module: OWASP || Next Module: GraphQL Security ->