如何像gitlab-runner那样将Go应用安装为系统服务

本文永久链接 – https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner

《让reviewdog支持gitlab-push-commit,守住代码质量下限》一文中,gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤):

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等,我们暂不care),系统服务就是由systemd管理的daemon process(守护进程)。

systemd是什么?linux主机上电后,os内核被加载并启动,os内核完成初始化以后,由内核第一个启动的程序是init程序,其PID(进程ID)为1,它为系统里所有进程的“祖先”,systemd便是主流新版linux中的那个init程序,它负责在主机启动后拉起所有安装为系统服务的程序

这些被systemd拉起的服务程序以守护进程(daemon process)的形式运行,那什么又是守护进程呢?《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的:

Daemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities.

守护进程是长期存在的进程。它们通常在系统启动时被启动,并在系统关闭时才终止。因为它们没有控制终端,我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。

该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules)),并给出了一个C语言示例:

#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void
daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /*
     * Clear file creation mask.
     */
    umask(0);
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can’t get file limit", cmd);
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won’t allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can’t ignore SIGHUP", cmd);
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    /*
     * Change the current working directory to the root so
     * we won’t prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can’t change directory to /", cmd);
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

那么,Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢?很遗憾!Go团队说很难做到。Go社区倒是有很多第三方的方案,比如像go-daemon这样的第三方实现,不过我并没有验证过这些方案,不保证完全ok。

Go团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换。gitlab-runner就是将自己安装为system服务,并由systemd对其进行管理的。

题外话:其实,自从有了容器技术(比如:docker)后,daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器,应用本身就运行于后台,使用–restart=always/on-failure选项,容器引擎(比如docker engine)会帮我们管理service,并在service宕掉后重启service。

那么,我们如何像gitlab-runner那样将自己安装为一个systemd service呢?我们继续向下看。

注意:这里只是将Go应用安装成一个systemd service,并不是自己将自己转换为守护进程,安装为systemd service本身是可行的,也是安全的。

翻看gitlab-runner源码,你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包,这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包,该包屏蔽了os层的差异,为开发人员提供了相对简单的Service操作接口,包括下面这些控制动作:

// github.com/kardianos/service/blob/master/service.go
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}

好了,下面我们就用一个例子myapp来介绍一下如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力

myapp是一个http server,它在某个端口上提供服务,当收到请求时,返回”Welcome”字样的应答:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

func run(config string) error {
    ... ...

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[%s]: receive a request from: %s\n", c.Server.Addr, r.RemoteAddr)
        w.Write([]byte("Welcome"))
    })
    fmt.Printf("listen on %s\n", c.Server.Addr)
    return http.ListenAndServe(c.Server.Addr, nil)
}

现在我们要为myapp增加一些能力,让它支持将自己安装为systemd service,并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。

我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包,只是用标准库的flag包。由于myapp支持subcommand,我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例,如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上,比如下面代码init函数体中的内容。

另外由于使用了subcommand,默认的flag.Usage不再能满足我们的要求了,我们需要自己实现一个usage函数并赋值给flag.Usage:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

var (
    installCommand = flag.NewFlagSet("install", flag.ExitOnError)
    runCommand     = flag.NewFlagSet("run", flag.ExitOnError)
    user           string
    workingdir     string
    config         string
)

const (
    defaultConfig = "/etc/myapp/config.ini"
)

func usage() {
    s := `
USAGE:
   myapp command [command options] 

COMMANDS:
     install               install service
     uninstall             uninstall service
     start                 start service
     stop                  stop service
     run                   run service

OPTIONS:
     -config string
        config file of the service (default "/etc/myapp/config.ini")
     -user string
        user account to run the service
     -workingdir string
        working directory of the service`

    fmt.Println(s)
}

func init() {
    installCommand.StringVar(&user, "user", "", "user account to run the service")
    installCommand.StringVar(&workingdir, "workingdir", "", "working directory of the service")
    installCommand.StringVar(&config, "config", "/etc/myapp/config.ini", "config file of the service")
    runCommand.StringVar(&config, "config", defaultConfig, "config file of the service")
    flag.Usage = usage
}

func main() {
    var err error
    n := len(os.Args)
    if n <= 1 {
        fmt.Printf("invalid args\n")
        flag.Usage()
        return
    }

    subCmd := os.Args[1] // the second arg

    // get Config
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }
... ...
}

这些都完成后,我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息:

// https://github.com/bigwhite/experiments/blob/master/system-service/config.go

func getServiceConfig(subCmd string) (*service.Config, error) {
    c := service.Config{
        Name:             "myApp",
        DisplayName:      "Go Daemon Service Demo",
        Description:      "This is a Go daemon service demo",
        Executable:       "/usr/local/bin/myapp",
        Dependencies:     []string{"After=network.target syslog.target"},
        WorkingDirectory: "",
        Option: service.KeyValue{
            "Restart": "always", // Restart=always
        },
    }   

    switch subCmd {
    case "install":
        installCommand.Parse(os.Args[2:])
        if user == "" {
            fmt.Printf("error: user should be provided when install service\n")
            return nil, errors.New("invalid user")
        }
        if workingdir == "" {
            fmt.Printf("error: workingdir should be provided when install service\n")
            return nil, errors.New("invalid workingdir")
        }
        c.UserName = user
        c.WorkingDirectory = workingdir

        // arguments
        // ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"
        c.Arguments = append(c.Arguments, "run", "-config", config)
    case "run":
        runCommand.Parse(os.Args[2:]) // parse config
    }   

    return &c, nil
}

这里要注意的是service.Config中的Option和Arguments,前者用于在systemd service unit配置文件中放置任意的键值对(比如这里的Restart=always),而Arguments则会被组成为ExecStart键的值,该值会在start service时传入使用。

接下来,我们便利用service包基于加载的Config创建操作服务的实例(srv),然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go
func main() {

    // ... ...
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }

    prg := &NullService{}
    srv, err := service.New(prg, c)
    if err != nil {
        fmt.Printf("new service error: %s\n", err)
        return
    }

    err = runServiceControl(srv, subCmd)
    if err != nil {
        fmt.Printf("%s operation error: %s\n", subCmd, err)
        return
    }

    fmt.Printf("%s operation ok\n", subCmd)
    return
}

func runServiceControl(srv service.Service, subCmd string) error {
    switch subCmd {
    case "run":
        return run(config)
    default:
        return service.Control(srv, subCmd)
    }
}

好了,代码已经完成!现在让我们来验证一下myapp的能力。

我们先来完成编译和二进制程序的安装:

$make
go build -o myapp main.go config.go

$sudo make install
cp ./myapp /usr/local/bin
$sudo make install-cfg
mkdir -p /etc/myapp
cp ./config.ini /etc/myapp

接下来,我们就来将myapp安装为systemd的服务:

$sudo ./myapp install -user tonybai -workingdir /home/tonybai
install operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead)

我们看到安装后,myApp已经成为了myApp.service,并处于inactive状态,其systemd unit文件/etc/systemd/system/myApp.service内容如下:

$sudo cat /etc/systemd/system/myApp.service
[Unit]
Description=This is a Go daemon service demo
ConditionFileIsExecutable=/usr/local/bin/myapp

After=network.target syslog.target 

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"

WorkingDirectory=/home/tonybai
User=tonybai

Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/myApp

[Install]
WantedBy=multi-user.target

接下来,我们来启动一下该服务:

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.3M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432

我们看到myApp服务成功启动,并在65432这个端口上监听!

我们利用curl向这个端口发送一个请求:

$curl localhost:65432
Welcome                                                                         

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348

我们看到myApp服务运行正常并返回预期应答结果。

现在我们利用stop subcommand停掉该服务:

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago
    Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM)
   Main PID: 623859 (code=killed, signal=TERM)

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo...
Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded.
Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo.

修改配置/etc/myapp/config.ini(将监听端口从65432改为65431),然后再重启该服务:

$sudo cat /etc/myapp/config.ini
[server]
addr=":65431"

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago
   Main PID: 624046 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431

从systemd的状态日志中我们看到myApp服务启动成功,并改为监听65431端口,我们访问一下该端口:

$curl localhost:65431
Welcome                                                                                                                      

$curl localhost:65432
curl: (7) Failed to connect to localhost port 65432: Connection refused

从上述结果可以看出,我们的配置更新和重启都是成功的!

我们亦可以使用myapp的uninstall功能从systemd中卸载该服务:

$sudo ./myapp uninstall
uninstall operation ok
$sudo systemctl status myApp
Unit myApp.service could not be found.

好了,到这里我们看到:在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。

最后小结一下:service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力,不过这个作业就留给大家了:)。大家如有问题,欢迎在评论区留言。

本文涉及的代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

有没有安全漏洞,你说了不算,govulncheck是裁判!

本文永久链接 – https://tonybai.com/2022/09/10/an-intro-of-govulncheck


2022年9月7日,Go安全团队在Go官博发表文章《Vulnerability Management for Go》,正式向所有Gopher介绍Go对安全漏洞管理的工具和方案。

在这篇文章中,Go安全团队引入了一个名为govulncheck的命令行工具。这个工具本质上只是Go安全漏洞数据库(Go vulnerability database)的一个前端,它通过Go官方维护的vuln仓库下面的vulncheck包对你仓库中的Go源码或编译后的Go应用可执行二进制文件进行扫描,形成源码的调用图(callgraph)和调用栈(callstack)。

vuln仓库下面的client包则提供了访问漏洞数据源(支持多数据源)的接口和默认实现,开发人员可以基于module的路径或ID从漏洞数据库中查找是否存在已知的漏洞。而漏洞项采用OSV, Open Source Vulnerability format格式存储和传输,vuln仓库同样提供了对osv格式的实现包osv


图:Go安全漏洞方案架构 – 来自Go官方博客

注:你也可以基于vuln仓库下面的vulncheck包、client包等开发你自己的vulnerability检查前端,或将其集成到你的组织内部的工具链中。

和sumdb、proxy等一样,Go官方维护了一个默认的漏洞数据库vuln.go.dev,如上图所示,该数据库接纳来自知名漏洞数据源的数据,比如:NVDGHSA等,Go安全团队发现和修复的漏洞以及最广大的go开源项目维护者提交的漏洞。

如果你是知名go开源项目的维护者,当你发现并修复你的项目的漏洞后,可以在Go漏洞管理页面找到不同类型漏洞的提交/上报入口,Go安全团队会对你上报的公共漏洞信息进行审核和确认。

好了,作为Gopher,我们更关心的是我们正在开发的Go项目中是否存在安全漏洞,这些漏洞或是来自Go编译器,或是来自依赖的有漏洞的第三方包。我们要学会使用govulncheck工具对我们的项目进行扫描。

govulncheck目前维护在golang.org/x/vuln下面,按照官博的说法,后期该工具会随同Go安装包一并发布,但是否会集成到go命令中尚不可知。现在要使用govulncheck,我们必须手动安装,命令如下:

$go install golang.org/x/vuln/cmd/govulncheck@latest

安装成功后,便可以在你的Go项目根目录下执行下面命令对整个项目进行漏洞检查了:

$govulncheck ./...

下面是我对自己项目的扫描的结果(扫描时使用的是Go 1.18版本):

$govulncheck ./...
govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.

Scanning for dependencies with known vulnerabilities...
Found 9 known vulnerabilities.

Vulnerability #1: GO-2022-0524
  Calling Reader.Read on an archive containing a large number of
  concatenated 0-length compressed files can cause a panic due to
  stack exhaustion.

  Call stacks in your code:
      raft/fsm.go:193:29: example.com/go/mynamespace/demo1/raft.updOnlyLinearizableSM.RecoverFromSnapshot calls io/ioutil.ReadAll, which eventually calls compress/gzip.Reader.Read

  Found in: compress/gzip@go1.18
  Fixed in: compress/gzip@go1.18.4
  More info: https://pkg.go.dev/vuln/GO-2022-0524

Vulnerability #2: GO-2022-0531
  An attacker can correlate a resumed TLS session with a previous
  connection. Session tickets generated by crypto/tls do not
  contain a randomly generated ticket_age_add, which allows an
  attacker that can observe TLS handshakes to correlate successive
  connections by comparing ticket ages during session resumption.

  Call stacks in your code:
      raft/raft.go:68:35: example.com/go/mynamespace/demo1/raft.NewRaftNode calls github.com/lni/dragonboat/v3.NewNodeHost, which eventually calls crypto/tls.Conn.Handshake

  Found in: crypto/tls@go1.18
  Fixed in: crypto/tls@go1.18.3
  More info: https://pkg.go.dev/vuln/GO-2022-0531

... ...

Vulnerability #6: GO-2021-0057
  Due to improper bounds checking, maliciously crafted JSON
  objects can cause an out-of-bounds panic. If parsing user input,
  this may be used as a denial of service vector.

  Call stacks in your code:
      cmd/demo1/main.go:352:23: example.com/go/mynamespace/demo1/cmd/demo1.main calls example.com/go/mynamespace/common/naming.Register, which eventually calls github.com/buger/jsonparser.GetInt

  Found in: github.com/buger/jsonparser@v0.0.0-20181115193947-bf1c66bbce23
  Fixed in: github.com/buger/jsonparser@v1.1.1
  More info: https://pkg.go.dev/vuln/GO-2021-0057

... ...
Vulnerability #9: GO-2022-0522
  Calling Glob on a path which contains a large number of path
  separators can cause a panic due to stack exhaustion.

  Call stacks in your code:
      service/service.go:45:12: example.com/go/mynamespace/demo1/service.NewPubsubService calls example.com/go/mynamespace/common/log.Logger.Fatal, which eventually calls path/filepath.Glob

  Found in: path/filepath@go1.18
  Fixed in: path/filepath@go1.18.4
  More info: https://pkg.go.dev/vuln/GO-2022-0522

=== Informational ===

The vulnerabilities below are in packages that you import, but your code
doesn't appear to call any vulnerable functions. You may not need to take any
action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
for details.

Vulnerability #1: GO-2022-0537
  Decoding big.Float and big.Rat types can panic if the encoded message is
  too short, potentially allowing a denial of service.

  Found in: math/big@go1.18
  Fixed in: math/big@go1.18.5
  More info: https://pkg.go.dev/vuln/GO-2022-0537

... ...

Vulnerability #9: GO-2021-0052
  Due to improper HTTP header santization, a malicious user can spoof their
  source IP address by setting the X-Forwarded-For header. This may allow
  a user to bypass IP based restrictions, or obfuscate their true source.

  Found in: github.com/gin-gonic/gin@v1.6.3
  Fixed in: github.com/gin-gonic/gin@v1.7.7
  More info: https://pkg.go.dev/vuln/GO-2021-0052

我们看到govulncheck输出的信息分为两部分,一部分是扫描出来的项目存在的安全漏洞,针对这些漏洞你必须fix;而另外一部分(由=== Informational === 分隔的)则是列出一些带有安全漏洞的包,这些包是你直接导入或间接依赖的,但是你并未直接调用存在漏洞的包中的函数或方法,因此无需采取任何弥补措施。这样,我们仅需重点关注第一部分信息即可。

根据漏洞所在的宿主不同,第一部分的信息也可以分为两类:一类是Go语言自身(包括Go编译器、Go运行时和Go标准库等)引入的漏洞,另外一类则是第三方包(包括直接依赖的以及间接依赖的)引入的漏洞。

针对这两类漏洞,我们的解决方法有所不同。

第一类漏洞的解决方法十分简单,直接升级Go版本即可,比如这里我将我的Go版本从Go 1.18升级到最新的Go 1.18.6(2022.9.7日刚刚发布的)即可消除上面的所有第一类漏洞。

而第二类漏洞,即第三方包引入的漏洞,消除起来就要仔细考量一番了。

我们也分成两种情况来看:

  • 直接依赖包中存在安全漏洞

如果是项目的直接依赖包的代码中有安全漏洞,这种情况较为简单,根据govulncheck的fix提示,直接升级(go get)到对应的版本即可。

  • 间接依赖包中存在安全漏洞

假设我们的project依赖A包,而A包又依赖B包,而govulncheck恰恰扫描出B包存在漏洞,且该漏洞所在函数/方法被我们的项目通过A包调用了。这时我们该如何fix呢?

我们可以直接升级B包的版本吗?不确定!这与go module的依赖管理机制有关,go module正确管理的前提是所有包的版本真正符合semver(语义版本)规范。如果B包没有完全遵守semver规范,一旦单独升级B包版本,这很可能导致A包无法使用升级后的B包而致使我们的项目无法编译通过。在这种情况下,我们应该首先考虑升级A包,如果A包是我们自己可控的基础库,比如common之类的,我们应该先消除A包的漏洞(顺便升级了B包的版本),然后通过升级A包版本来消除这样的漏洞情况。

如果A包并非我们可控的包,而是某个公共开源包,那也要先查找A包是否已经发布了修复B包漏洞的新版本,如果找到了,直接升级A包到新版本即可解决问题。

如果A包没有修复B包的漏洞,那么问题就略复杂了。我们可以尝试升级B包来修复,如果依旧无法修复,那么我们要么给A包提PR,要么fork一份A,自己修复并直接依赖fork后的A。

如果这种间接依赖链比较长,那么修正这样的漏洞的确比较繁琐,大家务必要有耐心地从直接依赖包逐层向下升级依赖包版本。


govulncheck工具的推出丰富了我们对项目进行安全漏洞检查的手段。如果你的项目在github上开源的话,还可以使用github每周security alert来获取安全漏洞信息(如下图所示这样):

并且github提供了很便利的一键fix的方案。

对于公司内的私有商业项目,不管你之前用什么工具对软件供应链进行安全扫描,现在我们有了govulncheck,建议定期用它扫描一下。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats