Integrasi Spring Boot dengan Vault
Sat. Apr 2nd, 2022 09:00 PM7 mins read
Integrasi Spring Boot dengan Vault
Source: Bing Image Creator - Vault Java

Sebenarnya ini adalah tulisan yang udah lama ingin gw bagikan, tapi gw mager banget😅. Ini adalah lanjutan dari tulisan sebelumnya tentang Vault. Sebelumnya hanya perkenalan aja menggunakan command line. Kali ini lebih ke praktek mengintegrasikannya dengan aplikasi. Berhubung gw sehari-hari lebih sering ngoding pakai Spring dan Java daripada bahasa lainnya, jadi tulisan ini hanya akan membahas integrasinya menggunakan Java dan Spring saja. Scope-nya hanya tentang enkripsi/dekripsi dan menyimpan secret key saja seperti yang pernah dibahas sebelumnya. Untuk full feature yang lebih advanced kalian bisa experiment sendiri.

Practice

Pertama kita setup dulu project-nya menggunakan Spring Init atau tools lainnya yang ada di IDE. Management tools-nya bebas, tapi karena gw udah terbiasa pakai Maven, jadinya gw pake Maven aja untuk management tools-nya. Untuk dependency-nya kita bisa pakai Spring Vault Core atau pakai Spring Cloud Starter Vault. Sebenarnya sama aja, bedanya dengan Spring Cloud Starter Vault kita bisa mengintegrasikan config pada Spring dengan Vault tanpa handle manual. Jadi untuk hal ini gw prefer menggunakan Spring Cloud Starter Vault. Versi yang gw gunakan adalah Spring Boot 2.6.6 dan Spring Cloud 2021.0.1, serta menggunakan Java 11. Berikut full lengkap pom.xml yang gw gunakan.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-vault-demo1</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-vault-demo1</name>
	<description>spring-vault-demo1</description>
	<properties>
		<java.version>11</java.version>
		<spring-cloud.version>2021.0.1</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-vault-config</artifactId>
		</dependency>

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Untuk dependency lainnya, seperti database dan lainnya silakan disesuaikan saja, kebetulan gw menggunakan PostgreSql. Gw juga menggunakan Lombok biar ga perlu generate getter, setter maupun constructor.

Menyiapkan DB

Untuk itu kita setup DB-nya dulu. Sebagai contoh kita akan menggunakan sebuah table bernama Child dengan kolom id, name, dan parentName. Kita bikin simple aja😁. DDL-nya kurang lebih begini:

CREATE TABLE child (
    id          SERIAL CONSTRAINT child_pkey PRIMARY KEY,
    name        VARCHAR(255),
    parent_name VARCHAR(255)
)
;

Lalu kita set config db-nya di application.yml sesuai database yang digunakan.

spring:
  jpa:
    hibernate:
      ddl-auto: none
  datasource:
    driver-class-name: org.postgresql.Driver
    url: 'jdbc:postgresql://localhost:5432/springboobs'
    username: postgres
    password: 12345

Selanjutnya kita bikin Entity, Repo dan Controllernya.

Entity
@Entity
@Getter
@Setter
@EqualsAndHashCode(of = "id")
public class Child{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	Integer id;
	String name;
	String parentName;

	@Override
	public boolean equals(Object o){
		if(this == o) return true;
		if(o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
		Child child = (Child) o;
		return id != null && Objects.equals(id, child.id);
	}

}
Repository
@Repository
public interface ChildRepo extends JpaRepository<Child, Integer>{
}
Controller
@RestController
@RequiredArgsConstructor
public class ChildController{
	private final ChildRepo childRepo;

	@PostMapping("save")
	public Child save(@RequestBody Child childReq){
		Child child = new Child();
		child.setName(childReq.getName());
		child.setParentName(childReq.getParentName());
		return childRepo.save(child);
	}

}

Ada baiknya coba di-run dan execute dulu untuk memastikan config-nya udah bener. Kalian bisa test pake curl, postman, browser, nodejs, atau apapun tools-nya. Kalau semuanya berjalan tanpa error, lanjut ke step berikutnya.

Menyimpan Secret

Nyalakan service Vault dan Unsealed service-nya seperti step-step pada tulisan sebelumnya. Pastikan kita berhasil login dengan lancar dan statusnya Unsealed. Biasanya password disimpan di application.yml, nah sekarang passwordnya akan kita keep di dalam Vault. Untuk itu kita tambahkan secret-nya terlebih dahulu seperti pada tulisan sebelumnya. Kita akan menambahkan key user-db yang berisi value postgres dan pass-db yang berisi value 12345 pada path testing/auth.

testing auth key
menginput key pass-db dan user-db ke Vault

Selanjutnya, config application.yml diubah dengan menambahkan config vault dan mengganti value user & password db menggunakan key dari Vault.

spring:
  config:
    import: vault://
  cloud:
    vault:
      authentication: TOKEN
      token: s.Rx3qmf9848cjr8k4OhA39T3g
      host: localhost
      port: 8200
      scheme: http
      kv:
        backend: testing
        application-name: auth
  jpa:
    hibernate:
      ddl-auto: none
  datasource:
    driver-class-name: org.postgresql.Driver
    url: 'jdbc:postgresql://localhost:5432/springboobs'
    username: ${user-db}
    password: ${pass-db}

Sesuaikan isinya dengan yang kalian punya. import: vault:// bertugas untuk mengimport key dari Vault. authentication: TOKEN artinya kita akan menggunakan token untuk authentikasi. Sebenarnya ada alternatif yang lebih baik selain token, tapi sebagai contoh kita pakai token saja karena paling sederhana. backend: testing adalah path dari secret yang kita simpan. application-name adalah sub-path dari secret yang kita simpan. username: ${user-db} dan password: ${pass-db} adalah variable key yang kita input tadi di Vault. Begitu juga nantinya jika ingin menyimpan secret key untuk hal lain, bisa dengan melakukan hal yang sama. Sekarang semuanya sudah siap, bisa test dengan menjalankan aplikasi.

Melakukan Enkripsi/Dekripsi

Jika tidak ada error kita lanjut ke tahap selanjutnya, yaitu enkripsi/dekripsi. Sebelumnya kita udah membuat transit key menggunakan NIK. Karena gw males bikin key baru, jadi gw akan menggunakan key itu kembali😅. Kalau lupa caranya gimana, bisa dicek aja postingannya. Pada Controller di atas, kita udah ada endpoint untuk melakukan save data, namun masih belum dienkripsi. Misalkan kita ingin mengenkripsi parentName biar nama orang tua kita ga bisa dibaca oleh teman🤭. Kita perlu menambahkan VaultOperations di Controller:

@RestController
@RequiredArgsConstructor
public class ChildController{
	private final ChildRepo childRepo;
	private final VaultOperations vaultOperations;

	@PostMapping("save")
	public Child save(@RequestBody Child childReq){
		Child child = new Child();
		child.setName(childReq.getName());
		Ciphertext encryptedParent = vaultOperations.opsForTransit()
				.encrypt("nik", Plaintext.of(childReq.getParentName()));
		child.setParentName(encryptedParent.getCiphertext());
		return childRepo.save(child);
	}

	@GetMapping("get")
	public Child get(@RequestParam int id){
		Child child = childRepo.getById(id);
		Plaintext decryptedParent = vaultOperations.opsForTransit()
				.decrypt("nik", Ciphertext.of(child.getParentName()));

		Child result = new Child();
		result.setId(child.getId());
		result.setName(child.getName());
		result.setParentName(decryptedParent.asString());
		return result;
	}

}

Sekarang kita tes endpoint tersebut. Jika berhasil, maka data yang disimpan dalam database kurang lebih seperti berikut:

encrypted db
hasil pada Database

Untuk enkripsi/dekripsi files, juga kurang lebih sama seperti code di atas. Bedanya, yang kita gunakan sebagai parameter pada Plaintext atau CipherText adalah bytes dari file tersebut, bukan String. Contohnya seperti ini.


@SneakyThrows
@GetMapping("encryptFile")
public void encryptFile(){
	File file = new File("C:\\readme.pdf");
	byte[] bytes = Files.readAllBytes(file.toPath());
	Ciphertext encrypt = vaultOperations.opsForTransit()
			.encrypt("nik", Plaintext.of(bytes));
	byte[] context = encrypt.getCiphertext().getBytes(StandardCharsets.UTF_8);
	OutputStream outputStream = new FileOutputStream("C:\\readme.pdf.enc");
	outputStream.write(context);
}

@SneakyThrows
@GetMapping("decryptFile")
public void decryptFile(){
	File file = new File("C:\\readme.pdf.enc");
	String string = Files.readString(file.toPath());
	Plaintext encrypt = vaultOperations.opsForTransit()
			.decrypt("nik", Ciphertext.of(string));
	byte[] context = encrypt.getPlaintext();
	OutputStream outputStream = new FileOutputStream("C:\\readme.dec.pdf");
	outputStream.write(context);
}

Pada code di atas, kita melakukan enkripsi pada file readme.pdf di directory C:\. Hasil enkripsinya disimpan dengan nama file readme.pdf.enc. Untuk dekripsi, hasil enkripsi itu dibaca dan didekripsi lalu disimpan hasilnya dengan nama file readme.dec.pdf.

Verdict

Finally, kelar juga tulisannya💃. Sekarang kita sudah berhasil mengintegrasikan Spring Boot dengan Hashicorp Vault. Secret Key yang tadinya disimpan secara plain text di properties atau yml, terutama secret key yang digunakan untuk production, sekarang jadi ga bisa dibaca langsung dari sana lagi, melainkan lewat Vault. Begitu juga dengan data sensitif, sekarang akan dienkripsi terlebih dahulu menggunakan Vault sebelum disimpan. Sehingga database tidak akan menyimpannya dalam bentuk plain text. Proteksi terhadap aplikasi kita sekarang jadi berlapis-lapis. Kalau system kita kena hack, hacker tidak akan menemukan password database kita. Kalau pun hacker berhasil mengakses database, kita tak perlu khawatir data sensitif di database bocor karena sudah dienkripsi. Untuk source code lengkap dari tutorial ini bisa cek di github.

© 2024 · Ferry Suhandri