Docker镜像简介

 

这篇文章算抛砖引玉,给大家提供一些简单的思路。

首先要做Docker镜像扫描,我们必须要懂Docker镜像是怎么回事。

c1.png

Docker镜像是由文件系统叠加而成。最底层是bootfs,之上的部分为rootfs。

bootfs是docker镜像最底层的引导文件系统,包含bootloader和操作系统内核。

rootfs通常包含一个操作系统运行所需的文件系统。这一层作为基础镜像。

在基础镜像之上,会加入各种镜像,如emacs、apache等。

 

如何分析镜像

对镜像进行分析,无外乎静态分析和动态分析两种方式。而开源的可参考的实现有

专注于静态分析的Clair和容器关联分析与监控的Weave Scope。但Weave Scope似乎跟安全关系不太大,下面笔者会给出一些动态分析的思路。

首先,我们看以下威名远扬的Clair。Clair目前仅支持appc和docker容器的静态分析。

Clair整体架构如下:

c2.png

Clair包含以下核心模块。

获取器(Fetcher)- 从公共源收集漏洞数据

检测器(Detector)- 指出容器镜像中包含的Feature

容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI

通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器

数据库(Databases)- 存储容器中各个层以及漏洞

Worker - 每个Post Layer都会启动一个worker进行Layer Detect

 

编译与使用

 

Clair目前共发布了21个release。我们这里使用第20个release版本,既V2.0.0进行源码剖析。

为了减少在编译过程中的错误,建议使用ubuntu进行编译。并在编译之前,确保git,bzr,rpm,xz等模块已经安装好。Golang版本使用1.8.3以上。并确保已经安装好postgresql,笔者使用的版本为9.5. 建议你也与笔者保持一致。

 

使用go build github.com/coreos/clair/cmd/clair编译clair

使用gobuild github.com/coreos/analyze-local-images 编译analyze-local-images

 

其中Clair作为server端analyze-local-images作为Client端

简单使用如下。通过analyze-local-images分析nginx:latest镜像。

c3.png

c4.png

 

两者交互的整个流程可以简化为:

c5.png

 

Analyze-local-images源码分析

在使用analyze-local-images时,我们可以指定一些参数。

analyze-local-images -endpoint ”http://10.28.182.152:6060

 -my-address ”10.28.182.151″ nginx:latest  

其中,endpoint为clair主机的ip地址。my-address为运行analyze-local-images这个客户端的地址。

postLayerURI是向clair API V1发送数据库的路由,getLayerFeaturesURI是从clair API V1获取漏洞信息的路由。

 

analyze-local-images在主函数调用intMain()函数,而intMain会首先去解析用户的输入参数。例如刚才的endpoint。

 

Analyze-local-images是主要执行流程为:

main()->intMain()->AnalyzeLocalImage()—>analyzeLayer()->getLayer()


func intMain() int {    

    //解析命令行参数,并给刚才定义的一些全局变量赋值。    

    ……    

        //创建一个临时目录    

    tmpPath, err := ioutil.TempDir(“”, ”analyze-local-image-”)    

    //在/tmp目录下创建以analyze-local-image-开头的文件夹。    

 //为了能够清楚的观察/tmp下目录的变化,我们将defer os.RemoveAll(tmpPath)这句注释掉,再重新编译。    

    

    ……    

    //调用AnalyzeLocalImage方法分析镜像    

    go func() {    

   analyzeCh <- AnalyzeLocalImage(imageName, minSeverity, *flagEndpoint, *flagMyAddress, tmpPath)    

    }()    

}  

 

 

镜像被解压到tmp目录下的目录结构如下:

c6.png

analyze-local-images与clair服务端进行交互的两个主要方法为analyzeLayer和getLayer。analyzeLayer向clair发送JSON格式的数据。而getLayer用来获取clair的请求。并将json格式数据解码后格式化输出。

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {    

    //保存镜像到tmp目录下    

    //调用save方法    

    //save方法的原理就是使用docker save 镜像名先将镜像打包成tar文件    

    //然后使用tar命令将文件再解压到tmp文件中。    

    err := save(imageName, tmpPath)    

    …….    

    //调用historyFromManifest方法,读取manifest.json文件获取每一层的id名,保存在layerIDs中。    

    //如果从manifest.json文件中获取不到,则读取历史记录    

        

    layerIDs, err := historyFromManifest(tmpPath)    

    if err != nil {    

    layerIDs, err = historyFromCommand(imageName)    

    }    

    ……    

    //如果clair不在本机,则在analyze-local-images上开启HTTP服务,默认端口为9279    

    ……    

    //分析每一层,既将每一层下的layer.tar文件发送到clair服务端    

    err = analyzeLayer(endpoint, tmpPath+”/”+layerIDs[i]+”/layer.tar”, layerIDs[i], layerIDs[i-1])    

    

    ……    

}    

  

 

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {  

  

    ……  

  

    //获取漏洞信息  

    layer, err := getLayer(endpoint, layerIDs[len(layerIDs)-1])  

    //打印漏洞报告  

    ……  

    for _, feature := range layer.Features {  

        if len(feature.Vulnerabilities) > 0 {  

            for _, vulnerability := range feature.Vulnerabilities {  

                severity := database.Severity(vulnerability.Severity)  

                isSafe = false  

  

                if minSeverity.Compare(severity) > 0 {  

                    continue  

                }  

  

                hasVisibleVulnerabilities = true  

                vulnerabilities = append(vulnerabilities, vulnerabilityInfo{vulnerability, feature, severity})  

            }  

        }  

    }  

    //排序输出报告美化  

    …..  

  

}  

 

至此,对analyze-local-images的源码已经分析完毕。从中可以可以看出。analyze-local-images做的事情很简单。

就是将layer.tar发送给clair。并将clair分析后的结果通过API接口获取到并在本地打印。

 

Clair源码剖析

analyze-local-images 发送layer.tar文件后主要是由/worker.go下的ProcessLayer方法进行处理的。
这里先简单讲下clair的目录结构,我们仅需要重点关注有注释的文件夹。
 

|–api  //api接口

|– cmd//服务端主程序

|–contrib

|–database //数据库相关

|–Documentation

|–ext  //拓展功能

|– pkg//通用方法

|– testdata

