Securing sensitive data is crucial in cloud-native applications. Micronaut Security, combined with encryption techniques, offers a robust solution for protecting your app’s valuable information.
Let’s dive into how we can use Micronaut Security to lock down our application. First, we’ll need to add the necessary dependencies to our project. In your build.gradle file, include:
implementation("io.micronaut.security:micronaut-security")
implementation("io.micronaut.security:micronaut-security-jwt")
Now, let’s configure basic authentication. In your application.yml file, add:
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
secret:
generator:
secret: "${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}"
This sets up JWT-based authentication. Remember to change the secret in a production environment!
Next, let’s create a simple endpoint that requires authentication:
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
@Controller("/api")
public class SecureController {
@Get("/secure")
@Secured(SecurityRule.IS_AUTHENTICATED)
public String secureEndpoint() {
return "This is a secure endpoint!";
}
}
This endpoint will only be accessible to authenticated users.
But what about encrypting sensitive data? Micronaut doesn’t have built-in encryption, but we can use Java’s cryptography extensions. Let’s create an encryption service:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Singleton
public class EncryptionService {
private static final String ALGORITHM = "AES";
private static final String KEY = "ThisIsASecretKey";
public String encrypt(String data) throws Exception {
SecretKeySpec key = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public String decrypt(String encryptedData) throws Exception {
SecretKeySpec key = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes);
}
}
Now we can use this service to encrypt sensitive data before storing it and decrypt it when needed.
Let’s create a simple entity to store some sensitive user data:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class UserData {
@Id
@GeneratedValue
private Long id;
private String username;
private String encryptedSocialSecurityNumber;
// getters and setters
}
And a repository to manage this entity:
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;
@Repository
public interface UserDataRepository extends CrudRepository<UserData, Long> {
}
Now, let’s create a service to handle user data operations:
import javax.inject.Singleton;
@Singleton
public class UserDataService {
private final UserDataRepository repository;
private final EncryptionService encryptionService;
public UserDataService(UserDataRepository repository, EncryptionService encryptionService) {
this.repository = repository;
this.encryptionService = encryptionService;
}
public UserData saveUserData(String username, String ssn) throws Exception {
UserData userData = new UserData();
userData.setUsername(username);
userData.setEncryptedSocialSecurityNumber(encryptionService.encrypt(ssn));
return repository.save(userData);
}
public String getUserSSN(Long userId) throws Exception {
UserData userData = repository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
return encryptionService.decrypt(userData.getEncryptedSocialSecurityNumber());
}
}
This service encrypts the social security number before saving it and decrypts it when retrieving.
Now, let’s update our controller to use this service:
import io.micronaut.http.annotation.*;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
@Controller("/api")
public class UserDataController {
private final UserDataService userDataService;
public UserDataController(UserDataService userDataService) {
this.userDataService = userDataService;
}
@Post("/user")
@Secured(SecurityRule.IS_AUTHENTICATED)
public UserData saveUser(@Body UserDataRequest request) throws Exception {
return userDataService.saveUserData(request.getUsername(), request.getSsn());
}
@Get("/user/{id}")
@Secured(SecurityRule.IS_AUTHENTICATED)
public String getUserSSN(Long id) throws Exception {
return userDataService.getUserSSN(id);
}
}
This setup ensures that only authenticated users can access these endpoints, and sensitive data is encrypted before being stored.
But we’re not done yet! In a real-world scenario, we’d want to use more secure methods for key management. Storing encryption keys in your code is a big no-no. Instead, consider using a key management service like AWS KMS or HashiCorp Vault.
Let’s modify our EncryptionService to use an external key management service. We’ll use a hypothetical KmsClient for this example:
import javax.inject.Singleton;
@Singleton
public class EncryptionService {
private final KmsClient kmsClient;
public EncryptionService(KmsClient kmsClient) {
this.kmsClient = kmsClient;
}
public String encrypt(String data) throws Exception {
String dataKey = kmsClient.generateDataKey();
String encryptedData = performEncryption(data, dataKey);
String encryptedDataKey = kmsClient.encryptDataKey(dataKey);
return encryptedDataKey + ":" + encryptedData;
}
public String decrypt(String encryptedData) throws Exception {
String[] parts = encryptedData.split(":");
String encryptedDataKey = parts[0];
String actualEncryptedData = parts[1];
String dataKey = kmsClient.decryptDataKey(encryptedDataKey);
return performDecryption(actualEncryptedData, dataKey);
}
private String performEncryption(String data, String key) {
// Actual encryption logic here
}
private String performDecryption(String encryptedData, String key) {
// Actual decryption logic here
}
}
This approach uses envelope encryption, where we generate a unique data key for each piece of data, encrypt the data with this key, then encrypt the data key with a master key stored in the KMS. This adds an extra layer of security and makes key rotation easier.
Another important aspect of securing cloud-native applications is protecting data in transit. Micronaut makes it easy to enable HTTPS. In your application.yml, add:
micronaut:
server:
ssl:
enabled: true
buildSelfSigned: true
This enables HTTPS with a self-signed certificate. In production, you’d want to use a proper SSL certificate.
Don’t forget about input validation! Micronaut provides built-in support for bean validation. Let’s add some validation to our UserDataRequest:
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class UserDataRequest {
@NotBlank
private String username;
@NotBlank
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{4}$", message = "Invalid SSN format")
private String ssn;
// getters and setters
}
Now, let’s update our controller to use this validation:
import io.micronaut.http.annotation.*;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import javax.validation.Valid;
@Controller("/api")
public class UserDataController {
private final UserDataService userDataService;
public UserDataController(UserDataService userDataService) {
this.userDataService = userDataService;
}
@Post("/user")
@Secured(SecurityRule.IS_AUTHENTICATED)
public UserData saveUser(@Valid @Body UserDataRequest request) throws Exception {
return userDataService.saveUserData(request.getUsername(), request.getSsn());
}
// ... rest of the code
}
The @Valid annotation ensures that the request is validated before the method is called.
Logging is another crucial aspect of security. We need to ensure we’re not accidentally logging sensitive data. Micronaut uses SLF4J for logging. Let’s add some logging to our UserDataService:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class UserDataService {
private static final Logger LOG = LoggerFactory.getLogger(UserDataService.class);
// ... other code
public UserData saveUserData(String username, String ssn) throws Exception {
LOG.info("Saving data for user: {}", username);
// DO NOT log the SSN!
UserData userData = new UserData();
userData.setUsername(username);
userData.setEncryptedSocialSecurityNumber(encryptionService.encrypt(ssn));
return repository.save(userData);
}
// ... rest of the code
}
Notice how we log the username but not the SSN. Always be cautious about what you’re logging!
Securing your application also means keeping your dependencies up to date. Micronaut makes this easy with the Micronaut Launch service, which always uses the latest stable versions of dependencies.
Remember, security is not a one-time task, but an ongoing process. Regularly review and update your security measures, conduct security audits, and stay informed about the latest security best practices and vulnerabilities.
In conclusion, securing sensitive data in cloud-native applications with Micronaut involves multiple layers: authentication, encryption, secure communication, input validation, and careful logging. By combining Micronaut’s built-in security features with additional measures like encryption and secure key management, you can create a robust, secure application ready for the cloud.