summaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:17:13 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:34:12 +0000
commit092aa81983335b2346a725eebd2a75fc785bb42b (patch)
tree4cae1a055b5027c9a049004a339bc3f752cbf8f1 /classes
parent4ed680e32e3670a4e50038387572ee7a35374c0e (diff)
downloadmeta-virtualization-092aa81983335b2346a725eebd2a75fc785bb42b.tar.gz
container-registry: add secure registry infrastructure with TLS and auth
Add opt-in secure registry mode with auto-generated TLS certificates and htpasswd authentication. New BitBake variables: CONTAINER_REGISTRY_SECURE - Enable TLS (HTTPS) for local registry CONTAINER_REGISTRY_AUTH - Enable htpasswd auth (requires SECURE=1) CONTAINER_REGISTRY_USERNAME/PASSWORD - Credential configuration CONTAINER_REGISTRY_CERT_DAYS/CA_DAYS - Certificate validity CONTAINER_REGISTRY_CERT_SAN - Custom SAN entries The bbclass validates conflicting settings (AUTH without SECURE) and provides credential helper functions for skopeo push operations. PKI infrastructure (CA + server cert with SAN) is auto-generated at bitbake build time via openssl-native. The generated helper script supports both TLS-only and TLS+auth modes. The script now supports environment variable overrides for CONTAINER_REGISTRY_STORAGE, CONTAINER_REGISTRY_URL, and CONTAINER_REGISTRY_NAMESPACE, uses per-port PID files to allow multiple instances, and auto-generates config files when running from an overridden storage path. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/container-registry.bbclass166
1 files changed, 165 insertions, 1 deletions
diff --git a/classes/container-registry.bbclass b/classes/container-registry.bbclass
index b0a26365..30a95034 100644
--- a/classes/container-registry.bbclass
+++ b/classes/container-registry.bbclass
@@ -23,6 +23,8 @@
23# CONTAINER_REGISTRY_TLS_VERIFY = "false" # TLS verification 23# CONTAINER_REGISTRY_TLS_VERIFY = "false" # TLS verification
24# CONTAINER_REGISTRY_TAG_STRATEGY = "timestamp latest" # Tag generation 24# CONTAINER_REGISTRY_TAG_STRATEGY = "timestamp latest" # Tag generation
25# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/container-registry" # Persistent storage 25# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/container-registry" # Persistent storage
26# CONTAINER_REGISTRY_SECURE = "1" # Enable TLS (HTTPS)
27# CONTAINER_REGISTRY_AUTH = "1" # Enable htpasswd auth (requires SECURE=1)
26# 28#
27# =========================================================================== 29# ===========================================================================
28 30
@@ -38,9 +40,153 @@ CONTAINER_REGISTRY_TAG_STRATEGY ?= "timestamp latest"
38# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/../container-registry" 40# CONTAINER_REGISTRY_STORAGE = "${TOPDIR}/../container-registry"
39CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry" 41CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry"
40 42
43# Authentication configuration
44# Modes: "none" (default), "home", "authfile", "credsfile"
45# none - No authentication (local/anonymous registries)
46# home - Use ~/.docker/config.json (opt-in, like BB_USE_HOME_NPMRC)
47# authfile - Explicit path to Docker-style config.json
48# credsfile - Simple key=value credentials file
49CONTAINER_REGISTRY_AUTH_MODE ?= "none"
50
51# Path to Docker-style auth file (config.json format)
52# Used when AUTH_MODE = "authfile"
53CONTAINER_REGISTRY_AUTHFILE ?= ""
54
55# Path to simple credentials file (key=value format)
56# Used when AUTH_MODE = "credsfile"
57# File contains: CONTAINER_REGISTRY_USER + CONTAINER_REGISTRY_PASSWORD, or CONTAINER_REGISTRY_TOKEN
58CONTAINER_REGISTRY_CREDSFILE ?= ""
59
60# Insecure registry mode (HTTP, no TLS) - legacy compatibility
61CONTAINER_REGISTRY_INSECURE ?= "0"
62
63# Secure registry mode (opt-in)
64# When enabled, generates TLS certificates for HTTPS
65CONTAINER_REGISTRY_SECURE ?= "0"
66
67# Authentication mode (opt-in, requires SECURE=1)
68# When enabled, generates htpasswd credentials
69CONTAINER_REGISTRY_AUTH ?= "0"
70
71# Credentials for auth mode (password empty = auto-generate)
72CONTAINER_REGISTRY_USERNAME ?= "yocto"
73CONTAINER_REGISTRY_PASSWORD ?= ""
74
75# Certificate validity periods
76CONTAINER_REGISTRY_CERT_DAYS ?= "365"
77CONTAINER_REGISTRY_CA_DAYS ?= "3650"
78
79# Custom SAN entries (auto-includes localhost, 127.0.0.1, 10.0.2.2, registry host)
80CONTAINER_REGISTRY_CERT_SAN ?= ""
81
82# Path to CA certificate (auto-set in secure mode)
83CONTAINER_REGISTRY_CA_CERT ?= "${CONTAINER_REGISTRY_STORAGE}/pki/ca.crt"
84
41# Require skopeo-native for registry operations 85# Require skopeo-native for registry operations
42DEPENDS += "skopeo-native" 86DEPENDS += "skopeo-native"
43 87
88# Validate conflicting settings at parse time
89python __anonymous() {
90 secure = d.getVar('CONTAINER_REGISTRY_SECURE') == '1'
91 insecure = d.getVar('CONTAINER_REGISTRY_INSECURE') == '1'
92 auth = d.getVar('CONTAINER_REGISTRY_AUTH') == '1'
93
94 if secure and insecure:
95 bb.fatal("CONTAINER_REGISTRY_SECURE and CONTAINER_REGISTRY_INSECURE cannot both be set to '1'. "
96 "Use CONTAINER_REGISTRY_SECURE='1' for TLS, or CONTAINER_REGISTRY_INSECURE='1' for HTTP.")
97
98 if auth and not secure:
99 bb.warn("CONTAINER_REGISTRY_AUTH='1' requires CONTAINER_REGISTRY_SECURE='1'. "
100 "Authentication without TLS is insecure. Enabling SECURE mode automatically.")
101 d.setVar('CONTAINER_REGISTRY_SECURE', '1')
102}
103
104def _container_registry_parse_credsfile(filepath):
105 """Parse a simple key=value credentials file.
106
107 Returns dict with CONTAINER_REGISTRY_USER, CONTAINER_REGISTRY_PASSWORD,
108 and/or CONTAINER_REGISTRY_TOKEN.
109 """
110 creds = {}
111 with open(filepath, 'r') as f:
112 for line in f:
113 line = line.strip()
114 if not line or line.startswith('#'):
115 continue
116 if '=' in line:
117 key, value = line.split('=', 1)
118 key = key.strip()
119 value = value.strip()
120 # Remove quotes if present
121 if value.startswith('"') and value.endswith('"'):
122 value = value[1:-1]
123 elif value.startswith("'") and value.endswith("'"):
124 value = value[1:-1]
125 creds[key] = value
126 return creds
127
128def _container_registry_get_auth_args(d):
129 """Build skopeo authentication arguments based on auth mode."""
130 import os
131
132 auth_mode = d.getVar('CONTAINER_REGISTRY_AUTH_MODE') or 'none'
133 secure_mode = d.getVar('CONTAINER_REGISTRY_SECURE') == '1'
134
135 if auth_mode == 'none':
136 # In secure mode with no explicit auth, auto-use generated credentials
137 if secure_mode:
138 storage = d.getVar('CONTAINER_REGISTRY_STORAGE')
139 password_file = os.path.join(storage, 'auth', 'password')
140 if os.path.exists(password_file):
141 with open(password_file, 'r') as f:
142 password = f.read().strip()
143 username = d.getVar('CONTAINER_REGISTRY_USERNAME') or 'yocto'
144 bb.note(f"Using auto-generated credentials for secure registry (user: {username})")
145 return ['--dest-creds', f'{username}:{password}']
146 return []
147
148 if auth_mode == 'home':
149 # Use ~/.docker/config.json (opt-in, like BB_USE_HOME_NPMRC)
150 home = os.environ.get('HOME', '')
151 authfile = os.path.join(home, '.docker', 'config.json')
152 if not os.path.exists(authfile):
153 bb.fatal(f"CONTAINER_REGISTRY_AUTH_MODE='home' but {authfile} not found. "
154 f"Run 'docker login' or use 'authfile'/'credsfile' mode instead.")
155 bb.note(f"Using home Docker config for registry auth: {authfile}")
156 return ['--dest-authfile', authfile]
157
158 if auth_mode == 'authfile':
159 authfile = d.getVar('CONTAINER_REGISTRY_AUTHFILE')
160 if not authfile:
161 bb.fatal("CONTAINER_REGISTRY_AUTH_MODE='authfile' requires CONTAINER_REGISTRY_AUTHFILE")
162 if not os.path.exists(authfile):
163 bb.fatal(f"Auth file not found: {authfile}")
164 return ['--dest-authfile', authfile]
165
166 if auth_mode == 'credsfile':
167 credsfile = d.getVar('CONTAINER_REGISTRY_CREDSFILE')
168 if not credsfile:
169 bb.fatal("CONTAINER_REGISTRY_AUTH_MODE='credsfile' requires CONTAINER_REGISTRY_CREDSFILE")
170 if not os.path.exists(credsfile):
171 bb.fatal(f"Credentials file not found: {credsfile}")
172
173 creds = _container_registry_parse_credsfile(credsfile)
174
175 # Token takes precedence
176 if 'CONTAINER_REGISTRY_TOKEN' in creds:
177 return ['--dest-registry-token', creds['CONTAINER_REGISTRY_TOKEN']]
178
179 username = creds.get('CONTAINER_REGISTRY_USER')
180 password = creds.get('CONTAINER_REGISTRY_PASSWORD')
181 if username and password:
182 return ['--dest-creds', f'{username}:{password}']
183
184 bb.fatal("Credentials file must contain CONTAINER_REGISTRY_TOKEN or both "
185 "CONTAINER_REGISTRY_USER and CONTAINER_REGISTRY_PASSWORD")
186
187 bb.fatal(f"Unknown CONTAINER_REGISTRY_AUTH_MODE: {auth_mode} "
188 "(use 'none', 'home', 'authfile', or 'credsfile')")
189
44def container_registry_generate_tags(d, image_name): 190def container_registry_generate_tags(d, image_name):
45 """Generate tags based on CONTAINER_REGISTRY_TAG_STRATEGY. 191 """Generate tags based on CONTAINER_REGISTRY_TAG_STRATEGY.
46 192
@@ -159,12 +305,30 @@ def container_registry_push(d, oci_path, image_name, tags=None):
159 pushed = [] 305 pushed = []
160 src = f"oci:{oci_path}" 306 src = f"oci:{oci_path}"
161 307
308 secure_mode = d.getVar('CONTAINER_REGISTRY_SECURE') == '1'
309 storage = d.getVar('CONTAINER_REGISTRY_STORAGE')
310
162 for tag in tags: 311 for tag in tags:
163 dest = f"docker://{registry}/{namespace}/{image_name}:{tag}" 312 dest = f"docker://{registry}/{namespace}/{image_name}:{tag}"
164 313
165 cmd = [skopeo, 'copy'] 314 cmd = [skopeo, 'copy']
166 if tls_verify == 'false': 315
316 # TLS handling: secure mode uses CA cert, insecure mode disables verification
317 if secure_mode:
318 pki_dir = os.path.join(storage, 'pki')
319 if os.path.exists(os.path.join(pki_dir, 'ca.crt')):
320 cmd.extend(['--dest-cert-dir', pki_dir])
321 else:
322 bb.warn(f"Secure mode enabled but CA cert not found at {pki_dir}/ca.crt")
323 bb.warn("Run 'container-registry.sh start' to generate PKI infrastructure")
324 cmd.append('--dest-tls-verify=false')
325 elif tls_verify == 'false':
167 cmd.append('--dest-tls-verify=false') 326 cmd.append('--dest-tls-verify=false')
327
328 # Add authentication arguments if configured
329 auth_args = _container_registry_get_auth_args(d)
330 cmd.extend(auth_args)
331
168 cmd.extend([src, dest]) 332 cmd.extend([src, dest])
169 333
170 bb.note(f"Pushing {image_name}:{tag} to {registry}/{namespace}/") 334 bb.note(f"Pushing {image_name}:{tag} to {registry}/{namespace}/")