`–vendor

 

为了能够深入理解Clair,我们还是要从其main函数开始分析。

 

/cmd/clair/main.go

 

funcmain() {
   // 解析命令行参数,默认从/etc/clair/config.yaml读取数据库配置信息

   ……
   // 加载配置文件
   config, err :=LoadConfig(*flagConfigPath)
   if err != nil {
      log.WithError(err).Fatal(“failedto load configuration”)
   }

   // 初始化日志系统

……

//启动clair
   Boot(config)
}

 

/cmd/clair/main.go

 

funcBoot(config *Config) {
   ……
   // 打开数据库
   db, err :=database.Open(config.Database)
   if err != nil {
      log.Fatal(err)
   }
   defer db.Close()

   // 启动notifier服务
   st.Begin()
   go clair.RunNotifier(config.Notifier,db, st)

   // 启动clair的Rest API 服务
   st.Begin()
   go api.Run(config.API, db, st)
   st.Begin()

//启动clair的健康检测服务
   go api.RunHealth(config.API, db, st)

   // 启动updater服务
   st.Begin()
   go clair.RunUpdater(config.Updater,db, st)

   // Wait for interruption and shutdowngracefully.
   waitForSignals(syscall.SIGINT,syscall.SIGTERM)
   log.Info(“Received interruption,gracefully stopping …”)
   st.Stop()
}

 

Go api.Run执行后,clair会开启Rest服务。

/api/api.go

func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {
   defer st.End()

   //
如果配置为空就不启动服务
   ......
   srv := &graceful.Server{
      Timeout:          0,    // Already handled by our TimeOut middleware
      NoSignalHandling: true, // We want to use our own Stopper
      Server: &http.Server{
         Addr:      ":" + strconv.Itoa(cfg.Port),
         TLSConfig: tlsConfig,
         Handler:   http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),
      },
   }

//
启动HTTP服务  
listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)

   log.Info("main API stopped")
}

 

Api.Run中调用api.newAPIHandler生成一个API Handler来处理所有的API请求。

 

/api/router.go

 

funcnewAPIHandler(cfg *Config, store database.Datastore) http.Handler {
   router := make(router)
   router["/v1"] =v1.NewRouter(store, cfg.PaginationKey)
   return router
}

 

所有的router对应的Handler都在

/api/v1/router.go中:

 funcNewRouter(store database.Datastore, paginationKey string) *httprouter.Router {
   router := httprouter.New()
   ctx := &context{store,paginationKey}

   // Layers
   router.POST(“/layers”,httpHandler(postLayer, ctx))
  router.GET(“/layers/:layerName”, httpHandler(getLayer, ctx))
  router.DELETE(“/layers/:layerName”, httpHandler(deleteLayer,ctx))

   // Namespaces
   router.GET(“/namespaces”,httpHandler(getNamespaces, ctx))

   // Vulnerabilities
  router.GET(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(getVulnerabilities, ctx))
   router.POST(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(postVulnerability, ctx))
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(getVulnerability, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(putVulnerability, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(deleteVulnerability, ctx))

   // Fixes
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes”,httpHandler(getFixes, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(putFix, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(deleteFix, ctx))

   // Notifications
  router.GET(“/notifications/:notificationName”,httpHandler(getNotification, ctx))
  router.DELETE(“/notifications/:notificationName”,httpHandler(deleteNotification, ctx))

   // Metrics
   router.GET(“/metrics”,httpHandler(getMetrics, ctx))

   return router
}

 

而具体的Handler是在/api/v1/routers.go中

例如analyze-local-images 发送的layer.tar文件,最终会交给postLayer方法处理。

 

funcpostLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx*context) (string, int) {
  ……
   err = clair.ProcessLayer(ctx.Store,request.Layer.Format, request.Layer.Name, request.Layer.ParentName,request.Layer.Path, request.Layer.Headers)
   ……
}

 

而ProcessLayer 方法就是在/worker.go中定义的。

 

 

funcProcessLayer(datastore database.Datastore, imageFormat, name, parentName, pathstring, headers map[string]string) error {
  //参数验证

……
   // 检测层是否已经入库

  layer, err := datastore.FindLayer(name, false, false)
   if err != nil && err !=commonerr.ErrNotFound {
      return err
   }

//如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则detectContent对数据进行解析。

  
   // Analyze the content.
   layer.Namespace, layer.Features, err =detectContent(imageFormat, name, path, headers, layer.Parent)
   if err != nil {
      return err
   }

   return datastore.InsertLayer(layer)
}

 

在detectContent方法如下:

func detectContent(imageFormat,name, path string, headers map[string]string, parent *database.Layer)(namespace *database.Namespace, featureVersions []database.FeatureVersion, errerror) {
  ……

//解析namespace
   namespace, err = detectNamespace(name,files, parent)
   if err != nil {
      return
   }

   //解析特征版本

  featureVersions, err = detectFeatureVersions(name, files, namespace,parent)
   if err != nil {
      return
   }
  ……

return
}

 

Docker镜像静态扫描器的简易实现

通过刚才的源码分析,结合analyze-local-images以及clair。我们可以先实现一个简易的Docker静态分析器。对docker镜像逐层分析,实现输出软件特征版本。以便于我们了解clair的工作原理。

 

这里直接给出github链接:

https://github.com/MXi4oyu/DockerXScan/releases/tag/0.1

感兴趣的朋友可以自行下载测试。

这里给出Docker镜像静态扫描器的简易架构。

last.jpg

 

Docker镜像深度分析

(1)Webshell检测

对于webshell检测,我们可以采用三种方式。

方式一:模糊hash

模糊hash算法使用的是:https://ssdeep-project.github.io

我们根据其API实现了Go语言的绑定:gossdeep

主要API函数有两个,一个是Fuzzy_hash_file,一个是Fuzzy_compare。

1.提取文件模糊hash

Fuzzy_hash_file(“/var/www/shell.php”)

2.比较模糊hash

Fuzzy_compare(“3:YD6xL4fYvn:Y2xMwvn”,”3:YD6xL4fYvn:Y2xMwvk”)

方式二:yara规则引擎

根据yara规则库进行检测

Yara(“./libs/php.yar”,”/var/www/”)

方式三:机器学习

机器学习,分类算法:CNN-Text-Classfication

https://github.com/dennybritz/cnn-text-classification-tf/

(2)木马病毒检测

我们知道开源杀毒引擎ClamAV的病毒库非常强大,主要有

 1) 已知的恶意二进制文件的MD5哈希值

 2) PE(Windows 中可执行文件格式)节的MD5哈希值

 3) 十六进制特征码(shellcode)

 4) 存档元数据特征码

 5) 已知的合法文件的白名单数据库

我们可以

将clamav的病毒库转换为yara规则,进行恶意代码识别。也可以利用开源的yara规则,进行木马病毒的检测。

(3)镜像历史分析

(4)动态扫描

通过docker的配置文件,我们可以获取到其暴漏出来的端口。模拟运行后,可以用常规的黑客漏洞扫描进行扫描。

(5)调用监控

利用Docker API检测文件与系统调用

这里先给出一些深度分析的思路,限于篇幅,我们会在以后的文章中做详细介绍。


前期文章

《docker容器的全面安全防护》

《配置一个安全的docker宿主机》

《利用docker插件实现细粒度权限控制》

后续预告

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注我们!!!

Docker镜像简介

 

这篇文章算抛砖引玉,给大家提供一些简单的思路。

首先要做Docker镜像扫描,我们必须要懂Docker镜像是怎么回事。

c1.png

Docker镜像是由文件系统叠加而成。最底层是bootfs,之上的部分为rootfs。

bootfs是docker镜像最底层的引导文件系统,包含bootloader和操作系统内核。

rootfs通常包含一个操作系统运行所需的文件系统。这一层作为基础镜像。

在基础镜像之上,会加入各种镜像,如emacs、apache等。

 

如何分析镜像

对镜像进行分析,无外乎静态分析和动态分析两种方式。而开源的可参考的实现有

专注于静态分析的Clair和容器关联分析与监控的Weave Scope。但Weave Scope似乎跟安全关系不太大,下面笔者会给出一些动态分析的思路。

首先,我们看以下威名远扬的Clair。Clair目前仅支持appc和docker容器的静态分析。

Clair整体架构如下:

c2.png

Clair包含以下核心模块。

获取器(Fetcher)- 从公共源收集漏洞数据

检测器(Detector)- 指出容器镜像中包含的Feature

容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI

通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器

数据库(Databases)- 存储容器中各个层以及漏洞

Worker - 每个Post Layer都会启动一个worker进行Layer Detect

 

编译与使用

 

Clair目前共发布了21个release。我们这里使用第20个release版本,既V2.0.0进行源码剖析。

为了减少在编译过程中的错误,建议使用ubuntu进行编译。并在编译之前,确保git,bzr,rpm,xz等模块已经安装好。Golang版本使用1.8.3以上。并确保已经安装好postgresql,笔者使用的版本为9.5. 建议你也与笔者保持一致。

 

使用go build github.com/coreos/clair/cmd/clair编译clair

使用gobuild github.com/coreos/analyze-local-images 编译analyze-local-images

 

其中Clair作为server端analyze-local-images作为Client端

简单使用如下。通过analyze-local-images分析nginx:latest镜像。

c3.png

c4.png

 

两者交互的整个流程可以简化为:

c5.png

 

Analyze-local-images源码分析

在使用analyze-local-images时,我们可以指定一些参数。

analyze-local-images -endpoint ”http://10.28.182.152:6060

 -my-address ”10.28.182.151″ nginx:latest  

其中,endpoint为clair主机的ip地址。my-address为运行analyze-local-images这个客户端的地址。

postLayerURI是向clair API V1发送数据库的路由,getLayerFeaturesURI是从clair API V1获取漏洞信息的路由。

 

analyze-local-images在主函数调用intMain()函数,而intMain会首先去解析用户的输入参数。例如刚才的endpoint。

 

Analyze-local-images是主要执行流程为:

main()->intMain()->AnalyzeLocalImage()—>analyzeLayer()->getLayer()


func intMain() int {    

    //解析命令行参数,并给刚才定义的一些全局变量赋值。    

    ……    

        //创建一个临时目录    

    tmpPath, err := ioutil.TempDir(“”, ”analyze-local-image-”)    

    //在/tmp目录下创建以analyze-local-image-开头的文件夹。    

 //为了能够清楚的观察/tmp下目录的变化,我们将defer os.RemoveAll(tmpPath)这句注释掉,再重新编译。    

    

    ……    

    //调用AnalyzeLocalImage方法分析镜像    

    go func() {    

   analyzeCh <- AnalyzeLocalImage(imageName, minSeverity, *flagEndpoint, *flagMyAddress, tmpPath)    

    }()    

}  

 

 

镜像被解压到tmp目录下的目录结构如下:

c6.png

analyze-local-images与clair服务端进行交互的两个主要方法为analyzeLayer和getLayer。analyzeLayer向clair发送JSON格式的数据。而getLayer用来获取clair的请求。并将json格式数据解码后格式化输出。

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {    

    //保存镜像到tmp目录下    

    //调用save方法    

    //save方法的原理就是使用docker save 镜像名先将镜像打包成tar文件    

    //然后使用tar命令将文件再解压到tmp文件中。    

    err := save(imageName, tmpPath)    

    …….    

    //调用historyFromManifest方法,读取manifest.json文件获取每一层的id名,保存在layerIDs中。    

    //如果从manifest.json文件中获取不到,则读取历史记录    

        

    layerIDs, err := historyFromManifest(tmpPath)    

    if err != nil {    

    layerIDs, err = historyFromCommand(imageName)    

    }    

    ……    

    //如果clair不在本机,则在analyze-local-images上开启HTTP服务,默认端口为9279    

    ……    

    //分析每一层,既将每一层下的layer.tar文件发送到clair服务端    

    err = analyzeLayer(endpoint, tmpPath+”/”+layerIDs[i]+”/layer.tar”, layerIDs[i], layerIDs[i-1])    

    

    ……    

}    

  

 

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {  

  

    ……  

  

    //获取漏洞信息  

    layer, err := getLayer(endpoint, layerIDs[len(layerIDs)-1])  

    //打印漏洞报告  

    ……  

    for _, feature := range layer.Features {  

        if len(feature.Vulnerabilities) > 0 {  

            for _, vulnerability := range feature.Vulnerabilities {  

                severity := database.Severity(vulnerability.Severity)  

                isSafe = false  

  

                if minSeverity.Compare(severity) > 0 {  

                    continue  

                }  

  

                hasVisibleVulnerabilities = true  

                vulnerabilities = append(vulnerabilities, vulnerabilityInfo{vulnerability, feature, severity})  

            }  

        }  

    }  

    //排序输出报告美化  

    …..  

  

}  

 

至此,对analyze-local-images的源码已经分析完毕。从中可以可以看出。analyze-local-images做的事情很简单。

就是将layer.tar发送给clair。并将clair分析后的结果通过API接口获取到并在本地打印。

 

Clair源码剖析

analyze-local-images 发送layer.tar文件后主要是由/worker.go下的ProcessLayer方法进行处理的。
这里先简单讲下clair的目录结构,我们仅需要重点关注有注释的文件夹。
 

|–api  //api接口

|– cmd//服务端主程序

|–contrib

|–database //数据库相关

|–Documentation

|–ext  //拓展功能

|– pkg//通用方法

|– testdata

`–vendor

 

为了能够深入理解Clair,我们还是要从其main函数开始分析。

 

/cmd/clair/main.go

 

funcmain() {
   // 解析命令行参数,默认从/etc/clair/config.yaml读取数据库配置信息

   ……
   // 加载配置文件
   config, err :=LoadConfig(*flagConfigPath)
   if err != nil {
      log.WithError(err).Fatal(“failedto load configuration”)
   }

   // 初始化日志系统

……

//启动clair
   Boot(config)
}

 

/cmd/clair/main.go

 

funcBoot(config *Config) {
   ……
   // 打开数据库
   db, err :=database.Open(config.Database)
   if err != nil {
      log.Fatal(err)
   }
   defer db.Close()

   // 启动notifier服务
   st.Begin()
   go clair.RunNotifier(config.Notifier,db, st)

   // 启动clair的Rest API 服务
   st.Begin()
   go api.Run(config.API, db, st)
   st.Begin()

//启动clair的健康检测服务
   go api.RunHealth(config.API, db, st)

   // 启动updater服务
   st.Begin()
   go clair.RunUpdater(config.Updater,db, st)

   // Wait for interruption and shutdowngracefully.
   waitForSignals(syscall.SIGINT,syscall.SIGTERM)
   log.Info(“Received interruption,gracefully stopping …”)
   st.Stop()
}

 

Go api.Run执行后,clair会开启Rest服务。

/api/api.go

func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {
   defer st.End()

   //
如果配置为空就不启动服务
   ......
   srv := &graceful.Server{
      Timeout:          0,    // Already handled by our TimeOut middleware
      NoSignalHandling: true, // We want to use our own Stopper
      Server: &http.Server{
         Addr:      ":" + strconv.Itoa(cfg.Port),
         TLSConfig: tlsConfig,
         Handler:   http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),
      },
   }

//
启动HTTP服务  
listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)

   log.Info("main API stopped")
}

 

Api.Run中调用api.newAPIHandler生成一个API Handler来处理所有的API请求。

 

/api/router.go

 

funcnewAPIHandler(cfg *Config, store database.Datastore) http.Handler {
   router := make(router)
   router["/v1"] =v1.NewRouter(store, cfg.PaginationKey)
   return router
}

 

所有的router对应的Handler都在

