Ubuntu Core and Snap Architecture Analysis

概述

Ubuntu Core 是一个以应用为中心的嵌入式操作系统,所有组件均以 snap容器化封装,空间占用小、安全加固并可以组合的系统镜像,另外所有 App 支持原子化的 OTA 更新(即如果更新失败可以自动回滚)。

整体架构

整体架构如下图所示:

图片来源:Ubuntu Core 手册

图片中的组件可以重用组合成定制的 Ubuntu Core 镜像。

  • Kernel snap: 内核及其关联的模块、固件和设备树文件。
  • Core snap: 标准的或者定制的 Ubuntu 根文件系统。
  • Gadget snap: 已经容器化的硬件相关的启动资产和配置文件。
  • System snaps:
  • Application snaps:
  • Snapd: 系统监护服务,该服务公开了便于进行设备管理的 REST API。

Snap

Snap 是 Linux 系统软件包格式(其他格式有 debian、rpm等),可以被用于桌面、云和 IoT 多种场景。Snap 是自包含的(比如依赖的共享库等)、易安装、安全、跨平台和免依赖的。

使用

在使用 snap 命令之前需要安装并启动snapd,Ubuntu 系统默认已经安装。

snap 命令可以用来做一些通用的软件包管理操作,具体用法参考 man snap。其中每个 Snap 都有若干个和发布相关的频道(Channels),用户可以选择从不同的频道安装(不同版本或者相同版本)snap,比如latest/beta,用于测试或者漏洞分析等。

接口(interface)

接口(interface)用于对 Snap 进行隔离限制,包括 Snap 应该做什么,不应该做什么,更重要的是对外部资源(如网络、文件系统、声卡等)进行访问控制。Snap 可以请求某个接口,也可以提供某个接口,因此接口实际上也描述了系统中 Snap 之间的依赖关系和数据交互方式。
从另一方面说,Snap 只能通过系统或者其他 Snap 提供的接口来访问相应的资源。

每个接口定义了插口(plug)和插槽(Slot) 的连接关系,每一个 Slot 可以多个 plug,Slot 可以看作资源的提供者,对应地,Plug 则可以被将看作资源的消费者。Plug 和 Slot 只有当它们有着相同的接口名时才能被连接。

使用命令 snap connections <snap-name> 可以查看该 Snap 正在使用的接口,以及哪些接口的 Plug 和 Slot 已经连接,哪些没有连接。

连接可以在安装 Snap 时自动建立,也可以使用命令 snap connect <snap>:<plug interface> [<snap>:<slot interface>] 手动建立,由 Snap 的具体功能决定。相应地,可以使用 snap disconnect <snap>:<plug interface> 来移除连接。

用户可以使用 snap interface <interface-name> 查看哪些 Snaps 在使用指定的接口。

1
2
3
4
5
6
7
8
9
$snap interface home  
name: home
summary: allows access to non-hidden files in the home directory
documentation: https://snapcraft.io/docs/home-interface
plugs:
- firefox
- vlc
slots:
- snapd

存储数据

大多数 Snap 的受限模式都是严格的(strict)。这将 Snap 的执行环境和数据与系统隔离开来,一个需要用户访问文件的受限 Snap 很可能会使用 Home Interface 来弥合这种限制差距,允许 Snap 自动从主目录保存和加载文件。

每个 Snap 的数据可以以Snap 的形式保存,数据实际上可以被看作是 Snap 的状态。因此,我们可以通过Snap (snapshot)来备份、回滚 Snap 的状态。
每一个 Snap 被移除的时候可以自动生成Snap ,也可以使用 snap save <snap-name> 命令手动生成。回滚时使用 snap restore <#n>命令,最后一个参数为Snap 编号。

其中一种数据是 Snap 的配置数据,这些配置数据可以使用 snap get <snap-name> [option] 来读取;对应地,可以使用snap set <snap-name> <option> <value>
特别的,对于 system snap 配置选项和系统相关,其中一部分会影响 Snapd 的行为。示例如下:

1
2
3
4
5
$ sudo snap get system system
Key Value
system.hostname mimose-MDG-XX
system.network {...}
system.timezone UTC

