author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
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.

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.

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
@ToString
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);
	}

	@Override
	public int hashCode(){
		return getClass().hashCode();
	}
}

Repo

@Repository
public interface ChildRepo extends JpaRepository<Child, Integer>{
}

Controller

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

	@PostMapping("save")
	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.

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, seperti pakai APP ID dan APP Role, tapi agak ribet setup-nya, mungkin bisa di-explore sendiri. 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 generate token, bisa dengan melakukan hal yang sama. Sekarang semuanya sudah siap, bisa test dengan menjalankan aplikasi. Pastikan tidak ada error bermunculan.

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")
	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")
	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")
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")
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.

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. Kalau laptop kita kena hack, hacker tidak akan menemukan password pada database system 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.