Kursus
Saya telah melihat tak terhitung banyaknya image Docker yang tanpa sengaja terekspos karena API key, kata sandi basis data, dan token autentikasi di-hardcode ke dalam layer-nya. Pelanggaran keamanan ini sering terjadi saat proses build ketika pengembang membutuhkan akses sementara ke sumber daya privat tetapi tanpa sengaja memanggang kredensial ke dalam image final.
Tantangannya adalah pendekatan tradisional untuk menangani kredensial saat build, seperti variabel lingkungan atau argumen build, tidak dirancang untuk penggunaan yang bersifat sementara. Pendekatan ini meninggalkan jejak permanen di metadata atau layer image, menciptakan kerentanan keamanan yang bertahan lama setelah build selesai. Ini menimbulkan dilema: bagaimana cara melakukan autentikasi ke sumber daya privat saat build tanpa mengorbankan keamanan?
Docker build secrets memecahkan tantangan keamanan krusial ini dengan menyediakan mekanisme untuk menggunakan data sensitif selama build image tanpa meninggalkan jejak pada artefak final.
Dalam tutorial ini, saya akan memandu Anda menerapkan build secrets secara efektif, dari konsep dasar hingga integrasi CI/CD tingkat lanjut, agar build kontainer Anda tetap aman.
Jika Anda baru mengenal Docker, saya sarankan mengikuti salah satu kursus kami, seperti Introduction to Docker, Containerization and Virtualization with Docker and Kubernetes, atau Intermediate Docker.
Apa Itu Docker Build Secrets?
Mari mulai dengan memahami apa yang membedakan build secrets dari pendekatan manajemen kredensial lainnya. Docker build secrets adalah potongan informasi sensitif yang bersifat sementara, tersedia selama proses build tetapi tidak pernah disimpan di layer atau sistem berkas image. Secret hanya ada di memori selama langkah build tertentu dan menghilang setelah langkah tersebut selesai.
Kebutuhan akan build secrets berasal dari alur kerja pengembangan aplikasi modern.
Sering kali Anda perlu melakukan autentikasi ke repositori paket privat, meng-clone repositori Git privat, mengunduh perangkat lunak berlisensi, atau mengakses API internal selama build.
Tanpa build secrets, pengembang menggunakan jalan pintas berbahaya, seperti meng-hardcode kredensial di Dockerfile, mengoper secret sebagai argumen build (yang bertahan di metadata image), atau membuat base image yang terlalu permisif dengan kredensial tertanam.
Risiko penanganan secret yang tidak memadai sangat besar. Kredensial yang terekspos dapat menyebabkan akses tidak sah ke sistem produksi, pelanggaran data, pelanggaran kepatuhan, dan kerusakan reputasi yang signifikan. Skenario umum kebocoran secret meliputi API key untuk pengelola paket, kunci SSH untuk operasi Git, token autentikasi untuk layanan internal, dan kredensial basis data untuk migrasi saat build.
Docker BuildKit: Fondasi Build Secrets yang Aman
Sebelum masuk ke implementasi, Anda perlu memahami BuildKit, mesin build modern yang memungkinkan secret yang aman. BuildKit adalah builder generasi berikutnya dari Docker yang menggantikan mesin build klasik, menawarkan peningkatan signifikan dalam performa, caching, dan keamanan.
BuildKit menggunakan model eksekusi berbasis graf yang melacak dependensi antar langkah build. Arsitektur ini memungkinkan BuildKit untuk me-mount secret sebagai sistem berkas sementara yang hanya ada selama instruksi RUN tertentu, memastikan secret tidak pernah menjadi bagian dari layer image.
Berbeda dengan build Docker klasik, di mana segala sesuatu dalam konteks build menjadi bagian dari cache build, BuildKit memperlakukan secret sebagai sumber daya khusus yang sepenuhnya melewati cache layer.
Mengaktifkan BuildKit sangat mudah. Untuk Docker 23.0 dan yang lebih baru, BuildKit adalah builder default. Untuk versi sebelumnya, setel variabel lingkungan DOCKER_BUILDKIT=1 sebelum menjalankan perintah build:
export DOCKER_BUILDKIT=1
docker build -t myapp .
Sebagai alternatif, aktifkan secara permanen dengan mengonfigurasi daemon Docker atau menggunakan Docker Buildx, yang selalu menggunakan BuildKit. Perbedaan kunci antara build klasik dan build yang diaktifkan BuildKit adalah bahwa secret pada build klasik akan bertahan di layer intermediate jika tidak dikelola dengan hati-hati, sementara BuildKit menjamin secret bersifat sementara dan tidak pernah ditulis ke disk sebagai bagian dari image.
Dengan fondasi aman dari BuildKit, mari jelajahi berbagai jenis secret yang didukung dan cara menerapkannya secara efektif.
Jenis dan Implementasi Docker Build Secrets
BuildKit mendukung tiga mekanisme utama untuk menangani secret selama build, masing-masing dirancang untuk kasus penggunaan tertentu. Memahami kapan menggunakan setiap jenis akan memastikan keamanan dan fungsionalitas yang optimal.
Secret mount: data sensitif serbaguna
Secret mount menyediakan pendekatan paling fleksibel untuk menangani data sensitif selama build. Anda dapat me-mount secret sebagai file di path tertentu selama instruksi RUN, sehingga kredensial tersedia untuk perintah build tanpa menetap di layer.
Prosesnya melibatkan dua langkah: meneruskan secret ke perintah build dan me-mount-nya di Dockerfile. Pertama, buat file secret secara lokal atau gunakan variabel lingkungan. Lalu referensikan saat build:
docker build --secret id=api_key,src=./api_key.txt -t myapp .
Di Dockerfile Anda, mount secret saat instruksi RUN yang membutuhkannya:
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) && \
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/package > /app/data.json
Secara default, secret di-mount pada /run/secrets/<id>, tetapi Anda dapat menentukan target kustom menggunakan parameter target.
Praktik terbaik mencakup hanya me-mount secret pada perintah RUN spesifik yang membutuhkannya, tidak pernah menyalin secret ke filesystem image, dan menggunakan multi-stage build untuk memisahkan tahap yang memerlukan secret dari image final.
Sumbernya bisa berupa path file, atau gunakan env untuk meneruskan secret dari variabel lingkungan: --secret id=token,env=API_TOKEN.
Untuk me-mount secret sebagai variabel lingkungan alih-alih file, gunakan opsi env:
RUN --mount=type=secret,id=db_user,env=DB_USER \
--mount=type=secret,id=db_password,env=DB_PASSWORD \
./setup-database.sh
Anda dapat menggunakan opsi target dan env secara bersamaan untuk me-mount secret sebagai file dan variabel lingkungan sekaligus.
Praktik terbaik mencakup hanya me-mount secret pada perintah RUN spesifik yang membutuhkannya, tidak pernah menyalin secret ke filesystem image, dan menggunakan multi-stage build untuk memisahkan tahap yang memerlukan secret dari image final.
SSH mount: akses aman ke sumber daya privat
SSH mount secara khusus menangani autentikasi berbasis SSH, terutama untuk mengakses repositori Git privat selama build. Alih-alih menyalin kunci SSH ke dalam image (mimpi buruk keamanan), SSH mount memberikan akses sementara ke agen SSH Anda selama waktu build.
Untuk menggunakan SSH mount, pastikan agen SSH Anda berjalan secara lokal dengan kunci yang diperlukan telah dimuat. Lalu referensikan SSH mount di Dockerfile Anda:
RUN --mount=type=ssh \
git clone git@github.com:myorg/private-repo.git /app
Build image dengan SSH forwarding diaktifkan:
docker build --ssh default -t myapp .
Untuk beberapa host dengan kunci berbeda, Anda dapat menentukan SSH mount bernama:
RUN --mount=type=ssh,id=github \
git clone git@github.com:myorg/repo.git
Kemudian sediakan kunci spesifik saat build:
docker build --ssh github=$HOME/.ssh/github_key -t myapp .
Anda akan melihat bahwa pendekatan ini menjaga keamanan dengan tidak pernah mengekspos kunci privat di image sekaligus memungkinkan akses terautentikasi ke repositori privat selama proses build.
Autentikasi Git untuk konteks jarak jauh
Ketika build Docker Anda perlu mengakses repositori Git privat, baik sebagai konteks build itu sendiri atau saat mengambil dependensi selama build, Anda memerlukan cara untuk melakukan autentikasi tanpa menanamkan kredensial. Ini sangat umum saat membangun dari URL repositori privat atau menggunakan instruksi ADD untuk mengambil kode privat.
BuildKit menyediakan dua secret pradefinisi untuk autentikasi Git: GIT_AUTH_TOKEN dan GIT_AUTH_HEADER. Ini adalah secret "pre-flight" yang mengautentikasi builder sebelum instruksi Dockerfile mana pun dijalankan, mengamankan pengambilan repositori awal.
Pola paling umum menggunakan GIT_AUTH_TOKEN untuk autentikasi HTTPS berbasis token:
GIT_AUTH_TOKEN=$(cat ~/.github-token) docker build \
--secret id=GIT_AUTH_TOKEN \
https://github.com/myorg/private-repo.git
Ini bekerja mulus dengan instruksi ADD yang mengambil repositori privat:
FROM alpine
ADD https://github.com/myorg/private-configs.git /configs
Untuk lingkungan CI/CD yang bersifat sementara, injeksikan token dari secret store platform Anda:
- name: Build from private repo
env:
GIT_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker build --secret id=GIT_AUTH_TOKEN https://github.com/myorg/app.git
Secret GIT_AUTH_HEADER menyediakan alternatif untuk skema autentikasi kustom ketika autentikasi token standar tidak memadai.
Cara Menggunakan Docker Build Secrets
Setelah membahas jenis-jenis build secret, mari kita lihat cara menerapkannya secara efektif dalam skenario dunia nyata.
Membuat dan menata secret untuk waktu build
Persiapan secret yang tepat sangat krusial untuk keamanan. Simpan secret dalam file di luar konteks build Docker Anda, pastikan ditambahkan ke .gitignore dan .dockerignore, serta gunakan izin file yang ketat (600 atau 400).
Untuk beberapa secret, buat direktori khusus secret:
mkdir -p .secrets
echo "my-api-key" > .secrets/api_key
chmod 600 .secrets/*
Lalu referensikan saat build:
docker build \
--secret id=api_key,src=.secrets/api_key \
--secret id=db_password,src=.secrets/db_password \
-t myapp .
Untuk secret berbasis variabel lingkungan yang umum di CI/CD:
docker build \
--secret id=api_key,env=API_KEY \
--secret id=db_pass,env=DB_PASSWORD \
-t myapp .
Integrasi dengan multi-stage build
Multi-stage build sangat kuat jika dikombinasikan dengan build secret. Gunakan secret di tahap awal untuk mengambil dependensi, lalu salin hanya artefak yang diperlukan ke tahap final:
# Build stage - secrets used here
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) && \
curl -H "Authorization: Bearer $API_KEY" https://api.company.com/data.json -o data.json && \
pip install -r requirements.txt
# Final stage - no secrets present
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /app/data.json ./data.json
COPY . .
CMD ["python", "app.py"]
Ini memastikan secret hanya ada selama tahap builder dan tidak pernah muncul di image final.
Multi-stage build menangani satu secret dengan elegan, tetapi aplikasi dunia nyata sering kali memerlukan kompleksitas lebih.
Mari kita bahas cara mengelola skenario yang melibatkan banyak secret atau kebutuhan build yang rumit
Menangani banyak secret dan skenario kompleks
Build yang kompleks sering kali membutuhkan banyak secret dalam instruksi RUN yang sama. BuildKit mendukung me-mount beberapa secret secara bersamaan:
RUN --mount=type=secret,id=api_key \
--mount=type=secret,id=db_password \
API_KEY=$(cat /run/secrets/api_key) && \
DB_PASS=$(cat /run/secrets/db_password) && \
./configure.sh
Untuk skenario yang memerlukan secret di beberapa perintah, gabungkan perintah dalam satu instruksi RUN untuk meminimalkan mount secret sambil menjaga batasan keamanan.
Praktik Keamanan Terbaik untuk Docker Build Secrets
Menerapkan build secret dengan benar memerlukan kepatuhan terhadap praktik keamanan yang sudah mapan. Berikut pola yang menurut saya paling efektif untuk menjaga keamanan sepanjang proses build.
Mencegah eksposur dan kebocoran secret
Setelah membangun image, verifikasi bahwa secret tidak bocor dengan memeriksa layer image:
docker history myapp:latest
docker save myapp:latest | tar -x
Periksa layer yang diekstrak untuk setiap data sensitif. Selain itu, bedakan antara secret waktu build (sementara, digunakan saat build) dan secret waktu runtime (dibutuhkan saat kontainer berjalan). Jangan pernah menggunakan build secret untuk kredensial runtime. Gunakan Docker secrets, Kubernetes secrets, atau variabel lingkungan yang diinjeksi saat runtime.
Selalu tambahkan file secret ke .gitignore:
# .gitignore
.secrets/
*.key
Dan ke .dockerignore untuk mencegah inklusi tidak sengaja dalam konteks build:
# .dockerignore
.secrets/
*.key
.env
Mengelola secret di lingkungan CI/CD
Platform CI/CD menyediakan manajemen secret native yang terintegrasi mulus dengan Docker build secrets. Di GitHub Actions:
- name: Build Docker image
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
docker build \
--secret id=api_key,env=API_KEY \
-t myapp .
Prinsip utamanya adalah memanfaatkan secret store yang disediakan platform alih-alih menyimpan secret di repositori. Secret CI/CD diinjeksi sebagai variabel lingkungan, yang dapat langsung dikonsumsi BuildKit melalui tipe sumber env.
Izin secret dan kontrol akses
Ketika membangun sebagai pengguna non-root, pastikan file secret memiliki izin baca yang sesuai. Jika Dockerfile Anda menggunakan USER untuk beralih ke pengguna non-root, mount secret di lokasi yang dapat diakses oleh pengguna tersebut:
FROM python:3.11-slim
RUN useradd -m appuser
USER appuser
RUN --mount=type=secret,id=token,uid=1000 \
cat /run/secrets/token > /dev/null
Untuk kepatuhan keamanan yang lebih ketat, terapkan strategi rotasi secret. Gunakan token berumur pendek jika memungkinkan, terapkan rotasi otomatis di pipeline CI/CD, dan pertahankan log audit penggunaan secret.
Pertimbangkan secret dengan tanggal kedaluwarsa dan proses pembaruan otomatis untuk meminimalkan jendela eksposur.
Sejauh ini, kita berfokus pada build satu kontainer, tetapi aplikasi modern sering terdiri dari banyak layanan. Mari lihat bagaimana Docker Compose memperluas kemampuan build secret ke arsitektur multi-layanan.
Mengintegrasikan Build Secrets Dengan Docker Compose
Docker Compose mempermudah pengelolaan build secret di aplikasi multi-layanan. Di docker-compose.yml Anda, definisikan secret di tingkat atas dan referensikan dalam konfigurasi build layanan:
services:
app:
build:
context: .
secrets:
- api_key
- db_password
secrets:
api_key:
file: ./.secrets/api_key
db_password:
environment: DB_PASSWORD
Kemudian di Dockerfile Anda, mount secret seperti biasa:
RUN --mount=type=secret,id=api_key \
--mount=type=secret,id=db_password \
./setup.sh
Build dengan Compose:
docker-compose build
Pola ini berskala baik untuk aplikasi dengan banyak layanan yang memerlukan secret berbeda, mempertahankan manajemen secret terpusat sambil menjaga batasan keamanan antar layanan.
Saat Anda menerapkan build secret di seluruh proyek, Anda pasti akan menemui masalah. Mari bahas tantangan paling umum dan solusinya agar Anda dapat melakukan troubleshooting secara efektif.
Tantangan Umum dan Troubleshooting
Bahkan dengan penerapan yang tepat, Anda akan menemui tantangan. Berikut masalah paling umum dan solusinya.
Kesalahan sintaks dan masalah deklarasi mount
Sintaks --mount bersifat ketat. Kesalahan umum mencakup kurangnya koma antar parameter, nama parameter yang salah, dan tipe mount yang keliru. Sintaks yang benar adalah:
RUN --mount=type=secret,id=secret_name,target=/path \
command
Jika build gagal dengan "secret not found", verifikasi bahwa BuildKit diaktifkan, dan ID di Dockerfile cocok dengan --secret id pada perintah build Anda.
Secret tidak tersedia atau tidak ditemukan
Ketika secret tidak dapat diakses, verifikasi bahwa file sumber ada dan dapat dibaca:
cat .secrets/api_key
docker build --secret id=api_key,src=.secrets/api_key .
Untuk variabel lingkungan, pastikan variabel sudah disetel:
echo $API_KEY
docker build --secret id=api_key,env=API_KEY .
Kebingungan antara variabel lingkungan vs. file
Dengan build secret, secret selalu di-mount sebagai file secara default di /run/secrets/<id>. Untuk menggunakannya sebagai variabel lingkungan, baca isi file:
RUN --mount=type=secret,id=token \
export TOKEN=$(cat /run/secrets/token) && \
curl -H "Authorization: Bearer $TOKEN" https://api.example.com
Jangan pernah menggunakan argumen build (ARG) untuk secret, karena akan bertahan di metadata image.
Masalah persistensi dan layer caching
Secret tidak bertahan antar instruksi RUN sesuai desain. Jika beberapa perintah RUN membutuhkan secret yang sama, gabungkan perintah atau mount ulang secret:
RUN --mount=type=secret,id=token \
command1
RUN --mount=type=secret,id=token \
command2
BuildKit memastikan secret tidak pernah masuk ke cache layer.
Setelah menguasai dasar-dasar dan troubleshooting umum, Anda mungkin menemui skenario yang memerlukan pendekatan lebih canggih. Mari jelajahi kasus penggunaan lanjutan dan edge case yang melampaui implementasi standar.
Penggunaan Lanjutan Docker Build Secrets
Untuk skenario penerapan yang kompleks, Anda perlu menangani pertimbangan tambahan di luar implementasi dasar.
Secret dalam pipeline CI/CD yang kompleks
Pipeline CI/CD multi-tahap sering memerlukan secret di berbagai tahap. Terapkan secret khusus tahap alih-alih berbagi kredensial. Gunakan fitur platform CI/CD untuk pembatasan ruang lingkup secret, membatasi tahap pipeline mana yang dapat mengakses secret tertentu. Otomatiskan pembersihan secret setelah build untuk meminimalkan jendela eksposur.
Secret dengan builder non-Docker
Builder alternatif seperti Buildah dan Kaniko memiliki dukungan yang bervariasi untuk sintaks secret BuildKit. Buildah mendukung mounting secret serupa melalui --secret flags. Kaniko, yang dirancang untuk lingkungan Kubernetes, menggunakan mekanisme berbeda, biasanya Kubernetes secret yang di-mount sebagai volume. Saat menggunakan builder non-Docker, lihat dokumentasi spesifik mereka, karena implementasinya sangat berbeda.
Secret dan kepatuhan keamanan
Build secret selaras dengan kerangka kepatuhan keamanan dengan menyediakan akses kredensial yang dapat diaudit dan bersifat sementara.
Untuk kepatuhan GDPR, pastikan secret tidak membocorkan informasi pribadi ke dalam layer. Persyaratan SOC2 diuntungkan dari pembuatan jejak audit secret—catat kapan secret digunakan dan oleh siapa. Pertahankan catatan akses secret untuk audit kepatuhan.
Merotasi dan memperbarui secret
Tetapkan proses untuk rotasi secret tanpa mengganggu build. Gunakan file secret versi atau konvensi penamaan variabel lingkungan yang memungkinkan pergantian antar versi.
Terapkan rotasi otomatis di pipeline CI/CD di mana secret diperbarui dari store pusat. Untuk token dengan tanggal kedaluwarsa, pantau kedaluwarsa dan otomatisasi proses pembaruan.
Kesimpulan: Praktik Terbaik dan Rekomendasi
Docker build secrets mewakili peningkatan keamanan mendasar untuk pengembangan aplikasi terkontainerisasi. Sepanjang tutorial ini, kita telah membahas cara menerapkan secret secara aman, dari secret mount dasar hingga integrasi CI/CD yang kompleks.
Inti yang perlu diingat sederhana: selalu gunakan mekanisme mounting secret BuildKit alih-alih argumen build atau kredensial yang di-hardcode, terapkan multi-stage build untuk mengisolasi tahap yang menggunakan secret dari image final, manfaatkan manajemen secret platform CI/CD untuk alur kerja otomatis, dan audit image secara berkala untuk memverifikasi bahwa secret tidak bocor ke layer.
Bagi organisasi yang mengadopsi praktik ini, mulailah dengan kebijakan manajemen secret yang mendefinisikan secret mana yang memerlukan akses saat build, tetapkan jadwal rotasi otomatis, terapkan pengujian komprehensif untuk memverifikasi secret tidak bertahan di image, dan latih tim pengembangan tentang pola penanganan secret yang tepat. Dengan memperlakukan build secret sebagai komponen keamanan kritis, Anda akan secara signifikan mengurangi permukaan serangan dan membangun aplikasi terkontainerisasi yang lebih aman.
Untuk terus belajar, saya sarankan meninjau sumber daya berikut:
Docker Build Secrets FAQs
Apa itu Docker build secrets?
Docker build secrets adalah potongan informasi sensitif yang bersifat sementara, tersedia selama proses build tetapi tidak pernah disimpan di layer image, hanya ada di memori selama langkah build tertentu.
Jenis build secret apa yang didukung Docker?
Docker mendukung tiga jenis: secret mount untuk data sensitif serbaguna, SSH mount untuk akses aman ke repositori Git, dan secret autentikasi Git (GIT_AUTH_TOKEN dan GIT_AUTH_HEADER) untuk konteks jarak jauh privat.
Bagaimana cara mencegah secret muncul di image Docker saya?
Gunakan sintaks --mount=type=secret milik BuildKit pada instruksi RUN yang dipadukan dengan multi-stage build, jangan pernah gunakan argumen build atau variabel lingkungan untuk secret, dan audit image secara berkala menggunakan docker history untuk memverifikasi tidak ada kebocoran.
Bisakah saya menggunakan Docker build secrets dengan Docker Compose?
Ya, Docker Compose mendukung build secret melalui bagian secrets di file compose Anda, memungkinkan Anda mendefinisikan secret di tingkat atas dan mereferensikannya dalam konfigurasi build layanan.
Bagaimana cara kerja build secret dalam pipeline CI/CD?
Platform CI/CD menginjeksi secret sebagai variabel lingkungan yang dapat diteruskan ke build Docker menggunakan --secret id=name,env=VARIABLE_NAME, sehingga memungkinkan alur kerja otomatis tanpa menyimpan kredensial di repositori.
Sebagai Founder Martin Data Solutions serta Data Scientist, ML, dan AI Engineer lepas, saya memiliki portofolio beragam dalam Regression, Classification, NLP, LLM, RAG, Neural Networks, Ensemble Methods, dan Computer Vision.
- Berhasil mengembangkan sejumlah proyek ML end-to-end, termasuk pembersihan data, analitik, pemodelan, dan deployment di AWS dan GCP, menghadirkan solusi yang berdampak dan dapat diskalakan.
- Membangun aplikasi web yang interaktif dan skalabel menggunakan Streamlit dan Gradio untuk berbagai use case industri.
- Mengajar dan membimbing mahasiswa dalam data science dan analitik, mendorong perkembangan profesional mereka melalui pendekatan pembelajaran yang dipersonalisasi.
- Merancang konten kursus untuk aplikasi retrieval-augmented generation (RAG) yang disesuaikan dengan kebutuhan enterprise.
- Menulis blog teknis AI & ML berdampak tinggi, mencakup topik seperti MLOps, vector database, dan LLM, dengan capaian keterlibatan yang signifikan.
Dalam setiap proyek yang saya tangani, saya memastikan menerapkan praktik terkini dalam rekayasa perangkat lunak dan DevOps, seperti CI/CD, code linting, formatting, pemantauan model, pelacakan eksperimen, dan penanganan error yang tangguh. Saya berkomitmen menyajikan solusi menyeluruh, mengubah wawasan data menjadi strategi praktis yang membantu bisnis bertumbuh dan memaksimalkan pemanfaatan data science, machine learning, dan AI.