更多信息参考 Data locations

配额 (quotas)

安全

Snap 被限制在一个安全沙箱中,无法访问 Snap 之外的系统资源。安全策略和存储策略协同工作,使开发人员能够快速更新他们的应用程序,并为最终用户提供安全保障。

Snap 运行时,要完成下面的步骤(其中和内核安全机制,如 seccompAppArmor 紧密相关):

  1. 设置各种环境变量:

    • HOME:将所有命令设置为SNAP_USER_DATA
    • SNAP:只读安装目录
    • SNAP_ARCH:设备的架构(例如amd64、arm64、armhf、i386等)
    • SNAP_DATA:Snap 特定版本的可写区域
    • SNAP_COMMON:Snap 所有版本中通用的可写区域
    • SNAP_LIBRARY_PATH:应添加到LD_LIBRARY_PATH的其他目录
    • SNAP_NAME:Snap 名称
    • SNAP_INSTANCE_NAME:Snap 实例名称,包括实例密钥(如果已设置)(Snap 2.36+)
    • SNAP_INSTANCE_KEY:实例密钥(如果有)(Snap 2.36+)
    • SNAP_REVISION:存储Snap 的修订
    • SNAP_USER_DATA:特定Snap 版本的每个用户可写区域
    • SNAP_USER_COMMON:所有Snap 版本中通用的每个用户可写区域
    • SNAP_VERSION:Snap 版本(来自SNAP.yaml)
  2. 当硬件分配给 Snap 时,将使用默认设备(例如/dev/null/dev/urandom等)和分配给此 Snap 的任何设备设置 cgroup 设备组。硬件被分配了接口连接。

  3. 设置一个在 Snap 中的所有命令之间共享的私有挂载命名空间。

  4. 使用 per-snap 私有挂载命名空间设置私有 /tmp 目录,并在/tmp上装载 per-snap 目录。

  5. 为每个命令设置一个新的 devpts 实例。

  6. 为命令设置seccomp过滤器。

  7. 在命令特定的AppArmor配置文件下,以默认的nice值执行命令。

安装 Snap 后,会检查其元数据,并将其与传统权限一起用于导出AppArmor配置文件、Seccomp过滤器和 cgroup 规则。这种组合提供了强大的应用程序限制和隔离。

AppArmor、Seccomp 和设备权限

  • Apparmor

    为每个命令生成AppArmor配置文件。这些具有适当的安全标签和特定于命令的AppArmor规则,用于协调文件访问、应用程序执行、Linux功能、挂载、ptrace、IPC、信号、粗粒度网络。

    如前所述,每个命令都在特定于应用程序的默认策略下运行,该策略可以通过声明的接口进行扩展,这些接口在元数据中表示为 Plug 和 Slot。Strict 模式 Snap 中违反AppArmor策略的行为将被拒绝访问,并且通常将errno设置为EACCES。违规行为通常会被记录下来。

  • Seccomp

    为 Snap 中要运行的每个命令生成一个seccomp过滤器,启用allowlist系统调用过滤,然后可以通过元数据中表示为 Plug 和 Slot 的声明接口进行扩展。

    违反seccomp策略的进程将被拒绝访问系统调用,errno设置为EPERM(2.32之前的 Snap 版本接收SIGSYS),并记录违规行为。

  • 设备 Cgroup

    为每个标记设备的命令生成udev规则,以便可以将它们添加/删除到命令的设备cgroup中。但是,默认情况下,不会标记任何设备,也不会使用设备cgroup,而是使用AppArmor来协调访问。

    根据snapd的决定,当声明依赖接口时,除了AppArmor外,还可以使用设备cgroup,如元数据中的 Plug 和 Slot 所示。

    访问不在 Snap 特定 cgroup 中的设备的进程将被拒绝访问,errno设置为EPERM。访问违规不会被记录。

  • 传统权限

    传统的文件权限(所有者、组、文件ACL等)也通过 Snap 强制执行。 尝试访问传统文件权限不允许的资源的进程将被拒绝访问,errno通常设置为EACCES(有关详细信息,请参阅操作手册页)。访问违规不会被记录。

