diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:17:13 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:34:12 +0000 |
| commit | 092aa81983335b2346a725eebd2a75fc785bb42b (patch) | |
| tree | 4cae1a055b5027c9a049004a339bc3f752cbf8f1 /classes | |
| parent | 4ed680e32e3670a4e50038387572ee7a35374c0e (diff) | |
| download | meta-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.bbclass | 166 |
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" |
| 39 | CONTAINER_REGISTRY_STORAGE ?= "${TOPDIR}/container-registry" | 41 | CONTAINER_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 | ||
| 49 | CONTAINER_REGISTRY_AUTH_MODE ?= "none" | ||
| 50 | |||
| 51 | # Path to Docker-style auth file (config.json format) | ||
| 52 | # Used when AUTH_MODE = "authfile" | ||
| 53 | CONTAINER_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 | ||
| 58 | CONTAINER_REGISTRY_CREDSFILE ?= "" | ||
| 59 | |||
| 60 | # Insecure registry mode (HTTP, no TLS) - legacy compatibility | ||
| 61 | CONTAINER_REGISTRY_INSECURE ?= "0" | ||
| 62 | |||
| 63 | # Secure registry mode (opt-in) | ||
| 64 | # When enabled, generates TLS certificates for HTTPS | ||
| 65 | CONTAINER_REGISTRY_SECURE ?= "0" | ||
| 66 | |||
| 67 | # Authentication mode (opt-in, requires SECURE=1) | ||
| 68 | # When enabled, generates htpasswd credentials | ||
| 69 | CONTAINER_REGISTRY_AUTH ?= "0" | ||
| 70 | |||
| 71 | # Credentials for auth mode (password empty = auto-generate) | ||
| 72 | CONTAINER_REGISTRY_USERNAME ?= "yocto" | ||
| 73 | CONTAINER_REGISTRY_PASSWORD ?= "" | ||
| 74 | |||
| 75 | # Certificate validity periods | ||
| 76 | CONTAINER_REGISTRY_CERT_DAYS ?= "365" | ||
| 77 | CONTAINER_REGISTRY_CA_DAYS ?= "3650" | ||
| 78 | |||
| 79 | # Custom SAN entries (auto-includes localhost, 127.0.0.1, 10.0.2.2, registry host) | ||
| 80 | CONTAINER_REGISTRY_CERT_SAN ?= "" | ||
| 81 | |||
| 82 | # Path to CA certificate (auto-set in secure mode) | ||
| 83 | CONTAINER_REGISTRY_CA_CERT ?= "${CONTAINER_REGISTRY_STORAGE}/pki/ca.crt" | ||
| 84 | |||
| 41 | # Require skopeo-native for registry operations | 85 | # Require skopeo-native for registry operations |
| 42 | DEPENDS += "skopeo-native" | 86 | DEPENDS += "skopeo-native" |
| 43 | 87 | ||
| 88 | # Validate conflicting settings at parse time | ||
| 89 | python __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 | |||
| 104 | def _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 | |||
| 128 | def _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 | |||
| 44 | def container_registry_generate_tags(d, image_name): | 190 | def 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}/") |