/api/v1/router.go中:

 funcNewRouter(store database.Datastore, paginationKey string) *httprouter.Router {
   router := httprouter.New()
   ctx := &context{store,paginationKey}

   // Layers
   router.POST(“/layers”,httpHandler(postLayer, ctx))
  router.GET(“/layers/:layerName”, httpHandler(getLayer, ctx))
  router.DELETE(“/layers/:layerName”, httpHandler(deleteLayer,ctx))

   // Namespaces
   router.GET(“/namespaces”,httpHandler(getNamespaces, ctx))

   // Vulnerabilities
  router.GET(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(getVulnerabilities, ctx))
   router.POST(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(postVulnerability, ctx))
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(getVulnerability, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(putVulnerability, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(deleteVulnerability, ctx))

   // Fixes
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes”,httpHandler(getFixes, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(putFix, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(deleteFix, ctx))

   // Notifications
  router.GET(“/notifications/:notificationName”,httpHandler(getNotification, ctx))
  router.DELETE(“/notifications/:notificationName”,httpHandler(deleteNotification, ctx))

   // Metrics
   router.GET(“/metrics”,httpHandler(getMetrics, ctx))

   return router
}

 

而具体的Handler是在/api/v1/routers.go中

例如analyze-local-images 发送的layer.tar文件,最终会交给postLayer方法处理。

 

funcpostLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx*context) (string, int) {
  ……
   err = clair.ProcessLayer(ctx.Store,request.Layer.Format, request.Layer.Name, request.Layer.ParentName,request.Layer.Path, request.Layer.Headers)
   ……
}

 

而ProcessLayer 方法就是在/worker.go中定义的。

 

 

funcProcessLayer(datastore database.Datastore, imageFormat, name, parentName, pathstring, headers map[string]string) error {
  //参数验证

……
   // 检测层是否已经入库

  layer, err := datastore.FindLayer(name, false, false)
   if err != nil && err !=commonerr.ErrNotFound {
      return err
   }

//如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则detectContent对数据进行解析。

  
   // Analyze the content.
   layer.Namespace, layer.Features, err =detectContent(imageFormat, name, path, headers, layer.Parent)
   if err != nil {
      return err
   }

   return datastore.InsertLayer(layer)
}

 

在detectContent方法如下:

func detectContent(imageFormat,name, path string, headers map[string]string, parent *database.Layer)(namespace *database.Namespace, featureVersions []database.FeatureVersion, errerror) {
  ……

//解析namespace
   namespace, err = detectNamespace(name,files, parent)
   if err != nil {
      return
   }

   //解析特征版本

  featureVersions, err = detectFeatureVersions(name, files, namespace,parent)
   if err != nil {
      return
   }
  ……

return
}

 

Docker镜像静态扫描器的简易实现

通过刚才的源码分析,结合analyze-local-images以及clair。我们可以先实现一个简易的Docker静态分析器。对docker镜像逐层分析,实现输出软件特征版本。以便于我们了解clair的工作原理。

 

这里直接给出github链接:

https://github.com/MXi4oyu/DockerXScan/releases/tag/0.1

感兴趣的朋友可以自行下载测试。

这里给出Docker镜像静态扫描器的简易架构。

last.jpg

 

Docker镜像深度分析

(1)Webshell检测

对于webshell检测,我们可以采用三种方式。

方式一:模糊hash

模糊hash算法使用的是:https://ssdeep-project.github.io

我们根据其API实现了Go语言的绑定:gossdeep

主要API函数有两个,一个是Fuzzy_hash_file,一个是Fuzzy_compare。

1.提取文件模糊hash

Fuzzy_hash_file(“/var/www/shell.php”)

2.比较模糊hash

Fuzzy_compare(“3:YD6xL4fYvn:Y2xMwvn”,”3:YD6xL4fYvn:Y2xMwvk”)

方式二:yara规则引擎

根据yara规则库进行检测

Yara(“./libs/php.yar”,”/var/www/”)

方式三:机器学习

机器学习,分类算法:CNN-Text-Classfication

https://github.com/dennybritz/cnn-text-classification-tf/

(2)木马病毒检测

我们知道开源杀毒引擎ClamAV的病毒库非常强大,主要有

 1) 已知的恶意二进制文件的MD5哈希值

 2) PE(Windows 中可执行文件格式)节的MD5哈希值

 3) 十六进制特征码(shellcode)

 4) 存档元数据特征码

 5) 已知的合法文件的白名单数据库

我们可以

将clamav的病毒库转换为yara规则,进行恶意代码识别。也可以利用开源的yara规则,进行木马病毒的检测。

(3)镜像历史分析

(4)动态扫描

通过docker的配置文件,我们可以获取到其暴漏出来的端口。模拟运行后,可以用常规的黑客漏洞扫描进行扫描。

(5)调用监控

利用Docker API检测文件与系统调用

这里先给出一些深度分析的思路,限于篇幅,我们会在以后的文章中做详细介绍。


前期文章

《docker容器的全面安全防护》

《配置一个安全的docker宿主机》

《利用docker插件实现细粒度权限控制》

后续预告

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注我们!!!

Docker镜像简介

 

这篇文章算抛砖引玉,给大家提供一些简单的思路。

首先要做Docker镜像扫描,我们必须要懂Docker镜像是怎么回事。

c1.png

Docker镜像是由文件系统叠加而成。最底层是bootfs,之上的部分为rootfs。

bootfs是docker镜像最底层的引导文件系统,包含bootloader和操作系统内核。

rootfs通常包含一个操作系统运行所需的文件系统。这一层作为基础镜像。

在基础镜像之上,会加入各种镜像,如emacs、apache等。

 

如何分析镜像

对镜像进行分析,无外乎静态分析和动态分析两种方式。而开源的可参考的实现有

专注于静态分析的Clair和容器关联分析与监控的Weave Scope。但Weave Scope似乎跟安全关系不太大,下面笔者会给出一些动态分析的思路。

首先,我们看以下威名远扬的Clair。Clair目前仅支持appc和docker容器的静态分析。

Clair整体架构如下:

c2.png

Clair包含以下核心模块。

获取器(Fetcher)- 从公共源收集漏洞数据

检测器(Detector)- 指出容器镜像中包含的Feature

容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI

通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器

数据库(Databases)- 存储容器中各个层以及漏洞

Worker - 每个Post Layer都会启动一个worker进行Layer Detect

 

编译与使用

 

Clair目前共发布了21个release。我们这里使用第20个release版本,既V2.0.0进行源码剖析。

为了减少在编译过程中的错误,建议使用ubuntu进行编译。并在编译之前,确保git,bzr,rpm,xz等模块已经安装好。Golang版本使用1.8.3以上。并确保已经安装好postgresql,笔者使用的版本为9.5. 建议你也与笔者保持一致。

 

使用go build github.com/coreos/clair/cmd/clair编译clair

使用gobuild github.com/coreos/analyze-local-images 编译analyze-local-images

 

其中Clair作为server端analyze-local-images作为Client端

简单使用如下。通过analyze-local-images分析nginx:latest镜像。

c3.png

c4.png

 

两者交互的整个流程可以简化为:

c5.png

 

Analyze-local-images源码分析

在使用analyze-local-images时,我们可以指定一些参数。

analyze-local-images -endpoint ”http://10.28.182.152:6060

 -my-address ”10.28.182.151″ nginx:latest  

其中,endpoint为clair主机的ip地址。my-address为运行analyze-local-images这个客户端的地址。

postLayerURI是向clair API V1发送数据库的路由,getLayerFeaturesURI是从clair API V1获取漏洞信息的路由。

 

analyze-local-images在主函数调用intMain()函数,而intMain会首先去解析用户的输入参数。例如刚才的endpoint。

 

Analyze-local-images是主要执行流程为:

main()->intMain()->AnalyzeLocalImage()—>analyzeLayer()->getLayer()


func intMain() int {    

    //解析命令行参数,并给刚才定义的一些全局变量赋值。    

    ……    

        //创建一个临时目录    

    tmpPath, err := ioutil.TempDir(“”, ”analyze-local-image-”)    

    //在/tmp目录下创建以analyze-local-image-开头的文件夹。    

 //为了能够清楚的观察/tmp下目录的变化,我们将defer os.RemoveAll(tmpPath)这句注释掉,再重新编译。    

    

    ……    

    //调用AnalyzeLocalImage方法分析镜像    

    go func() {    

   analyzeCh <- AnalyzeLocalImage(imageName, minSeverity, *flagEndpoint, *flagMyAddress, tmpPath)    

    }()    

}  

 

 

镜像被解压到tmp目录下的目录结构如下:

c6.png

analyze-local-images与clair服务端进行交互的两个主要方法为analyzeLayer和getLayer。analyzeLayer向clair发送JSON格式的数据。而getLayer用来获取clair的请求。并将json格式数据解码后格式化输出。

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {    

    //保存镜像到tmp目录下    

    //调用save方法    

    //save方法的原理就是使用docker save 镜像名先将镜像打包成tar文件    

    //然后使用tar命令将文件再解压到tmp文件中。    

    err := save(imageName, tmpPath)    

    …….    

    //调用historyFromManifest方法,读取manifest.json文件获取每一层的id名,保存在layerIDs中。    

    //如果从manifest.json文件中获取不到,则读取历史记录    

        

    layerIDs, err := historyFromManifest(tmpPath)    

    if err != nil {    

    layerIDs, err = historyFromCommand(imageName)    

    }    

    ……    

    //如果clair不在本机,则在analyze-local-images上开启HTTP服务,默认端口为9279    

    ……    

    //分析每一层,既将每一层下的layer.tar文件发送到clair服务端    

    err = analyzeLayer(endpoint, tmpPath+”/”+layerIDs[i]+”/layer.tar”, layerIDs[i], layerIDs[i-1])    

    

    ……    

}    

  

 

func AnalyzeLocalImage(imageName string, minSeverity database.Severity, endpoint, myAddress, tmpPath string) error {  

  

    ……  

  

    //获取漏洞信息  

    layer, err := getLayer(endpoint, layerIDs[len(layerIDs)-1])  

    //打印漏洞报告  

    ……  

    for _, feature := range layer.Features {  

        if len(feature.Vulnerabilities) > 0 {  

            for _, vulnerability := range feature.Vulnerabilities {  

                severity := database.Severity(vulnerability.Severity)  

                isSafe = false  

  

                if minSeverity.Compare(severity) > 0 {  

                    continue  

                }  

  

                hasVisibleVulnerabilities = true  

                vulnerabilities = append(vulnerabilities, vulnerabilityInfo{vulnerability, feature, severity})  

            }  

        }  

    }  

    //排序输出报告美化  

    …..  

  

}  

 

至此,对analyze-local-images的源码已经分析完毕。从中可以可以看出。analyze-local-images做的事情很简单。

就是将layer.tar发送给clair。并将clair分析后的结果通过API接口获取到并在本地打印。

 

Clair源码剖析

analyze-local-images 发送layer.tar文件后主要是由/worker.go下的ProcessLayer方法进行处理的。
这里先简单讲下clair的目录结构,我们仅需要重点关注有注释的文件夹。
 

|–api  //api接口

|– cmd//服务端主程序

|–contrib

|–database //数据库相关

|–Documentation

|–ext  //拓展功能

|– pkg//通用方法

|– testdata

