1+ terraform {
2+ required_providers {
3+ hpcr = {
4+ source = " ibm-hyper-protect/hpcr"
5+ version = " >= 0.1.6"
6+ }
7+
8+ tls = {
9+ source = " hashicorp/tls"
10+ version = " >= 4.0.1"
11+ }
12+
13+ random = {
14+ source = " hashicorp/random"
15+ version = " >= 3.4.3"
16+ }
17+
18+ time = {
19+ source = " hashicorp/time"
20+ version = " >= 0.9.1"
21+ }
22+
23+ ibm = {
24+ source = " IBM-Cloud/ibm"
25+ version = " >= 1.37.1"
26+ }
27+ }
28+ }
29+
30+ # make sure to target the correct region and zone
31+ provider "ibm" {
32+ region = var. region
33+ zone = " ${ var . region } -${ var . zone } "
34+ }
35+
36+ resource "random_uuid" "attestation_uuid" {
37+ }
38+
39+ locals {
40+ # some reusable tags that identify the resources created by his sample
41+ tags = [" hpcr" , " sample" , var . prefix ]
42+ }
43+
44+ # the VPC
45+ resource "ibm_is_vpc" "attestation_vpc" {
46+ name = format (" %s-vpc" , var. prefix )
47+ tags = local. tags
48+ }
49+
50+ # locate the COS instance
51+ data "ibm_resource_instance" "attestation_cos_instance" {
52+ name = " secure-execution"
53+ location = " global"
54+ service = " cloud-object-storage"
55+ }
56+
57+ # create a bucket we use to upload the attestation record
58+ resource "ibm_cos_bucket" "attestation_cos_bucket" {
59+ resource_instance_id = data. ibm_resource_instance . attestation_cos_instance . id
60+ bucket_name = format (" %s-bucket" , var. prefix )
61+ region_location = var. region
62+ storage_class = " standard"
63+ }
64+
65+ # get the authentication token we use to upload the attestation record
66+ data "ibm_iam_auth_token" "attestation_auth_token" {
67+ }
68+
69+ # the security group
70+ resource "ibm_is_security_group" "attestation_security_group" {
71+ name = format (" %s-security-group" , var. prefix )
72+ vpc = ibm_is_vpc. attestation_vpc . id
73+ tags = local. tags
74+ }
75+
76+ # rule that allows the VSI to make outbound connections, this is required
77+ # to connect to the logDNA instance as well as to docker to pull the image
78+ resource "ibm_is_security_group_rule" "attestation_outbound" {
79+ group = ibm_is_security_group. attestation_security_group . id
80+ direction = " outbound"
81+ remote = " 0.0.0.0/0"
82+ }
83+
84+ # the subnet
85+ resource "ibm_is_subnet" "attestation_subnet" {
86+ name = format (" %s-subnet" , var. prefix )
87+ vpc = ibm_is_vpc. attestation_vpc . id
88+ total_ipv4_address_count = 256
89+ zone = " ${ var . region } -${ var . zone } "
90+ tags = local. tags
91+ }
92+
93+ # we use a gateway to allow the VSI to connect to the internet to logDNA
94+ # and docker. Without a gateway the VSI would need a floating IP. Without
95+ # either the VSI will not be able to connect to the internet despite
96+ # an outbound rule
97+ resource "ibm_is_public_gateway" "attestation_gateway" {
98+ name = format (" %s-gateway" , var. prefix )
99+ vpc = ibm_is_vpc. attestation_vpc . id
100+ zone = " ${ var . region } -${ var . zone } "
101+ tags = local. tags
102+ }
103+
104+ # attach the gateway to the subnet
105+ resource "ibm_is_subnet_public_gateway_attachment" "attestation_gateway_attachment" {
106+ subnet = ibm_is_subnet. attestation_subnet . id
107+ public_gateway = ibm_is_public_gateway. attestation_gateway . id
108+ }
109+
110+ # archive of the folder containing docker-compose file. This folder could create additional resources such as files
111+ # to be mounted into containers, environment files etc. This is why all of these files get bundled in a tgz file (base64 encoded)
112+ resource "hpcr_tgz" "contract" {
113+ folder = " compose"
114+ }
115+
116+ locals {
117+ # URL to the attestation object
118+ attestationKey = format (" %s.enc" , random_uuid. attestation_uuid . result )
119+ attestationURL = format (" https://%s/%s/%s" , ibm_cos_bucket. attestation_cos_bucket . s3_endpoint_public , urlencode (ibm_cos_bucket. attestation_cos_bucket . bucket_name ), urlencode (local. attestationKey ))
120+ # contract in clear text
121+ contract = yamlencode ({
122+ " env" : {
123+ " type" : " env" ,
124+ " logging" : {
125+ " logDNA" : {
126+ " ingestionKey" : var.logdna_ingestion_key,
127+ " hostname" : var.logdna_ingestion_hostname,
128+ }
129+ },
130+ " env" : {
131+ " AUTHORIZATION" : data.ibm_iam_auth_token.attestation_auth_token.iam_access_token,
132+ " S3_URL" : local.attestationURL
133+ }
134+ },
135+ " workload" : {
136+ " type" : " workload" ,
137+ " compose" : {
138+ " archive" : hpcr_tgz.contract.rendered
139+ }
140+ },
141+ " attestationPublicKey" : tls_private_key.attestation_enc_rsa_key.public_key_pem
142+ })
143+ }
144+
145+ # create a key pair for the purpose of encrypting the attestation record
146+ resource "tls_private_key" "attestation_enc_rsa_key" {
147+ algorithm = " RSA"
148+ rsa_bits = 4096
149+ }
150+
151+ # create a random key pair, because for formal reasons we need to pass an SSH key into the VSI. It will not be used, that's why
152+ # it can be random
153+ resource "tls_private_key" "attestation_rsa_key" {
154+ algorithm = " RSA"
155+ rsa_bits = 4096
156+ }
157+
158+ # we only need this because VPC expects this
159+ resource "ibm_is_ssh_key" "attestation_sshkey" {
160+ name = format (" %s-key" , var. prefix )
161+ public_key = tls_private_key. attestation_rsa_key . public_key_openssh
162+ tags = local. tags
163+ }
164+
165+ # locate all public image
166+ data "ibm_is_images" "hyper_protect_images" {
167+ visibility = " public"
168+ status = " available"
169+ }
170+
171+ # locate the latest hyper protect image
172+ data "hpcr_image" "hyper_protect_image" {
173+ images = jsonencode (data. ibm_is_images . hyper_protect_images . images )
174+ }
175+
176+ # In this step we encrypt the fields of the contract and sign the env and workload field. The certificate to execute the
177+ # encryption it built into the provider and matches the latest HPCR image. If required it can be overridden.
178+ # We use a temporary, random keypair to execute the signature. This could also be overriden.
179+ resource "hpcr_contract_encrypted" "contract" {
180+ contract = local. contract
181+ }
182+
183+ # construct the VSI
184+ resource "ibm_is_instance" "attestation_vsi" {
185+ name = format (" %s-vsi" , var. prefix )
186+ image = data. hpcr_image . hyper_protect_image . image
187+ profile = var. profile
188+ keys = [ibm_is_ssh_key . attestation_sshkey . id ]
189+ vpc = ibm_is_vpc. attestation_vpc . id
190+ tags = local. tags
191+ zone = " ${ var . region } -${ var . zone } "
192+
193+ # the user data field carries the encrypted contract, so all information visible at the hypervisor layer is encrypted
194+ user_data = hpcr_contract_encrypted. contract . rendered
195+
196+ primary_network_interface {
197+ name = " eth0"
198+ subnet = ibm_is_subnet. attestation_subnet . id
199+ security_groups = [ibm_is_security_group . attestation_security_group . id ]
200+ }
201+
202+ }
203+
204+
205+ # huhh, this is not nice
206+ resource "time_sleep" "wait_for_attestation" {
207+ depends_on = [
208+ ibm_is_instance . attestation_vsi
209+ ]
210+
211+ create_duration = " 45s"
212+ }
213+
214+ data "ibm_cos_bucket_object" "attestation_record" {
215+ bucket_crn = ibm_cos_bucket. attestation_cos_bucket . crn
216+ bucket_location = ibm_cos_bucket. attestation_cos_bucket . region_location
217+ key = local. attestationKey
218+ endpoint_type = " public"
219+ depends_on = [
220+ time_sleep . wait_for_attestation
221+ ]
222+ }
223+
224+ data "hpcr_attestation" "attestation_decoded" {
225+ attestation = data. ibm_cos_bucket_object . attestation_record . body
226+ privkey = tls_private_key. attestation_enc_rsa_key . private_key_pem
227+ }
228+
229+ resource "local_file" "contract" {
230+ content = hpcr_contract_encrypted. contract . rendered
231+ filename = " ${ path . module } /build/contract.yml"
232+ }
233+
234+ resource "local_file" "attestation_pub_key" {
235+ content = tls_private_key. attestation_enc_rsa_key . public_key_pem
236+ filename = " ${ path . module } /build/attestation.pub"
237+ }
238+
239+ resource "local_file" "attestation_priv_key" {
240+ content = tls_private_key. attestation_enc_rsa_key . private_key_pem_pkcs8
241+ filename = " ${ path . module } /build/attestation"
242+ }
243+
244+ resource "local_file" "attestation_record" {
245+ content = jsonencode (data. hpcr_attestation . attestation_decoded . checksums )
246+ filename = " ${ path . module } /build/se-checksums.json"
247+ }
0 commit comments