因此,所有 Snap 都在默认安全策略下运行,该策略可以通过使用 接口(interface) 进行扩展,还句话说,当进行接口连接时,安全策略可以进行传递或者说扩展。

限制 (Confinement)

Snap 限制决定了应用程序对系统资源(如文件、网络、外围设备和服务)的访问量。有几个级别的限制。

限制确保单个软件不会影响用户系统的健壮性,也不会导致其他应用程序出现问题。因此,当用户运行 Snap 时,它提供的软件在一定程度上与系统隔离,默认情况下限制了对严格最低功能的访问。

Snap 的限制级别控制其与用户系统的隔离程度。应用程序开发人员或打包人员可以调整限制级别,以广义地指定应用程序在正常使用或开发过程中需要多少对系统资源的访问权限。

已发布 Snap 有两个级别的 Snap 限制:

  • Strict:被大多数 Snap 使用。Strict 限制的 Snap 在完全隔离的情况下运行,最高可达被认为始终安全的最低访问级别。因此,Strict 限制的 Snap 无法访问文件、网络、进程或任何其他系统资源,除非通过接口请求特定的访问权限(见下文)。
  • Classic:允许以与传统软件包几乎相同的方式访问系统资源。为了防止滥用,发布 Classic Snap 需要手动批准,安装需要--Classic命令行参数。

在开发过程中,另一种模式很有用:

  • Devmode:为 Snap 创建者和开发人员提供的特殊模式。devmode Snap 作为严格受限的 Snap 运行,具有对系统资源的完全访问权限,并产生调试输出以识别未指定的接口。安装需要--devmode命令行参数。Devmode Snap 无法发布到 stable 频道,不会出现在搜索结果中,也不会自动更新。

Strict 限制的 Snap 会使用Linux内核的安全功能,包括AppArmorseccomp和命名空间,以防止应用程序和服务访问更广泛的系统。

缺点

Snap 的一部分问题源于中心化和不透明的 App Store,与开源精神不符而受到质疑和批评,(可以参考为什么Ubuntu的Snap是不受欢迎的)。另外其在性能上的问题也遭到批评,参考 Snap 官方对性能的描述:Snap performance

SquashFS 文件系统

SquashFS 是一个标准 Linux 文件系统,它将整个目录结构打包到单个压缩文件中。它经常被用于在 USB 设备上提供可引导的原生 Linux 环境,当然也可以用到打包 Snap。

Snap 是一个 SquashFS 文件,其中包含 Snap 的库和二进制环境,以及描述其访问和功能的元数据。SquashFS 文件要么在首次安装 Snap 时由 systemd挂载,要么在已经安装 Snap 的系统(如Ubuntu Core)启动的早期阶段挂载。

SquashFS 解压缩发生在 Snap 首次在系统上运行并且其性能受到密切监视时。特别是,解压缩性能可能因使用的压缩算法、SquashFS 存档的大小、它包含的文件数量、它是第一次被访问(冷缓存)还是再次被访问(热缓存)而有所不同。

安全

安全特性

a. 容器化带来的安全性

Snaps 是不可修改的,默认遵循“最小化特权”原则,这使得构建防篡改设备非常容易。Snaps 之间的通信和交互和传统的应用非常不同,抽象地说,Snaps 构建了一个基于事务的系统。

b. 安全启动

c. 全盘加密

d. 基于内核安全机制的限制

第三方安全分析

实践:Ubuntu Core镜像制作

注册 Ubuntu One 账号

注册 Ubuntu One 账号,最重要的是使用 Ubuntu 的平台(snapcraft.io)进行 Snap 包的在线生命周期管理,包括包的发布、更新、回退和卸载等。
如下图所示,可以看到当前账号所管理的 snaps:

为了保证构建的 Ubuntu Core 镜像以及 snap包的合法性,还需要生成 snapcraft 证书,持有该证书的用户才能对 snap 包拥有限制的权限。创建证书时会要求输入 Ubuntu One 的账户和口令。

1
2
3
4
5
6
7
$ sudo snap install snapcraft --classic
$ snapcraft export-login credentials.txt
$ export SNAPCRAFT_STORE_CREDENTIALS=$(cat credentials.txt)

