如何建立一個安全的Docker基礎映象
背景
我最初使用Docker的時候,每個人都在說它用起來有多簡單方便,它內部的機制是有多麼好,它為我們節省了多少時間。但是當我一使用它就發現,幾乎所有映象都是臃腫而且不安全的(沒有使用包簽名,盲目相信上游的映象庫以curl | sh
的方式安裝),而且也沒有一個映象能實現Docker的初衷:隔離,單程序,容易分發,簡潔。
Docker映象本來不是為了取代複雜的虛擬機器而設計的,後者有完整的日誌、監控、警報和資源管理模組。而Docker則傾向於利用核心的cgroups
和namespaces
特性進行封裝組合,這就好像:
在物理機器環境下,一旦核心完成了初始化,
init
程序就起來了。
這也是為什麼當你在Dockerfile的CMD
指令啟動的程序PID是1,這是與Unix中的程序機制類似的。
現在請檢視一下你的程序列表,使用top
或者ps
,你會看到init
程序佔用的也是這個PID,這是每個類Unix系統的核心程序,所有程序的父程序,一旦你理解這個概念:在類Unix系統上每個程序都是init程序的子程序,你會理解Docker容器裡不應該有無關的修飾檔案,它應該是剛好滿足程序執行需要。
如何開始
現在的應用多數是大型複雜的系統,通常都需要很多依賴庫,例如有排程,編譯和很多其他相關工具類應用,它們的架構通常封裝性良好,通過一層層的抽象和介面把底層細節隱藏了,從某種程度上說,這也算是一種容器,但是從系統架構視角看,我們需要一種比以往虛擬環境更簡單的方案了。
以Java為例
從零開始,思考你要構建一個最通用的基礎容器,想想你的應用本身,它執行需要什麼?
可能性有很多,如果你要執行Java應用,它需要Java執行時;如果執行Rails應用,它需要Ruby直譯器,對Python應用也一樣。Go和其他一些編譯型語言有些許不同,我以下會提到。
在Java例子中,下一步要想的是:JRE需要什麼依賴才能執行?因為它是讓應用能執行的最重要的元件,所以很自然的下一步就是要想清楚JRE執行依賴於什麼。
而實際上JRE並沒太多依賴,它本來就是作為作業系統的抽象層,使程式碼不依賴於宿主系統執行,因此安裝好JRE就基本準備就緒了。
(實際上,對作業系統的獨立性並不是理所當然的事,有非常多的系統特有API和專有的系統擴充套件,但是便於舉例,我們把注意力放在簡單的情況下)
在Linux上,JVM主要是呼叫系統的C語言庫,Oracle的官方JRE,使用的是libc,也就是glibc,這意味著你要執行任何Java程式,都需要先裝好glibc。另外你可能需要某種shell來管理環境,還有一個與外部通訊的介面,例如網路和資源的介面。
我們總結一下Java應用示例需要的最低配置是:
- JRE,在例子中我們使用Oracle JRE
- glibc,JRE的依賴
- 一個基礎環境(包含網路、記憶體、檔案系統等資源管理工具)
走進Alpine Linux
Alpine Linux最近得到很多關注,主要是因為它打包了一系列的經過驗籤的可信任的依賴,並且還保持體積在2MB!而在本文釋出時,其他的一些映象分發版如下:
- ubuntu:latest: 66MB (已經瘦身了非常多了,以前有些版本超過600MB)
- debian:latest: 55MB (同上,一開始是200MB以上的)
- arch:latest: 145MB
- busybox:latest: 676KB (是的!KB,我稍後會討論它)
- alpine:latest: 2MB (2MB,包含一個包管理工具的Linux系統)
Busybox是最小的競爭者?
從上邊的對比中你可以看到,在體積上唯一能打敗Alpine Linux的是Busybox,所以現在幾乎所有嵌入式系統都是使用它,它被應用在路由器,交換機,ATM,或者你的吐司機上。它作為一個最最基礎的環境,但是又提供了足夠容易維護的shell介面。
在網上有很多文章解釋了為什麼人們會選擇Alpine Linux而不是Busybox,我在這總結一下:
- 開放活躍的軟體包倉庫:Alpine Linux使用apk包管理工具,它整合在Docker映象中,而Busybox你需要另外安裝一個包管理器,例如opkg,更甚者,你需要尋找一個穩定的包倉庫源(這幾乎沒有),Alpine的包倉庫中提供了大量常用的依賴包,例如,如果你仍然需要在容器中編譯NodeJS或Ruby之類的程式碼,你可以直接執行apk來新增nodejs和ruby,這在幾秒內便可以完成。
- 體積確實重要,但是當你在功能性,靈活性,易用性和1.5MB之間衡量,體積就不那麼重要了,Alpine上新增的包使這些方面都大大增強了。
- 廣泛的支援:Docker公司已經聘請了Alpine Linux的作者來維護它,所有官方映象,在以後都將基於Alpine Linux來構建。沒有比這個更有說服力的理由去讓你在自己的容器中使用它了吧。
- 希雲cSphere很早就意識到映象越來越龐大的問題,因此在去年推出微映象,也是引導大家如何更好地構建和理解映象,映象只是一種軟體包格式而已。
構建一個Java環境基映象
正如我剛解釋的,Alpine Linux是一個構建自有映象時不錯的選擇,因此,我們在此將使用它來構建簡潔高效的Docker映象,我們開始吧!
組合:Alpine + bash
每個Dockerfile第一個指令都是指定它的父級容器,通常是用於繼承,在我們的例子中是alpine:latest
:
FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>
我們同時聲明瞭誰為這個映象負責,這個資訊對上傳到Docker Hub的映象是必要的。
就這樣,你就有了往下操作的基礎,接下來安裝我們選好的shell,把下邊的命令加上:
RUN apk add --no-cache --update-cache bash
CMD ["/bin/bash"]
最終的Dockerfile是這樣:
FROM alpine:latest
MAINTAINER cSphere <[email protected].cn>
RUN apk add --no-cache --update-cache bash
CMD ["/bin/bash"]
好了,現在我們構建容器:
$ docker build -t my-java-base-image .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM alpine:latest
---> 2314ad3eeb90
Step 2 : MAINTAINER cSphere <[email protected]>
---> Running in 63433312d77e
---> bfe94713797a
Removing intermediate container 63433312d77e
... 省略若干行
Step 4 : CMD /bin/bash
---> Running in d2291684b797
---> ecc443d68f27
Removing intermediate container d2291684b797
Successfully built ecc443d68f27
並且執行它:
$ docker run --rm -ti my-java-base-image
bash-4.3#
成功了!我們有了一個執行著bash的Alpine Linux。
glibc and friends
前邊提到,Oracle的JRE依賴於glibc,Alpine Linux上並沒有glibc,它使用一個更小體積的替代版,叫musl libc。glibc發展了這麼多年,幾乎包含了所有C語言中需要的依賴包,顯然這樣會很不靈活,一個glibc庫被編譯進Alpine Linux,勉強能維持在5MB的體積,而它的替代者musl-libc是一個二進位制檔案,只有897KB,並且支援了所有Linux架構上的C依賴。
對Oracle的JRE,沒有辦法不把glibc加上,幸運的是,Andy Shinn已經做過了這些,他提供了一個預編譯的glibc映象給Alpine Linux,在Github上的alpine-pkg-glibc,最新版是2.23-r1。
這樣把這相關依賴加到Dockerfile中:
ENV GLIBC_PKG_VERSION=2.23-r1
RUN apk add --no-cache --update-cache curl ca-certificates bash && \
curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && \
現在我們的Dockerfile看起來是這樣:
FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>
ENV GLIBC_PKG_VERSION=2.23-r1
RUN apk add --no-cache --update-cache curl ca-certificates bash && \
curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk
CMD ["/bin/bash"]
我們一句句解釋一下這些指令:
ENV GLIBC_PKG_VERSION=2.23-r1
我們通過變數指定GitHub上的glibc版本,所以每當一個新版本釋出,都不需要更改URL,而直接更改這個變數即可。
RUN apk add --update-cache curl ca-certificates bash && \
這個指令會使用apk命令安裝我們需要的包,包括curl和ca-certificates(以便使用TLS的頁面),最後的bash是我們Dockerfile上個版本已經有的了。
curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
這些命令會接著剛剛的RUN指令,它們會從GitHub下載相關公鑰和依賴包。
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk
所有包下載完成後,我們會用這一行命令安裝全部,由於我們之前添加了公鑰,所以它們的簽名會被驗證。
好了!我們現在有了一個能執行幾乎全部依賴於glibc包的環境。
Java執行環境
一般來說,Oracle不提供軟體倉庫的形式讓人們下載,但是人們總是會找到一些方法繞過它,你可以使用以下命令把JRE新增到Docker映象中:
ENV JAVA_VERSION_MAJOR=8 \
JAVA_VERSION_MINOR=73 \
JAVA_VERSION_BUILD=02 \
JAVA_PACKAGE=server-jre
WORKDIR /tmp
RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
"http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \
apk del curl ca-certificates && \
mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && \
rm /jre/bin/jjs && \
rm /jre/bin/keytool && \
rm /jre/bin/orbd && \
rm /jre/bin/pack200 && \
rm /jre/bin/policytool && \
rm /jre/bin/rmid && \
rm /jre/bin/rmiregistry && \
rm /jre/bin/servertool && \
rm /jre/bin/tnameserv && \
rm /jre/bin/unpack200 && \
rm /jre/lib/ext/nashorn.jar && \
rm /jre/lib/jfr.jar && \
rm -rf /jre/lib/jfr && \
rm -rf /jre/lib/oblique-fonts && \
rm -rf /tmp/* /var/cache/apk/* && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
ENV JAVA_HOME /jre
ENV PATH ${PATH}:${JAVA_HOME}/bin
這堆命令究竟做了什麼,我們還是一句句來看一下吧:
ENV JAVA_VERSION_MAJOR=8 \
JAVA_VERSION_MINOR=73 \
JAVA_VERSION_BUILD=02
JAVA_PACKAGE=server-jre
WORKDIR /tmp
這句非常簡單,它定義了我們要從Oracle伺服器上要下載的軟體版本,本文編寫時,上邊的版本號是最新的,以後可能會變化,你可以從Oracle官網上檢視。它同時也指定了WORKDIR
工作目錄,我們需要從一個臨時目錄開始執行,所以這裡設定了/tmp。
RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
"http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \
這句稍微有點複雜,它使用curl傳了一個指定的頭資訊(“Cookie: oraclelicense=accept-securebackup-cookie”),以從Oracle上獲取真正的下載包,這是必須的,不然會返回一個錯誤頁。然後它會把下載好的包通過管道傳給gunzip和tar ,換言之,它並不會儲存下載回來的tar包,而是直接解壓出來到磁碟上。
apk del curl ca-certificates && \
這時curl和ca-certificates兩個包都完成了它們的使命,可以刪除了它們以節省空間。
rm /jre/bin/jjs && \
rm /jre/bin/keytool && \
rm /jre/bin/orbd && \
rm /jre/bin/pack200 && \
rm /jre/bin/policytool && \
rm /jre/bin/rmid && \
rm /jre/bin/rmiregistry && \
rm /jre/bin/servertool && \
rm /jre/bin/tnameserv && \
rm /jre/bin/unpack200 && \
rm /jre/lib/ext/nashorn.jar && \
rm /jre/lib/jfr.jar && \
rm -rf /jre/lib/jfr && \
rm -rf /jre/lib/oblique-fonts && \
rm -rf /tmp/* /var/cache/apk/* && \
JRE自帶了一些工具包,可能永遠都不會用到的,我們也將它們刪掉。 最後一行,會把全部臨時檔案和apk的包快取也清理了。
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
這一行中,我們修改了nsswitch.conf,以確保網路正常,這會被glibc等包所用到。
最後,我們的Dockerfile會是下邊這樣:
FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>
ENV JAVA_VERSION_MAJOR=8 \
JAVA_VERSION_MINOR=73 \
JAVA_VERSION_BUILD=02 \
JAVA_PACKAGE=server-jre \
GLIBC_PKG_VERSION=2.23-r1 \
LANG=en_US.UTF8
WORKDIR /tmp
RUN apk add --no-cache --update-cache curl ca-certificates bash && \
curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && \
curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
"http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \
apk del curl ca-certificates && \
mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && \
rm /jre/bin/jjs && \
rm /jre/bin/keytool && \
rm /jre/bin/orbd && \
rm /jre/bin/pack200 && \
rm /jre/bin/policytool && \
rm /jre/bin/rmid && \
rm /jre/bin/rmiregistry && \
rm /jre/bin/servertool && \
rm /jre/bin/tnameserv && \
rm /jre/bin/unpack200 && \
rm /jre/lib/ext/nashorn.jar && \
rm /jre/lib/jfr.jar && \
rm -rf /jre/lib/jfr && \
rm -rf /jre/lib/oblique-fonts && \
rm -rf /tmp/* /var/cache/apk/* && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
ENV JAVA_HOME=/jre
ENV PATH=${PATH}:${JAVA_HOME}/bin
注意這裡,我整合了兩個ENV和RUN指令,因為最好是用更少的中間層,特別是這個容器是作為通用的構建單元。
簡單來說,有一個規則:你需要更大的靈活性,那你需要更多的層;如果你需要減小體積和降低複雜度,你需要更少的層。這完全取決於你的需求。
在頂部我還加上了這句:
ENV LANG=en_US.UTF-8
這句是為了確保執行在這個系統環境的應用能指定語言。你可以根據需要設定這個LANG環境變數。
另外,JAVA_HOME和PATH也要設定好,以使用剛剛裝好的JRE。
CMD指令會怎麼執行?
我之前提到,我們這是在構建一個能提供給其他服務作為基礎的映象,它不需要帶上CMD指令,因為它永遠不會執行,但是一旦一個服務關聯上它,就需要用到了。
不過你還是可以通過其他方式啟動這個容器,例如docker run
或docker exec
指令:
$ docker run --rm -ti my-java-base-image /bin/bash
構建最終映象
最後,我們終於到了構建映象這步了:
$ docker build -t my-java-base-image .
Sending build context to Docker daemon 60.42 kB
Step 1 : FROM alpine:latest
---> 2314ad3eeb90
Step 2 : MAINTAINER cSphere <[email protected]>
---> Using cache
---> 93cc2bc0bd60
Step 3 : ENV JAVA_VERSION_MAJOR 8 JAVA_VERSION_MINOR 73 JAVA_VERSION_BUILD 02 JAVA_PACKAGE server-jre GLIBC_PKG_VERSION 2.23-r1 LANG en_US.UTF8
---> Running in 3f0ffeaeca78
---> 1dcfd34b0f1a
Removing intermediate container 3f0ffeaeca78
... 省略若干行
Removing intermediate container 0a98b36a6e37
Step 7 : ENV PATH ${PATH}:${JAVA_HOME}/bin
---> Running in 54d0dfb04f98
---> 493399ac9ca6
Removing intermediate container 54d0dfb04f98
Successfully built 493399ac9ca6
哈哈!它執行成功了。我們執行容器裡的java來驗證一下吧:
$ docker run --rm -ti my-java-base-image java -version
java version "1.8.0_73"
Java(TM) SE Runtime Environment (build 1.8.0_73-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)
太好了,這正是我們要看到的結果,我們已經有了一個獨立的Oracle JRE環境,以後我們只需要基於這個映象來構建應用映象即可:
FROM my-java-base-image
[...]
最終映象有多大?
我們來看看:
$ docker images | grep my-java-base-image | awk '{print $7,$8}'
130.4 MB
說實話,這還是挺大的,但是畢竟裡邊裝的是Java嘛~
總結
我們現在構建了一個安全、輕量的Docker映象,基本上可以執行任何Java應用在上面,當然你也可以根據實際情況調整這個Dockerfile,但是主要的思想還是像上邊說的那樣,減小體積,使用安全的軟體源。
一旦你明白Docker容器只是一個基礎的單程序容器,只是一個應用執行的環境,它能讓你專注於應用的構建而不是其他雜七雜八的依賴關係,你就會把Docker應用到得心應手。
以下是簡單的幾點指引:
- 在每個容器中執行一個程序,如果你需要多個程序,那就構建多個容器,並且使用如docker-compose之類的工具去組合這些元件。
- 從一個非常小的映象開始構建。你不需要整個Debian或者Ubuntu映象,特別是當你使用的是編譯型語言(例如 C / C++ / Golang)。幾乎所有的應用加上Alpine Linux就足夠了。
- 高效地使用層:新增更多的檔案層會便於打標籤和除錯,但是這樣會使映象體積膨脹。你需要平衡這兩點。
- 安全性是非常重要的,確保從安全的倉庫拉取映象,從安全的安裝源安裝相關軟體包。(Alpine Linux映象從Docker官方或希雲微映象拉取,JRE從Oracle官方下載)
docker login index.csphere.cn # 賬號在 https://csphere.cn/hub 上獲取
docker pull index.cspehre.cn/microimages/alpine:3.3
docker pull index.csphere.cn/microimages/alpine-glibc:3.3
關於希雲cSphere
希雲cSphere是一個高度整合、功能強大的Docker私有云平臺和類PaaS解決方案,其架構設計借鑑了VMWare vSphere的思想。系統健壯性比肩VMWare這樣的商業產品,產品經過一年多十多個版本的迭代更新,在內部更是經歷了1000次以上的破壞性測試,目前已經在金融、製造、遊戲、安全、電商、教育等多個領域落地。
cSphere的亮點:
- 平臺適應應用,不需要應用適應平臺。
- 多應用架構多場景支援,希雲承諾不論是5年前的應用、現在的甚至5年後的應用架構都可以在希雲上完美支援
- 搬遷現有業務,程式碼、架構無需任何修改
- 希雲產品以自研發為主,拋棄了“拼湊”模式,有力保證了工程質量
- 真正企業級的PaaS,可滿足高複雜專案需求
歡迎聯絡我們:
- 電話 400-686-1560
- 郵箱 [email protected]