`–vendor

 

为了能够深入理解Clair,我们还是要从其main函数开始分析。

 

/cmd/clair/main.go

 

funcmain() {
   // 解析命令行参数,默认从/etc/clair/config.yaml读取数据库配置信息

   ……
   // 加载配置文件
   config, err :=LoadConfig(*flagConfigPath)
   if err != nil {
      log.WithError(err).Fatal(“failedto load configuration”)
   }

   // 初始化日志系统

……

//启动clair
   Boot(config)
}

 

/cmd/clair/main.go

 

funcBoot(config *Config) {
   ……
   // 打开数据库
   db, err :=database.Open(config.Database)
   if err != nil {
      log.Fatal(err)
   }
   defer db.Close()

   // 启动notifier服务
   st.Begin()
   go clair.RunNotifier(config.Notifier,db, st)

   // 启动clair的Rest API 服务
   st.Begin()
   go api.Run(config.API, db, st)
   st.Begin()

//启动clair的健康检测服务
   go api.RunHealth(config.API, db, st)

   // 启动updater服务
   st.Begin()
   go clair.RunUpdater(config.Updater,db, st)

   // Wait for interruption and shutdowngracefully.
   waitForSignals(syscall.SIGINT,syscall.SIGTERM)
   log.Info(“Received interruption,gracefully stopping …”)
   st.Stop()
}

 

Go api.Run执行后,clair会开启Rest服务。

/api/api.go

func Run(cfg *Config, store database.Datastore, st *stopper.Stopper) {
   defer st.End()

   //
如果配置为空就不启动服务
   ......
   srv := &graceful.Server{
      Timeout:          0,    // Already handled by our TimeOut middleware
      NoSignalHandling: true, // We want to use our own Stopper
      Server: &http.Server{
         Addr:      ":" + strconv.Itoa(cfg.Port),
         TLSConfig: tlsConfig,
         Handler:   http.TimeoutHandler(newAPIHandler(cfg, store), cfg.Timeout, timeoutResponse),
      },
   }

//
启动HTTP服务  
listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile)

   log.Info("main API stopped")
}

 

Api.Run中调用api.newAPIHandler生成一个API Handler来处理所有的API请求。

 

/api/router.go

 

funcnewAPIHandler(cfg *Config, store database.Datastore) http.Handler {
   router := make(router)
   router["/v1"] =v1.NewRouter(store, cfg.PaginationKey)
   return router
}

 

所有的router对应的Handler都在

/api/v1/router.go中:

 funcNewRouter(store database.Datastore, paginationKey string) *httprouter.Router {
   router := httprouter.New()
   ctx := &context{store,paginationKey}

   // Layers
   router.POST(“/layers”,httpHandler(postLayer, ctx))
  router.GET(“/layers/:layerName”, httpHandler(getLayer, ctx))
  router.DELETE(“/layers/:layerName”, httpHandler(deleteLayer,ctx))

   // Namespaces
   router.GET(“/namespaces”,httpHandler(getNamespaces, ctx))

   // Vulnerabilities
  router.GET(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(getVulnerabilities, ctx))
   router.POST(“/namespaces/:namespaceName/vulnerabilities”,httpHandler(postVulnerability, ctx))
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(getVulnerability, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(putVulnerability, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName”,httpHandler(deleteVulnerability, ctx))

   // Fixes
  router.GET(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes”,httpHandler(getFixes, ctx))
  router.PUT(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(putFix, ctx))
  router.DELETE(“/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName”,httpHandler(deleteFix, ctx))

   // Notifications
  router.GET(“/notifications/:notificationName”,httpHandler(getNotification, ctx))
  router.DELETE(“/notifications/:notificationName”,httpHandler(deleteNotification, ctx))

   // Metrics
   router.GET(“/metrics”,httpHandler(getMetrics, ctx))

   return router
}

 

而具体的Handler是在/api/v1/routers.go中

例如analyze-local-images 发送的layer.tar文件,最终会交给postLayer方法处理。

 

funcpostLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx*context) (string, int) {
  ……
   err = clair.ProcessLayer(ctx.Store,request.Layer.Format, request.Layer.Name, request.Layer.ParentName,request.Layer.Path, request.Layer.Headers)
   ……
}

 

而ProcessLayer 方法就是在/worker.go中定义的。

 

 

funcProcessLayer(datastore database.Datastore, imageFormat, name, parentName, pathstring, headers map[string]string) error {
  //参数验证

……
   // 检测层是否已经入库

  layer, err := datastore.FindLayer(name, false, false)
   if err != nil && err !=commonerr.ErrNotFound {
      return err
   }

//如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则detectContent对数据进行解析。

  
   // Analyze the content.
   layer.Namespace, layer.Features, err =detectContent(imageFormat, name, path, headers, layer.Parent)
   if err != nil {
      return err
   }

   return datastore.InsertLayer(layer)
}

 

在detectContent方法如下:

func detectContent(imageFormat,name, path string, headers map[string]string, parent *database.Layer)(namespace *database.Namespace, featureVersions []database.FeatureVersion, errerror) {
  ……

//解析namespace
   namespace, err = detectNamespace(name,files, parent)
   if err != nil {
      return
   }

   //解析特征版本

  featureVersions, err = detectFeatureVersions(name, files, namespace,parent)
   if err != nil {
      return
   }
  ……

return
}

 

Docker镜像静态扫描器的简易实现

通过刚才的源码分析,结合analyze-local-images以及clair。我们可以先实现一个简易的Docker静态分析器。对docker镜像逐层分析,实现输出软件特征版本。以便于我们了解clair的工作原理。

 

这里直接给出github链接:

https://github.com/MXi4oyu/DockerXScan/releases/tag/0.1

感兴趣的朋友可以自行下载测试。

这里给出Docker镜像静态扫描器的简易架构。

last.jpg

 

Docker镜像深度分析

(1)Webshell检测

对于webshell检测,我们可以采用三种方式。

方式一:模糊hash

模糊hash算法使用的是:https://ssdeep-project.github.io

我们根据其API实现了Go语言的绑定:gossdeep

主要API函数有两个,一个是Fuzzy_hash_file,一个是Fuzzy_compare。

1.提取文件模糊hash

Fuzzy_hash_file(“/var/www/shell.php”)

2.比较模糊hash

Fuzzy_compare(“3:YD6xL4fYvn:Y2xMwvn”,”3:YD6xL4fYvn:Y2xMwvk”)

方式二:yara规则引擎

根据yara规则库进行检测

Yara(“./libs/php.yar”,”/var/www/”)

方式三:机器学习

机器学习,分类算法:CNN-Text-Classfication

https://github.com/dennybritz/cnn-text-classification-tf/

(2)木马病毒检测

我们知道开源杀毒引擎ClamAV的病毒库非常强大,主要有

 1) 已知的恶意二进制文件的MD5哈希值

 2) PE(Windows 中可执行文件格式)节的MD5哈希值

 3) 十六进制特征码(shellcode)

 4) 存档元数据特征码

 5) 已知的合法文件的白名单数据库

我们可以

将clamav的病毒库转换为yara规则,进行恶意代码识别。也可以利用开源的yara规则,进行木马病毒的检测。

(3)镜像历史分析

(4)动态扫描

通过docker的配置文件,我们可以获取到其暴漏出来的端口。模拟运行后,可以用常规的黑客漏洞扫描进行扫描。

(5)调用监控

利用Docker API检测文件与系统调用

这里先给出一些深度分析的思路,限于篇幅,我们会在以后的文章中做详细介绍。


前期文章

《docker容器的全面安全防护》

《配置一个安全的docker宿主机》

《利用docker插件实现细粒度权限控制》

后续预告

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注我们!!!

前言

     我们在实际的docker运行环境下,大都会遇到多用户的情况,为了安全起见,有些用户我们不想给予其全面的docker控制权限,比如不想某些用户执行docker stop 以及docker rm 等危险指令,当然我们可以从系统账号权限来控制,但docker在实际生产环节中大都以集群方式部署,都是通过docker守护进程接口来操作docker,系统控制实现起来略显麻烦,还好docker官方提供了非常方便的扩展插件来实现对用户权限细粒度控制,可细到只允许指定用户执行指定的docker命令,本文详细介绍如何来实现。

Docker 插件

   Docker 引擎允许用户使用第三方插件的形式扩展 Docker 功能。Docker 的插件类型分为以下三种大类:

  • Network plugins 网络插件可以提供容器间互联网络模型。

  • Volume plugins 数据卷插件可以使 Docker 数据卷跨多个主机。

  • Authorization plugins 验证插件可以提供基于权限的访问控制,也是本文主要讲的插件,比较出名的就是 Twistlock AuthZ Broker 。

Twistlock AuthZ Broker

   Twistlock AuthZ Broker 是一个直接运行在主机或容器中的 Dockers 授权插件,基于简单的用户审计策略,可以过滤命令是否允许执行。

源码地址

  https://github.com/deathmemory/authz

Authz Build

   编译需要依赖 go 语言。在 Ubuntu 下可直接执行 sudo apt-get install golang 进行安装。

   安装完成后需要配置 GOPATH 环境变量, GOPATH 是 golang 的扩展库目录。golang 会首先搜索标准库目录,然后搜索 GOPATH 扩展库目录。所以开发时可以把非标准库都放在 GOPATH 目录下。

   配置好开发环境后就可以下载源码到本地,执行以下命令就可以成功编译并 Build 成 Docker 镜像文件。

$ cd broker/$ go build$ mv broker ../authz-broker $ cd ..$ docker build .

本地Build 源码修改

   在上面的 Build 中可以根据个人需要来修改一些配置,比如修改虚拟机引用和生成文件。

diff --git a/Dockerfile b/Dockerfile
index 2cfb355..c459421 100755--- a/Dockerfile+++ b/Dockerfile@@ -1,4 +1,4 @@-FROM alpine:3.3+FROM ubuntu:16.04
 MAINTAINER Liron Levin <[email protected]>
 
 # Indicates basic authorization is enforced
@@ -13,6 +13,7 @@ ENV AUDITOR-HOOK ""
 VOLUME /var/lib/twistlock/policy.json
 VOLUME /run/docker/plugins/ 
-ADD ./authz-broker  /usr/bin/authz-broker+ADD ./authz-broker  /usr/bin/authz-dm++CMD ["/usr/bin/authz-dm"]
 -CMD ["/usr/bin/authz-broker"]\ No newline at end of file

    项目依赖默认是引用了 github 上的库,想要修改库里的代码实现自定义功能,需要把引用改为本地引用,然后再修改相应的功能即可。

diff --git a/authz/basic.go b/authz/basic.go
index 0ed708d..042a904 100755--- a/authz/basic.go+++ b/authz/basic.go@@ -3,11 +3,13 @@ package authz
 import (
        "encoding/json"
        "fmt"+
        "github.com/Sirupsen/logrus"
        logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
        "github.com/docker/docker/pkg/authorization"
        "github.com/howeyc/fsnotify"-       "github.com/twistlock/authz/core"++       "authz/core"
        "io/ioutil"
        "log/syslog"
        "os"
diff --git a/broker/main.go b/broker/main.go
index 0526811..f3e76f0 100755--- a/broker/main.go+++ b/broker/main.go@@ -3,11 +3,13 @@ package main
 
 import (
        "fmt"+       "os"+

Twistlock AuthZ Broker 插件的使用

安装插件

  Twistlock AuthZ Broker 可以在容器中直接安装也可以在Docker外的主机中安装。

在容器中安装

  在容器中安装直接运行

$ docker run -d --restart=always -v /var/lib/authz-broker/policy.json:/var/lib/authz-broker/policy.json -v /run/docker/plugins/:/run/docker/plugins twistlock/authz-broker

在主机中安装

  在主机中安装需要编辑 docker 服务配置文件。

sudo systemctl edit --full docker.service

添加 authz 运行参数。

# add plugin flag ExecStart=/usr/bin/dockerd --authorization-plugin=authz-broker

  重启服务

# reload deamonsystemctl daemon-reload
systemctl restart docker

授权配置

  在路径 /var/lib/authz-broker/policy.json 下配置授权内容

  1. {"name":"policy_1","users":["alice"],"actions":[""]}

此处配置指明方案 policy_1 的用户 alice 可以执行的命令 actions 为所有命令。

  1. {"name":"policy_3","users":["alice","bob"],"actions":["container_create"]}

方案 policy_3 用户 alice 和 bob 可以执行的命令只有创建容器。

检查结果

允许结果

docker version

Client: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: OS/Arch: linux/amd64 Server: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: OS/Arch: linux/amd64</pre> #and log output: Sep 04 15:08:29 mj authz-broker[28646]: {"allow":true,"err":"","fields.msg":"action 'docker_version' allowed for user '' by policy 'policy_1'","level":"info","method":"GET","msg":"Request","time":"2016-09-04T15:08:29+01:00","uri":"/v1.24/version","user":""}

拒绝结果

Client: Version:      17.03.1-ce
 API version:  1.27
 Go version:   go1.7.5
 Git commit:   c6d412e Built:        Mon Mar 27 17:14:09 2017
 OS/Arch:      linux/amd64
Error response from daemon: authorization denied by plugin authz-broker: no policy applied (user: '' action: 'docker_version')

错误处理

no such file or directory

这是 authz Dockerfile 引用的是 FROM alpine:3.3 这个系统没有 bash ,改成 FROM ubuntu:16.04 就可以了。

还有其他的情况导致这个报错:

  1. 32位64位程序和系统不兼容导致

  2. Dockerfile 指定的文件没有打包到 image 中 
    可以用 docker run -it image/name [/bin/sh | /bin/bash] 进入image 环境,看看里面的文件是否齐全。


前期文章

《docker容器的全面安全防护》

《配置一个安全的docker宿主机》

后续预告

《docker镜像安全扫描器的实现》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注我们!!!

前言

     我们在实际的docker运行环境下,大都会遇到多用户的情况,为了安全起见,有些用户我们不想给予其全面的docker控制权限,比如不想某些用户执行docker stop 以及docker rm 等危险指令,当然我们可以从系统账号权限来控制,但docker在实际生产环节中大都以集群方式部署,都是通过docker守护进程接口来操作docker,系统控制实现起来略显麻烦,还好docker官方提供了非常方便的扩展插件来实现对用户权限细粒度控制,可细到只允许指定用户执行指定的docker命令,本文详细介绍如何来实现。

Docker 插件

   Docker 引擎允许用户使用第三方插件的形式扩展 Docker 功能。Docker 的插件类型分为以下三种大类:

  • Network plugins 网络插件可以提供容器间互联网络模型。

  • Volume plugins 数据卷插件可以使 Docker 数据卷跨多个主机。

  • Authorization plugins 验证插件可以提供基于权限的访问控制,也是本文主要讲的插件,比较出名的就是 Twistlock AuthZ Broker 。

Twistlock AuthZ Broker

   Twistlock AuthZ Broker 是一个直接运行在主机或容器中的 Dockers 授权插件,基于简单的用户审计策略,可以过滤命令是否允许执行。

源码地址

  https://github.com/deathmemory/authz

Authz Build

   编译需要依赖 go 语言。在 Ubuntu 下可直接执行 sudo apt-get install golang 进行安装。

   安装完成后需要配置 GOPATH 环境变量, GOPATH 是 golang 的扩展库目录。golang 会首先搜索标准库目录,然后搜索 GOPATH 扩展库目录。所以开发时可以把非标准库都放在 GOPATH 目录下。

   配置好开发环境后就可以下载源码到本地,执行以下命令就可以成功编译并 Build 成 Docker 镜像文件。

$ cd broker/$ go build$ mv broker ../authz-broker $ cd ..$ docker build .

本地Build 源码修改

   在上面的 Build 中可以根据个人需要来修改一些配置,比如修改虚拟机引用和生成文件。

diff --git a/Dockerfile b/Dockerfile
index 2cfb355..c459421 100755--- a/Dockerfile+++ b/Dockerfile@@ -1,4 +1,4 @@-FROM alpine:3.3+FROM ubuntu:16.04
 MAINTAINER Liron Levin <[email protected]>
 
 # Indicates basic authorization is enforced
@@ -13,6 +13,7 @@ ENV AUDITOR-HOOK ""
 VOLUME /var/lib/twistlock/policy.json
 VOLUME /run/docker/plugins/ 
-ADD ./authz-broker  /usr/bin/authz-broker+ADD ./authz-broker  /usr/bin/authz-dm++CMD ["/usr/bin/authz-dm"]
 -CMD ["/usr/bin/authz-broker"]\ No newline at end of file

    项目依赖默认是引用了 github 上的库,想要修改库里的代码实现自定义功能,需要把引用改为本地引用,然后再修改相应的功能即可。

diff --git a/authz/basic.go b/authz/basic.go
index 0ed708d..042a904 100755--- a/authz/basic.go+++ b/authz/basic.go@@ -3,11 +3,13 @@ package authz
 import (
        "encoding/json"
        "fmt"+
        "github.com/Sirupsen/logrus"
        logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
        "github.com/docker/docker/pkg/authorization"
        "github.com/howeyc/fsnotify"-       "github.com/twistlock/authz/core"++       "authz/core"
        "io/ioutil"
        "log/syslog"
        "os"
diff --git a/broker/main.go b/broker/main.go
index 0526811..f3e76f0 100755--- a/broker/main.go+++ b/broker/main.go@@ -3,11 +3,13 @@ package main
 
 import (
        "fmt"+       "os"+

Twistlock AuthZ Broker 插件的使用

安装插件

  Twistlock AuthZ Broker 可以在容器中直接安装也可以在Docker外的主机中安装。

在容器中安装

  在容器中安装直接运行

$ docker run -d --restart=always -v /var/lib/authz-broker/policy.json:/var/lib/authz-broker/policy.json -v /run/docker/plugins/:/run/docker/plugins twistlock/authz-broker

在主机中安装

  在主机中安装需要编辑 docker 服务配置文件。

sudo systemctl edit --full docker.service

添加 authz 运行参数。

# add plugin flag ExecStart=/usr/bin/dockerd --authorization-plugin=authz-broker

  重启服务

# reload deamonsystemctl daemon-reload
systemctl restart docker

授权配置

  在路径 /var/lib/authz-broker/policy.json 下配置授权内容

  1. {"name":"policy_1","users":["alice"],"actions":[""]}

此处配置指明方案 policy_1 的用户 alice 可以执行的命令 actions 为所有命令。

  1. {"name":"policy_3","users":["alice","bob"],"actions":["container_create"]}

方案 policy_3 用户 alice 和 bob 可以执行的命令只有创建容器。

检查结果

允许结果

docker version

Client: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: OS/Arch: linux/amd64 Server: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: OS/Arch: linux/amd64</pre> #and log output: Sep 04 15:08:29 mj authz-broker[28646]: {"allow":true,"err":"","fields.msg":"action 'docker_version' allowed for user '' by policy 'policy_1'","level":"info","method":"GET","msg":"Request","time":"2016-09-04T15:08:29+01:00","uri":"/v1.24/version","user":""}

拒绝结果

Client: Version:      17.03.1-ce
 API version:  1.27
 Go version:   go1.7.5
 Git commit:   c6d412e Built:        Mon Mar 27 17:14:09 2017
 OS/Arch:      linux/amd64
Error response from daemon: authorization denied by plugin authz-broker: no policy applied (user: '' action: 'docker_version')

错误处理

no such file or directory

这是 authz Dockerfile 引用的是 FROM alpine:3.3 这个系统没有 bash ,改成 FROM ubuntu:16.04 就可以了。

还有其他的情况导致这个报错:

  1. 32位64位程序和系统不兼容导致

  2. Dockerfile 指定的文件没有打包到 image 中 
    可以用 docker run -it image/name [/bin/sh | /bin/bash] 进入image 环境,看看里面的文件是否齐全。


前期文章

《docker容器的全面安全防护》

《配置一个安全的docker宿主机》

后续预告

《docker镜像安全扫描器的实现》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注我们!!!

前言

     本文以CentOS为例,介绍如何配置一个符合安全要求的docker运行宿主机。如使用默认安装来运行Docker守护进程和容器,会使你的主机出现安全和性能问题。 在本文中,我们将使用CentOS7的最小安装作为示例来说明如何配置来一个安全的docker运行宿主机。 CentOS已经大规模的应用到生产系统中,已被证明是一个稳定而安全的Linux版本。如果选择使用不同的Linux发行版,本文中的做法仍然值得参考,只需要将相关的示例命令转换为你的目标环境的命令。配置需要重点关注三个方面:

  • 1、一个不需要任何额外服务和软件的操作系统,只运行Docker所需的工具。

  • 2、安装并配置Docker守护程序以运行容器。 这包括性能和安全的设置,配置Docker更适合于生产环境而不是开发测试环境。

  • 3、设置访问控制的安全策略,将防火墙配置为仅允许SSH和用于外部通信的容器必要端口

 安装操作系统

         –最小安装

         从官方镜像中最小安装CentOS 7, 如果以前安装过CentOS,还有一些步骤需要考虑:

  • >删除所有开发工具(编译器等)

  • >删除所有监听的服务,只保留22端口以进行SSH访问。 防火墙配置阻止其他正在运行的服务和删除多余服务

  • –更新系统源

         更新系统源确保所有库和程序都运行最新的版本。

          sudo yum update –y

1.jpg

  • –创建一个新用户

         不能使用root用户运行容器,所以必须添加一个新的用户。

          例如:添加用户名为dockeruser的新用户

         1、向主机添加新用户。

          adduser dockeruser

           2 、为新用户设置密码

           passwd dockeruser

           3 、将用户添加到wheel组以提供sudo访问权限

          usermod -a -G wheel dockeruser

2.jpg

  • –生成SSHD密钥

         安全最佳做法是禁用使用密码的身份验证。我们可使密钥,并将其复制到服务器作为认证密钥。 以下步骤将是如何为linux或OSX主机生成密钥。

         1、 在的客户端上生成ssh密钥

              ssh-keygen –t rsa

           2、运行此命令将在你的主目录的.ssh目录中创建两个文件

            a.    id_rsa –私钥

            b.     id_rsa.pub –公钥

          3、使用新建用户dockeruser登录到Docker主机

              mkdir ~/.ssh

               sudo chmod 700 ~/.ssh

   4 、 将公钥复制到Docker主机。 例如

      scp ~/.ssh/id_rsa.pub [email protected]:~/.ssh/authorized_keys

   5、现在可以不使用密码登录到主机

      ssh [email protected]

  • –SSH禁用root登录和使用密码的身份验证

禁用root用户登录到主机系统。 另外不允许使用密码登录。这是为了防止使用暴力破解主机系统的账号 ,在上面介绍中,向系统添加了一个新用户,并将其密钥复制到服务器。 使用该用户配置主机。

   1、使用新添加的用户登录到主机

   2、编辑SSH程序的配置文件禁用root登录

      sudo vi /etc/ssh/sshd_config

   3、找到以下文本:

      #PermitRootLogin yes

   4、更改内容为如下

      PermitRootLogin no

   5、找到以下文本的行

      #PasswordAuthentication yes

    6、更改内容为如下

      PasswordAuthentication no

     7、重启SSH服务

      sudo systemctl restart sshd.service

  • –停止SSHD以外的任何服务

出于安全考虑,宿主机是用于运行容器,因此,不应该运行任何多余的服务。建议更改ssh监听端口以进一步增强安全性。 对于本文中,已经更改SSH监听端口41022。

   1、列出任何开放和侦听端口

      sudo nmap -sU -sS -p 1-65535 localhost

3.jpg

在这种情况下,我们有两个打开和侦听的TCP端口。 41022端口是可以运行的ssh侦听端口 。25端口是SMTP服务,为系统默认安装,我们需要停止。

 2、停止和删除postfix

      a.  停止postfix服务

          sudo systemctl stop postfix

      b.  检查postfix是否还在监听端口25

          sudo nmap -sU -sS -p 1-65535 localhost

4.jpg

       c.  从主机中删除postfix

             sudo yum remove postfix


安装和配置Docker

  • –安装Docker

   1、配置yum以查找Docker存储库

    a.   创建一个新文件来保存Docker存储库的信息

        sudo vi /etc/yum.repos.d/docker-ce.repo

    b.   将以下内容添加到文件中:

         [docker-ce-stable]

          name=Docker CE Stable – $basearch

          baseurl=https://download.docker.com/linux/centos/7/$basearch/stable

          enabled=1

          gpgcheck=1

          gpgkey=https://download.docker.com/linux/centos/gpg

    c.    保存文件

   2、安装Docker软件包

      sudo yum install docker-ce –y

5.jpg

   3、启动docker

       sudo service docker start

       Alternate: systemctl start docker

   4、验证docker是否正常运行

       sudo docker run hello-world

6.jpg

   5 、配置Docker守护进程在开机时启动

       sudo chkconfig docker on

        Alternate: sudo systemctl enable docker

7.jpg

  • –配置Docker

通过配置Docker来增加安全性,用于生产环境限制容器资源消耗。我们通过给docker守护进程传递命令行参数来执行操作。 我们将要使用的命令行总结如下

   –icc=false

  禁止容器之间通信。

       • 如果了解应用环境的网络拓扑,可以启用某几个容器与容器之间来进行通信使用—link参数

         (–link=CONTAINER_NAME_or_ID:ALIAS)

      • 如果需要所有容器之间相互通信,可以将其设置为true,但这种情况在某种程度上是不安全的,它允许所有容器之间的完全网络通信。

   –log-level“info”

   将日志级别设置为info。 日志记录有多个级别,但有时记录内容非常多,会导致磁盘耗尽。日志级别的设置是为了获取需要的日志信息,而不是获取所有记录信息,而且有些日志信息无用且占用大量磁盘空间。

  –iptables=true

   启用iptables规则。

  –default-ulimit

    为容器设置默认的ulimits。这个参数将设置限制进程和文件的数量有关。这会确保容器不会占用过多的主机资源而造成主机宕机。

      •–default-ulimit nproc=1024:2048 –default-ulimit nofile=1020:2048

Docker进程启动参数非常多,本文中只说明配置方法,更多进程启动参数请参阅docker官网。下面是进程启动参数的配置方法:

1 、编辑docker.service文件

     sudo vi /usr/lib/systemd/system/docker.service

2 、找到以下文本行

     ExecStart=/usr/bin/dockerd

3 、修改为如下

    ExecStart=/usr/bin/dockerd –icc=false –log-level  “info” –iptables=true –default-ulimit nproc=1024:2408–default-ulimit nofile=1024:2048 

 4、重新加载

     sudo systemctl daemon-reload

  5、重新启动Docker服务

      sudo service docker restart

  6、验证命令行操作现在被传递给Docker

      sudo ps -eaf | grep docker 

8.jpg

设置访问策略

    在安全性较高的系统中,防火墙配置应启用白名单策略,默认禁用所有入站和出站流量。CentOS 7的防火墙默认使用firewalld,为了方便和docker容器访问控制集成,这里将禁用firewalld并使用iptables服务用于访问控制。

   1 、禁用firewalld

         sudo systemctl disable firewalld

   2 、安装iptables-services

         sudo yum install iptables-services –y

   3 、启用iptables服务

         sudo systemctl enable iptables

   4、配置iptables规则(在最后的规则中,将<serverip>替换为你的服务器ip-address)

           sudo iptables -P INPUT DROP

         sudo iptables -P FORWARD DROP

         sudo iptables -A INPUT -p tcp -s 0/0 -d<serverip> –sport 513:65535 –dport 22 -mstate –state NEW,ESTABLISHED -j ACCEPT

   5、保存iptables规则

         sudo service iptables save

   6 、验证iptables规则

         sudo iptables –L

通过安装nginx进行测试

让我们通过下载并运行nginx来测试Docker的安装。 

  1、下载images

        sudo docker pull nginx (alpine /nginx:latest)

  2 、启动nginx容器

         sudo docker run –name docker-nginx -p 80:80 nginx

          注意:-p 80:80将容器的端口80映射到主机外部端口80

           这条命令会修改iptables的规则,以允许流量通过端口80

   3 、连接到端口80上主机的IP地址

            http://serverip:80

你应该在浏览器中看到以下内容:

  

9.jpg

总结

    本文介绍了如何通过安全配置来生成一个相对安全的docker运行宿主机,除此之外,还需通过安装最新的安全补丁并保持系统更新,定期安全巡检等来保证docker运行环境的安全性。



前期文章

《docker容器的全面安全防护》

后续预告

《docker镜像安全扫描器的实现》

《利用docker插件实现细粒度权限控制》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注!!!

前言

     本文以CentOS为例,介绍如何配置一个符合安全要求的docker运行宿主机。如使用默认安装来运行Docker守护进程和容器,会使你的主机出现安全和性能问题。 在本文中,我们将使用CentOS7的最小安装作为示例来说明如何配置来一个安全的docker运行宿主机。 CentOS已经大规模的应用到生产系统中,已被证明是一个稳定而安全的Linux版本。如果选择使用不同的Linux发行版,本文中的做法仍然值得参考,只需要将相关的示例命令转换为你的目标环境的命令。配置需要重点关注三个方面:

  • 1、一个不需要任何额外服务和软件的操作系统,只运行Docker所需的工具。

  • 2、安装并配置Docker守护程序以运行容器。 这包括性能和安全的设置,配置Docker更适合于生产环境而不是开发测试环境。

  • 3、设置访问控制的安全策略,将防火墙配置为仅允许SSH和用于外部通信的容器必要端口

 安装操作系统

         –最小安装

         从官方镜像中最小安装CentOS 7, 如果以前安装过CentOS,还有一些步骤需要考虑:

  • >删除所有开发工具(编译器等)

  • >删除所有监听的服务,只保留22端口以进行SSH访问。 防火墙配置阻止其他正在运行的服务和删除多余服务

  • –更新系统源

         更新系统源确保所有库和程序都运行最新的版本。

          sudo yum update –y

1.jpg

  • –创建一个新用户

         不能使用root用户运行容器,所以必须添加一个新的用户。

          例如:添加用户名为dockeruser的新用户

         1、向主机添加新用户。

          adduser dockeruser

           2 、为新用户设置密码

           passwd dockeruser

           3 、将用户添加到wheel组以提供sudo访问权限

          usermod -a -G wheel dockeruser

2.jpg

  • –生成SSHD密钥

         安全最佳做法是禁用使用密码的身份验证。我们可使密钥,并将其复制到服务器作为认证密钥。 以下步骤将是如何为linux或OSX主机生成密钥。

         1、 在的客户端上生成ssh密钥

              ssh-keygen –t rsa

           2、运行此命令将在你的主目录的.ssh目录中创建两个文件

            a.    id_rsa –私钥

            b.     id_rsa.pub –公钥

          3、使用新建用户dockeruser登录到Docker主机

              mkdir ~/.ssh

               sudo chmod 700 ~/.ssh

   4 、 将公钥复制到Docker主机。 例如

      scp ~/.ssh/id_rsa.pub [email protected]:~/.ssh/authorized_keys

   5、现在可以不使用密码登录到主机

      ssh [email protected]

  • –SSH禁用root登录和使用密码的身份验证

禁用root用户登录到主机系统。 另外不允许使用密码登录。这是为了防止使用暴力破解主机系统的账号 ,在上面介绍中,向系统添加了一个新用户,并将其密钥复制到服务器。 使用该用户配置主机。

   1、使用新添加的用户登录到主机

   2、编辑SSH程序的配置文件禁用root登录

      sudo vi /etc/ssh/sshd_config

   3、找到以下文本:

      #PermitRootLogin yes

   4、更改内容为如下

      PermitRootLogin no

   5、找到以下文本的行

      #PasswordAuthentication yes

    6、更改内容为如下

      PasswordAuthentication no

     7、重启SSH服务

      sudo systemctl restart sshd.service

  • –停止SSHD以外的任何服务

出于安全考虑,宿主机是用于运行容器,因此,不应该运行任何多余的服务。建议更改ssh监听端口以进一步增强安全性。 对于本文中,已经更改SSH监听端口41022。

   1、列出任何开放和侦听端口

      sudo nmap -sU -sS -p 1-65535 localhost

3.jpg

在这种情况下,我们有两个打开和侦听的TCP端口。 41022端口是可以运行的ssh侦听端口 。25端口是SMTP服务,为系统默认安装,我们需要停止。

 2、停止和删除postfix

      a.  停止postfix服务

          sudo systemctl stop postfix

      b.  检查postfix是否还在监听端口25

          sudo nmap -sU -sS -p 1-65535 localhost

4.jpg

       c.  从主机中删除postfix

             sudo yum remove postfix


安装和配置Docker

  • –安装Docker

   1、配置yum以查找Docker存储库

    a.   创建一个新文件来保存Docker存储库的信息

        sudo vi /etc/yum.repos.d/docker-ce.repo

    b.   将以下内容添加到文件中:

         [docker-ce-stable]

          name=Docker CE Stable – $basearch

          baseurl=https://download.docker.com/linux/centos/7/$basearch/stable

          enabled=1

          gpgcheck=1

          gpgkey=https://download.docker.com/linux/centos/gpg

    c.    保存文件

   2、安装Docker软件包

      sudo yum install docker-ce –y

5.jpg

   3、启动docker

       sudo service docker start

       Alternate: systemctl start docker

   4、验证docker是否正常运行

       sudo docker run hello-world

6.jpg

   5 、配置Docker守护进程在开机时启动

       sudo chkconfig docker on

        Alternate: sudo systemctl enable docker

7.jpg

  • –配置Docker

通过配置Docker来增加安全性,用于生产环境限制容器资源消耗。我们通过给docker守护进程传递命令行参数来执行操作。 我们将要使用的命令行总结如下

   –icc=false

  禁止容器之间通信。

       • 如果了解应用环境的网络拓扑,可以启用某几个容器与容器之间来进行通信使用—link参数

         (–link=CONTAINER_NAME_or_ID:ALIAS)

      • 如果需要所有容器之间相互通信,可以将其设置为true,但这种情况在某种程度上是不安全的,它允许所有容器之间的完全网络通信。

   –log-level“info”

   将日志级别设置为info。 日志记录有多个级别,但有时记录内容非常多,会导致磁盘耗尽。日志级别的设置是为了获取需要的日志信息,而不是获取所有记录信息,而且有些日志信息无用且占用大量磁盘空间。

  –iptables=true

   启用iptables规则。

  –default-ulimit

    为容器设置默认的ulimits。这个参数将设置限制进程和文件的数量有关。这会确保容器不会占用过多的主机资源而造成主机宕机。

      •–default-ulimit nproc=1024:2048 –default-ulimit nofile=1020:2048

Docker进程启动参数非常多,本文中只说明配置方法,更多进程启动参数请参阅docker官网。下面是进程启动参数的配置方法:

1 、编辑docker.service文件

     sudo vi /usr/lib/systemd/system/docker.service

2 、找到以下文本行

     ExecStart=/usr/bin/dockerd

3 、修改为如下

    ExecStart=/usr/bin/dockerd –icc=false –log-level  “info” –iptables=true –default-ulimit nproc=1024:2408–default-ulimit nofile=1024:2048 

 4、重新加载

     sudo systemctl daemon-reload

  5、重新启动Docker服务

      sudo service docker restart

  6、验证命令行操作现在被传递给Docker

      sudo ps -eaf | grep docker 

8.jpg

设置访问策略

    在安全性较高的系统中,防火墙配置应启用白名单策略,默认禁用所有入站和出站流量。CentOS 7的防火墙默认使用firewalld,为了方便和docker容器访问控制集成,这里将禁用firewalld并使用iptables服务用于访问控制。

   1 、禁用firewalld

         sudo systemctl disable firewalld

   2 、安装iptables-services

         sudo yum install iptables-services –y

   3 、启用iptables服务

         sudo systemctl enable iptables

   4、配置iptables规则(在最后的规则中,将<serverip>替换为你的服务器ip-address)

           sudo iptables -P INPUT DROP

         sudo iptables -P FORWARD DROP

         sudo iptables -A INPUT -p tcp -s 0/0 -d<serverip> –sport 513:65535 –dport 22 -mstate –state NEW,ESTABLISHED -j ACCEPT

   5、保存iptables规则

         sudo service iptables save

   6 、验证iptables规则

         sudo iptables –L

通过安装nginx进行测试

让我们通过下载并运行nginx来测试Docker的安装。 

  1、下载images

        sudo docker pull nginx (alpine /nginx:latest)

  2 、启动nginx容器

         sudo docker run –name docker-nginx -p 80:80 nginx

          注意:-p 80:80将容器的端口80映射到主机外部端口80

           这条命令会修改iptables的规则,以允许流量通过端口80

   3 、连接到端口80上主机的IP地址

            http://serverip:80

你应该在浏览器中看到以下内容:

  

9.jpg

总结

    本文介绍了如何通过安全配置来生成一个相对安全的docker运行宿主机,除此之外,还需通过安装最新的安全补丁并保持系统更新,定期安全巡检等来保证docker运行环境的安全性。



前期文章

《docker容器的全面安全防护》

后续预告

《docker镜像安全扫描器的实现》

《利用docker插件实现细粒度权限控制》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎订阅关注!!!

    Docker作为应用容器中最引人瞩目的实现方式,在近几年得到飞速的发展,大有成为应用容器事实标准的趋势,国内外不少企业已经将其应用到生产系统中了,有理由相信随着docker自身技术的完善和相关技术生态的建立,将成为下一代云计算的基石。

    Docker的优点很多,由于其诞生的目的就是便于持续的集成和快速部署,尽量减少中间环节,这也为其安全控制带来难度, Gartner在确定2017年中最高安全技术,关于容器安全是其中一项,原文如下:

     Containers use a shared operating system (OS) model. Anattack on a vulnerability in the host OS could lead to a compromise of allcontainers. Containers are not inherently unsecure, but they are being deployedin an unsecure manner by developers, with little or no involvement fromsecurity teams and little guidance from security architects. Traditionalnetwork and host-based security solutions are blind to containers. Containersecurity solutions protect the entire life cycle of containers from creationinto production and most of the container security solutions providepreproduction scanning combined with runtime monitoring and protection.

    报告分析了容器安全面临的挑战:容器使用共享操作系统(OS)模型。对主机操作系统中的漏洞的攻击可能导致所有容器被攻击,且容器本身并不完全安全。但真正的问题在于由开发人员以不安全的方式部署,安全团队很少或根本没有参与,安全架构师也没有指导。

Docker容器安全吗?

   本身这个问题就是一个哲学问题,答案是否定的-不安全,因为没有绝对安全。其实对docker容器安全质疑最大的一点就是其隔离的彻底性,与其对比就是当前成熟的虚拟机(VM)技术。相对于VM,docker容器只是对进程和文件进行虚拟化,而VM做到了OS级别的虚拟化。从这个角度看VM的隔离性确实要好于docker容器,也就是说对宿主机的安全影响VM要远远小于docker,但换个角度看,这也恰恰正是docker的一个优点:轻量级,高效以及易移植。所以,安全和易用永远存在在一个平衡点,本文探讨的前提是认同docker带来的便利性,也接受其带来的安全风险,而要做的是利用一些的安全手段来将其风险降到可接受范围。

而容器安全如何来实现呢?

     其实在Gartner的报告中也提到了,需要对docker全生命周期的安全防护,信息安全本质上就是控制风险,如果从一个docker的生命中周期面临的安全威胁来设计docker的安全防护策略,那安全控制的思路就会十分清晰。

     首先,来简单捋一下docker容器的生命周期,一个docker容器从产生到运行部署大致分为如下三个状态:

 

1.jpg

  • –Dockerfile:用于创建image镜像的模板文件,出于管理和安全的考虑,docker官方建议所有的镜像文件应该由dockerfile来创建,而当前不少用户把docker当虚拟机来使用,甚至容器中安装SSH,从安全的角度,这是不恰当的。

  • –Image:镜像文件,对比PC端的概念,我们可以把它理解为服务器端的可执行软件包。一旦打包生成,如存在安全问题,那这些问题也被一并打包,最后导致安全事件。

  • –Container:运行起来的image文件就是容器了,从外来看就是一个应用,可对外提供服务了。

      所以不难发现,docker容器的生命周期,就是一个镜像文件从产生、运行到停止的过程,对其安全防护的目标就很明确了,那就是:

2.jpg

     接下来,我们把docker容器生命周期和实际工作中结合起来,大致如下图所示:

 

3.jpg

      在一般企业内,一个标准的产品发布流程大致如下:研发人员将代码提交给代码库;QA和安全人员通过jekins等工具进行编译并测试;测试完成后,由运维人员获取最终上线版本,发布到生产环境。也可能是测试完成后,直接发布到生产环境。

     化繁为简,可将docker生命周期拆为两个大阶段,非生产环境阶段和生产环境阶段,这两个阶段安全控制的目标如下:非生产环境中保证镜像安全可信,生产环境中保证镜像正确的运行。


两个阶段安全保护措施

   Docker公司与美国互联网安全中心(CIS)合作,制定了docker的最佳安全实践,其中包括了主机安全配置、docker守护进程配置、docker守护程序配置文件、容器镜像和构建、容器运行安全、docker安全操作六大项,99个控制点。几乎覆盖了docker安全要求各个方面,我们也对其进行了翻译和整理,在本专栏的后续文章中会陆续发布。

保证非生产环境中的镜像安全

   –容器使用非root用户运行

   为了防止容器逃逸而获得宿主机的权限,容器内应用以非root用户身份运行,如果用户已经在容器镜像中定义,则默认情况下容器将作为该用户运行,且不需要特定的用户命名空间重新映射。可以在Dockerfile中添加用户:RUN useradd -d / home /username -m -s / bin / bash username USER username 

   –使用安全的基础镜像

   如果基础镜像存在安全问题,那整个镜像文件的安全性也无从谈起,用户可根据自身需求定制基础镜像,并强制要求组织内使用认可的基础镜像;也可使用第三方安全的镜像,这里推荐使用Alpine-linux,docker所有的官方镜像都使用其作为基础镜像,docker也会对其维护更新,所以安全性有保证。

   –删除镜像中的setuid和setgid权限

   setuid和setgid权限可用于提权。虽然有时候必须要使用到,但如果被滥用,可能会导致非法的提升权限。可以在镜像中限制这些权限的使用。具体做法可参考:在构建镜像时通过在Dockerfile中添加以下命令来删除这些权限,一般在Dockerfile的末尾添加:RUN find / -perm +6000-type f-exec chmod a-s {} \;|| true

  –启用Docker的内容信任

    内容信任允许当用户使用远程Docker仓库进行操作时,以执行镜像标记的客户端签名和验证。内容信任提供了对从Docker仓库发送和接收的数据使用数字签名的能力。这些签名允许客户端验证特定镜像标签的完整性。

在默认情况下,内容信任是禁用的。可通过如下命令进行启动: export DOCKER_CONTENT_TRUST = 1

 –最小安装原则:

   安全的普适法则,不要安装任何与应用无关的东西。

 –对镜像进行安全漏洞扫描

   镜像中包含了很多的插件及软件包,需要对这些软件包进行漏洞扫描,并根据结果安装补丁或更新软件,Coreos提供了一款开源docker镜像安全扫描器-Clair,(github地址:https://github.com/coreos/clair)。Clair可对镜像文件进行静态的安全扫描,并结合CVE给出漏洞扫描结果,运行效果如下:

4.jpg

     关于Clair的实现原理,会在后续的文章介绍,同时我们参考了Clair的实现方式,优化了开发了一款docker镜像扫描器,也会在适当的时候推出并开源。

如何保证生产环境中容器的安全?

  • –对docker宿主机进行安全加固

     务必保证docker宿主机的安全,需要对宿主机的系统进行安全加固处理,主机加固可以参考相关的安全checklist以及各企业制定的主机安全规范,在这里就不在赘述。

  • –限制容器之间的网络流量

     在默认情况下,同一主机上的所有容器之间网络流量不受限制。因此,每个容器都有可能在同一主机上的容器网络上读取所有数据包。这可能会导致意外泄露信息。因此,需要限制容器间通信具体操作:在守护进程模式下运行docker,并将’–icc = false’作为参数。如:/ usr / bin / dockerd –icc = false 

  • –配置Docker守护程序的TLS身份验证

    在默认情况下,Docker守护程序绑定到非联网的Unix套接字,并以root权限运行。若将默认的docker守护程序更改为绑定到TCP端口或任何其他Unix套接字,那么任何有权访问该端口或套接字的人都可以完全访问Docker守护程序。因此,不应该将Docker守护程序绑定到另一个IP /端口或Unix套接字。如果必须通过网络套接字暴露Docker守护程序,需为守护程序和Docker Swarm API配置TLS身份验证。 

  • –启用用户命名空间支持

    防止容器内的提权攻击的最佳方法是将容器的应用程序配置为无特权用户运行。对于必须使用roo身份运行的容器,可以将该用户重新映射到Docker主机上特定用户。映射的用户被分配一个范围的UID,它们在命名空间内作为正常的UID,但对主机本身没有特权。关于使用用户命名空间隔离容器在后续文章中详细介绍。

  • –限制容器的内存使用量

    在默认情况下,容器可以使用主机上的所有内存。可以使用内存限制机制来防止一个容器消耗所有主机资源的拒绝服务攻击,具体可使用使用“-m”或“–memory”参数运行容器。如下:

$> docker run <运行参数> –memory <memory-size> <Container ImageName或ID> <Command>

  • –适当设置容器CPU优先级

   在默认情况下,CPU时间在容器间平均分配,可使用CPU共享功能来设定优先级。 CPU共享允许将一个容器优先于另一个容器,并禁止较低优先级的容器频繁地占用CPU资源。这样可确保高优先级的容器更好地运行,且可以有效的防止资源耗尽攻击。

    针对docker安全配置检查,docker官方提供了一个脚本工具docker-bench-secruity(github地址:https://github.com/docker/docker-bench-security),检查依据便是CIS的最佳安全实践,运行界面如下:

 

5.jpg

 

      通过前文介绍,列举了在docker容器生命周期需要进行的安全控制措施,但如果仅靠人工实施和监督,可能效果不会太好。若能将docker各个生命周期的安全管理自动化才是最佳实现方式,docker安全刚刚开始呀:)

 


后续预告

由于docker安全涉及内容较多,dosec专栏会发布一系列的原创文章,目前拟定有:

《docker镜像安全扫描器的实现》

《利用docker插件实现细粒度权限控制》

《配置安全的docker运行liunx主机》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎关注!!!

    Docker作为应用容器中最引人瞩目的实现方式,在近几年得到飞速的发展,大有成为应用容器事实标准的趋势,国内外不少企业已经将其应用到生产系统中了,有理由相信随着docker自身技术的完善和相关技术生态的建立,将成为下一代云计算的基石。

    Docker的优点很多,由于其诞生的目的就是便于持续的集成和快速部署,尽量减少中间环节,这也为其安全控制带来难度, Gartner在确定2017年中最高安全技术,关于容器安全是其中一项,原文如下:

     Containers use a shared operating system (OS) model. Anattack on a vulnerability in the host OS could lead to a compromise of allcontainers. Containers are not inherently unsecure, but they are being deployedin an unsecure manner by developers, with little or no involvement fromsecurity teams and little guidance from security architects. Traditionalnetwork and host-based security solutions are blind to containers. Containersecurity solutions protect the entire life cycle of containers from creationinto production and most of the container security solutions providepreproduction scanning combined with runtime monitoring and protection.

    报告分析了容器安全面临的挑战:容器使用共享操作系统(OS)模型。对主机操作系统中的漏洞的攻击可能导致所有容器被攻击,且容器本身并不完全安全。但真正的问题在于由开发人员以不安全的方式部署,安全团队很少或根本没有参与,安全架构师也没有指导。

Docker容器安全吗?

   本身这个问题就是一个哲学问题,答案是否定的-不安全,因为没有绝对安全。其实对docker容器安全质疑最大的一点就是其隔离的彻底性,与其对比就是当前成熟的虚拟机(VM)技术。相对于VM,docker容器只是对进程和文件进行虚拟化,而VM做到了OS级别的虚拟化。从这个角度看VM的隔离性确实要好于docker容器,也就是说对宿主机的安全影响VM要远远小于docker,但换个角度看,这也恰恰正是docker的一个优点:轻量级,高效以及易移植。所以,安全和易用永远存在在一个平衡点,本文探讨的前提是认同docker带来的便利性,也接受其带来的安全风险,而要做的是利用一些的安全手段来将其风险降到可接受范围。

而容器安全如何来实现呢?

     其实在Gartner的报告中也提到了,需要对docker全生命周期的安全防护,信息安全本质上就是控制风险,如果从一个docker的生命中周期面临的安全威胁来设计docker的安全防护策略,那安全控制的思路就会十分清晰。

     首先,来简单捋一下docker容器的生命周期,一个docker容器从产生到运行部署大致分为如下三个状态:

 

1.jpg

  • –Dockerfile:用于创建image镜像的模板文件,出于管理和安全的考虑,docker官方建议所有的镜像文件应该由dockerfile来创建,而当前不少用户把docker当虚拟机来使用,甚至容器中安装SSH,从安全的角度,这是不恰当的。

  • –Image:镜像文件,对比PC端的概念,我们可以把它理解为服务器端的可执行软件包。一旦打包生成,如存在安全问题,那这些问题也被一并打包,最后导致安全事件。

  • –Container:运行起来的image文件就是容器了,从外来看就是一个应用,可对外提供服务了。

      所以不难发现,docker容器的生命周期,就是一个镜像文件从产生、运行到停止的过程,对其安全防护的目标就很明确了,那就是:

2.jpg

     接下来,我们把docker容器生命周期和实际工作中结合起来,大致如下图所示:

 

3.jpg

      在一般企业内,一个标准的产品发布流程大致如下:研发人员将代码提交给代码库;QA和安全人员通过jekins等工具进行编译并测试;测试完成后,由运维人员获取最终上线版本,发布到生产环境。也可能是测试完成后,直接发布到生产环境。

     化繁为简,可将docker生命周期拆为两个大阶段,非生产环境阶段和生产环境阶段,这两个阶段安全控制的目标如下:非生产环境中保证镜像安全可信,生产环境中保证镜像正确的运行。


两个阶段安全保护措施

   Docker公司与美国互联网安全中心(CIS)合作,制定了docker的最佳安全实践,其中包括了主机安全配置、docker守护进程配置、docker守护程序配置文件、容器镜像和构建、容器运行安全、docker安全操作六大项,99个控制点。几乎覆盖了docker安全要求各个方面,我们也对其进行了翻译和整理,在本专栏的后续文章中会陆续发布。

保证非生产环境中的镜像安全

   –容器使用非root用户运行

   为了防止容器逃逸而获得宿主机的权限,容器内应用以非root用户身份运行,如果用户已经在容器镜像中定义,则默认情况下容器将作为该用户运行,且不需要特定的用户命名空间重新映射。可以在Dockerfile中添加用户:RUN useradd -d / home /username -m -s / bin / bash username USER username 

   –使用安全的基础镜像

   如果基础镜像存在安全问题,那整个镜像文件的安全性也无从谈起,用户可根据自身需求定制基础镜像,并强制要求组织内使用认可的基础镜像;也可使用第三方安全的镜像,这里推荐使用Alpine-linux,docker所有的官方镜像都使用其作为基础镜像,docker也会对其维护更新,所以安全性有保证。

   –删除镜像中的setuid和setgid权限

   setuid和setgid权限可用于提权。虽然有时候必须要使用到,但如果被滥用,可能会导致非法的提升权限。可以在镜像中限制这些权限的使用。具体做法可参考:在构建镜像时通过在Dockerfile中添加以下命令来删除这些权限,一般在Dockerfile的末尾添加:RUN find / -perm +6000-type f-exec chmod a-s {} \;|| true

  –启用Docker的内容信任

    内容信任允许当用户使用远程Docker仓库进行操作时,以执行镜像标记的客户端签名和验证。内容信任提供了对从Docker仓库发送和接收的数据使用数字签名的能力。这些签名允许客户端验证特定镜像标签的完整性。

在默认情况下,内容信任是禁用的。可通过如下命令进行启动: export DOCKER_CONTENT_TRUST = 1

 –最小安装原则:

   安全的普适法则,不要安装任何与应用无关的东西。

 –对镜像进行安全漏洞扫描

   镜像中包含了很多的插件及软件包,需要对这些软件包进行漏洞扫描,并根据结果安装补丁或更新软件,Coreos提供了一款开源docker镜像安全扫描器-Clair,(github地址:https://github.com/coreos/clair)。Clair可对镜像文件进行静态的安全扫描,并结合CVE给出漏洞扫描结果,运行效果如下:

4.jpg

     关于Clair的实现原理,会在后续的文章介绍,同时我们参考了Clair的实现方式,优化了开发了一款docker镜像扫描器,也会在适当的时候推出并开源。

如何保证生产环境中容器的安全?

  • –对docker宿主机进行安全加固

     务必保证docker宿主机的安全,需要对宿主机的系统进行安全加固处理,主机加固可以参考相关的安全checklist以及各企业制定的主机安全规范,在这里就不在赘述。

  • –限制容器之间的网络流量

     在默认情况下,同一主机上的所有容器之间网络流量不受限制。因此,每个容器都有可能在同一主机上的容器网络上读取所有数据包。这可能会导致意外泄露信息。因此,需要限制容器间通信具体操作:在守护进程模式下运行docker,并将’–icc = false’作为参数。如:/ usr / bin / dockerd –icc = false 

  • –配置Docker守护程序的TLS身份验证

    在默认情况下,Docker守护程序绑定到非联网的Unix套接字,并以root权限运行。若将默认的docker守护程序更改为绑定到TCP端口或任何其他Unix套接字,那么任何有权访问该端口或套接字的人都可以完全访问Docker守护程序。因此,不应该将Docker守护程序绑定到另一个IP /端口或Unix套接字。如果必须通过网络套接字暴露Docker守护程序,需为守护程序和Docker Swarm API配置TLS身份验证。 

  • –启用用户命名空间支持

    防止容器内的提权攻击的最佳方法是将容器的应用程序配置为无特权用户运行。对于必须使用roo身份运行的容器,可以将该用户重新映射到Docker主机上特定用户。映射的用户被分配一个范围的UID,它们在命名空间内作为正常的UID,但对主机本身没有特权。关于使用用户命名空间隔离容器在后续文章中详细介绍。

  • –限制容器的内存使用量

    在默认情况下,容器可以使用主机上的所有内存。可以使用内存限制机制来防止一个容器消耗所有主机资源的拒绝服务攻击,具体可使用使用“-m”或“–memory”参数运行容器。如下:

$> docker run <运行参数> –memory <memory-size> <Container ImageName或ID> <Command>

  • –适当设置容器CPU优先级

   在默认情况下,CPU时间在容器间平均分配,可使用CPU共享功能来设定优先级。 CPU共享允许将一个容器优先于另一个容器,并禁止较低优先级的容器频繁地占用CPU资源。这样可确保高优先级的容器更好地运行,且可以有效的防止资源耗尽攻击。

    针对docker安全配置检查,docker官方提供了一个脚本工具docker-bench-secruity(github地址:https://github.com/docker/docker-bench-security),检查依据便是CIS的最佳安全实践,运行界面如下:

 

5.jpg

 

      通过前文介绍,列举了在docker容器生命周期需要进行的安全控制措施,但如果仅靠人工实施和监督,可能效果不会太好。若能将docker各个生命周期的安全管理自动化才是最佳实现方式,docker安全刚刚开始呀:)

 


后续预告

由于docker安全涉及内容较多,dosec专栏会发布一系列的原创文章,目前拟定有:

《docker镜像安全扫描器的实现》

《利用docker插件实现细粒度权限控制》

《配置安全的docker运行liunx主机》

《docker最佳安全实践详解》

《docker内容信任详解》

《docker安全管理平台的架构设计》

 欢迎关注!!!