# 获取相关信息
$ snapcraft whoami

创建模型

制作定制 Ubuntu Core 镜像的核心是编写模型断言(model assertion)。一个 assertion 可以看作是一个被签名的 JSON 的文本,该文本描述了组成镜像的各个组件。模型包括的信息如下:

  • 身份 ID 信息。
  • 组成设备系统的基础 snaps (essential snap)。
  • 为了实现设备的功能强制要求或者可选的 snaps。

为了快速创建模型,可以直接从 snapcore github 仓库下载对应设备的参考模型文件。以 raspberry pi 平台为例:
$ wget -O my-model.json https://raw.githubusercontent.com/snapcore/models/master/ubuntu-core-24-pi-arm64.json

编辑 my-model.json, 修改authority-idbrand-id 为自己的开发者 ID(developer ID),该 ID 可以在命令中snapcraft whoami 获取到。这样做在逻辑上将该镜像与自己的 Ubuntu One 账号绑定,也就是只有开发者自己可以 push 镜像。

1
2
"authority-id": "XXXXXXXXXXXX",
"brand-id": "XXXXXXXXXXXX",

snaps 描述了构建系统需要的基础 snaps(kernelcore等) 和 应用 snaps(console-conf),参考架构图。

对模型进行签名

首先确认是否已经有密钥对关联到开发者的账号:

1
2
$ snapcraft list-keys
No keys have been registered. See 'snapcraft register-key --help' to register a key.

如果没有的话,新建一个:

1
$ snapcraft create-key my-model-key

然后注册 key:

1
2
3
4
$ snapcraft register-key my-model-key
...
Registering key ...
Done. The key "my-model-key" (<key fingerprint>) may be used to sign your assertions.

重新查看 key 状态:

1
2
3
4
5
$ snapcraft list-keys
The following keys are available on this system:
Name SHA3-384 fingerprint
* my-model-key gX8JXpNUvUsAfS4IwNoxQVLeHzHqoI_ktGKdX-WRw9VlCEcTI6HNfgWdbdjvRFKm

最后对 JSON 文件进行签名生成 模型断言

1
$ snap sign -k my-model-key my-model.json > my-model.model

构建模型

安装 ubuntu-image 工具:

1
$ snap install ubuntu-image --classic

然后使用 ubuntu-image 构建镜像,并使用上一节生成的模型断言作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ubuntu-image snap my-model.model
......
[1] load_gadget_yaml
[2] set_artifact_names
[3] populate_rootfs_contents
[4] generate_disk_info
[5] calculate_rootfs_size
[6] populate_bootfs_contents
[7] populate_prepare_partitions
[8] make_disk
[9] generate_snap_manifest
Build successful

引导镜像

参考 Test Ubuntu Core with QEMU

通过qemu 创建 Ubuntu Core 虚拟机,并进入系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ qemu-system-x86_64 \
-enable-kvm \
-smp 1 \
-m 2048 \
-machine q35 \
-cpu host \
-global ICH9-LPC.disable_s3=1 \
-net nic,model=virtio \
-net user,hostfwd=tcp::8022-:22 \
-drive file=OVMF_CODE_4M.secboot.fd,if=pflash,format=raw,unit=0,readonly=on \
-drive file=OVMF_VARS_4M.ms.fd,if=pflash,format=raw,unit=1 \
-drive "file=pc.img",if=none,format=raw,id=disk1 \
-device virtio-blk-pci,drive=disk1,bootindex=1 \
-serial mon:stdio

引导过程中会弹出窗口,用户可以在窗口内进行相关配置。

访问

使用 SSH 登陆 Ubuntu Core,但 OpenSSH Server 服务器端不要求一个口令(Password),而是被配置成需要使用 SSH 公钥进行认证,该 SSH 公钥和 Ubuntu One 账户是绑定关系。

配置 SSH key 可以参考Test Ubuntu Core with QEMU

使用下面的命令登陆:

1
$ ssh -i <path-to-private-key> <sso-username>@localhost -p 8022

其中 sso-username 和 Ubutnu One 账户关联。