initial
This commit is contained in:
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3 shadow
|
||||||
|
|
||||||
|
COPY --chmod=0755 entrypoint.py /bin/entrypoint.py
|
||||||
|
|
||||||
|
VOLUME /etc/sysconfig.toml
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/entrypoint.py"]
|
||||||
|
|
||||||
|
CMD ["/bin/sh"]
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2026 Dmitry Fedotov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
18
Makefile
Normal file
18
Makefile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.PHONY: all build push
|
||||||
|
|
||||||
|
IMAGE_NAME := alpine-customizable
|
||||||
|
IMAGE_VERSION := v0.1.0
|
||||||
|
IMAGE_TAG := $(IMAGE_NAME):$(IMAGE_VERSION)
|
||||||
|
REMOTE_TAG = registry.uint32.ru/$(IMAGE_TAG)
|
||||||
|
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building Docker image: $(IMAGE_TAG)"
|
||||||
|
docker build -t $(IMAGE_TAG) .
|
||||||
|
|
||||||
|
push:
|
||||||
|
@echo "Pushing Docker image: $(REMOTE_TAG)"
|
||||||
|
docker tag $(IMAGE_TAG) $(REMOTE_TAG)
|
||||||
|
docker push $(REMOTE_TAG)
|
||||||
89
README.md
Normal file
89
README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Custom Alpine Image
|
||||||
|
|
||||||
|
This repository contains the necessary files to build a customizable Alpine Linux Docker image. The image can be configured at runtime by providing a `sysconfig.toml` file.
|
||||||
|
|
||||||
|
## How to build the image
|
||||||
|
|
||||||
|
To build the Docker image, run the following command from the root of this repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t alpine-customizable .
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to configure the image
|
||||||
|
|
||||||
|
You can configure the container at runtime by mounting a `sysconfig.toml` file at `/etc/sysconfig.toml`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run -it --rm -v ./sysconfig.toml:/etc/sysconfig.toml alpine-customizable
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sysconfig.toml` file supports the following sections for configuration:
|
||||||
|
|
||||||
|
### `[general]`
|
||||||
|
|
||||||
|
This section is used for general system-wide settings.
|
||||||
|
|
||||||
|
- `packages`: A list of strings specifying additional Alpine packages to install using `apk add`.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
```toml
|
||||||
|
[general]
|
||||||
|
packages = ["openssh-server", "curl"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `[users]`
|
||||||
|
|
||||||
|
This section allows you to define users that will be created on the container. Each user is defined in a sub-section using the format `[users.username]`.
|
||||||
|
|
||||||
|
The following keys are supported for each user:
|
||||||
|
|
||||||
|
- `password` (optional): A string to set the user's password.
|
||||||
|
- `pubkeys` (optional): A list of public SSH keys (strings) to add to the user's `~/.ssh/authorized_keys` file, enabling key-based authentication.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
```toml
|
||||||
|
[users.dmitry]
|
||||||
|
password = "a-secure-password"
|
||||||
|
pubkeys = [
|
||||||
|
"ssh-rsa AAAA...",
|
||||||
|
"ssh-ed25519 AAAA..."
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `[groups]`
|
||||||
|
|
||||||
|
This section allows you to define groups and manage their members. Each group is defined in a sub-section using the format `[groups.groupname]`.
|
||||||
|
|
||||||
|
The following keys are supported for each group:
|
||||||
|
|
||||||
|
- `users`: A list of usernames to be added to this group. These users should typically be defined in the `[users]` section or already exist on the system.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
```toml
|
||||||
|
[groups.sftp-users]
|
||||||
|
users = ["dmitry"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `[configs]`
|
||||||
|
|
||||||
|
This section allows you to create arbitrary configuration files on the container's filesystem. Each file is defined in a sub-section where the name is the full, quoted path to the file, e.g., `[configs."/etc/motd"]`.
|
||||||
|
|
||||||
|
The following keys are supported for each file:
|
||||||
|
|
||||||
|
- `body` (required): A string (often a multi-line string) containing the content of the file.
|
||||||
|
- `permissions` (optional): An integer representing the file permissions in standard Linux octal notation (e.g., `644`, `755`).
|
||||||
|
- `owner` (optional): A string in `"user:group"` format to set the file's ownership.
|
||||||
|
|
||||||
|
*Example:*
|
||||||
|
```toml
|
||||||
|
[configs."/etc/ssh/sshd_config.d/sftp.conf"]
|
||||||
|
body = """
|
||||||
|
Match group sftp-users
|
||||||
|
ChrootDirectory /chroot
|
||||||
|
ForceCommand internal-sftp
|
||||||
|
AllowTcpForwarding no
|
||||||
|
"""
|
||||||
|
permissions = 644
|
||||||
|
owner = "root:root"
|
||||||
|
```
|
||||||
156
entrypoint.py
Normal file
156
entrypoint.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
SYSCONFIG_PATH = "/etc/sysconfig.toml"
|
||||||
|
|
||||||
|
# Config sections
|
||||||
|
SECTION_GENERAL = "general"
|
||||||
|
SECTION_USERS = "users"
|
||||||
|
SECTION_GROUPS = "groups"
|
||||||
|
SECTION_CONFIGS = "configs"
|
||||||
|
|
||||||
|
# Config keys
|
||||||
|
KEY_PACKAGES = "packages"
|
||||||
|
KEY_PASSWORD = "password"
|
||||||
|
KEY_PUBKEYS = "pubkeys"
|
||||||
|
KEY_USERS_LIST = "users"
|
||||||
|
KEY_BODY = "body"
|
||||||
|
KEY_PERMISSIONS = "permissions"
|
||||||
|
KEY_OWNER = "owner"
|
||||||
|
|
||||||
|
|
||||||
|
class SystemConfigurator:
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.config_path = path
|
||||||
|
|
||||||
|
def _parse_config(self):
|
||||||
|
with open(self.config_path, "rb") as f:
|
||||||
|
self.config = tomllib.load(f)
|
||||||
|
|
||||||
|
def _exec_command(self, command: list[str], quiet=False) -> int:
|
||||||
|
return subprocess.run(command, capture_output=quiet).returncode
|
||||||
|
|
||||||
|
def _install_packages(self):
|
||||||
|
packages = self.config.get(SECTION_GENERAL, {}).get(KEY_PACKAGES, [])
|
||||||
|
if not packages:
|
||||||
|
print("No packages to install.")
|
||||||
|
return
|
||||||
|
print(f"Installing packages: {', '.join(packages)}")
|
||||||
|
command = ["apk", "add", "--no-cache"] + packages
|
||||||
|
if self._exec_command(command) != 0:
|
||||||
|
raise RuntimeError(f"Failed to install packages: {', '.join(packages)}")
|
||||||
|
print("Packages installed successfully.")
|
||||||
|
|
||||||
|
def _create_users(self):
|
||||||
|
users = self.config.get(SECTION_USERS, {})
|
||||||
|
if not users:
|
||||||
|
print("No users to create.")
|
||||||
|
return
|
||||||
|
for username, user_config in users.items():
|
||||||
|
print(f"Creating user: {username}")
|
||||||
|
if self._exec_command(["useradd", "-m", "-s", "/bin/sh", username]) != 0:
|
||||||
|
raise RuntimeError(f"Failed to create user: {username}")
|
||||||
|
if KEY_PASSWORD in user_config:
|
||||||
|
print(f"Setting password for user: {username}")
|
||||||
|
proc = subprocess.run(
|
||||||
|
["chpasswd"],
|
||||||
|
input=f"{username}:{user_config[KEY_PASSWORD]}".encode(),
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to set password for user: {username}: {proc.stderr.decode()}"
|
||||||
|
)
|
||||||
|
if KEY_PUBKEYS in user_config:
|
||||||
|
print(f"Adding SSH key for user: {username}")
|
||||||
|
ssh_dir = os.path.join("/home", username, ".ssh")
|
||||||
|
os.makedirs(ssh_dir, mode=0o700, exist_ok=True)
|
||||||
|
authorized_keys_path = os.path.join(ssh_dir, "authorized_keys")
|
||||||
|
with open(authorized_keys_path, "w") as f:
|
||||||
|
for key in user_config[KEY_PUBKEYS]:
|
||||||
|
f.write(key + "\n")
|
||||||
|
os.chmod(authorized_keys_path, 0o600)
|
||||||
|
shutil.chown(ssh_dir, user=username, group=username)
|
||||||
|
shutil.chown(authorized_keys_path, user=username, group=username)
|
||||||
|
print(f"User {username} created successfully.")
|
||||||
|
|
||||||
|
def _create_groups(self):
|
||||||
|
groups = self.config.get(SECTION_GROUPS, {})
|
||||||
|
if not groups:
|
||||||
|
print("No groups to create.")
|
||||||
|
return
|
||||||
|
for groupname, group_config in groups.items():
|
||||||
|
if self._exec_command(["getent", "group", groupname], quiet=True) == 0:
|
||||||
|
print(f"Group '{groupname}' already exists, skipping creation.")
|
||||||
|
else:
|
||||||
|
print(f"Creating group: {groupname}")
|
||||||
|
if self._exec_command(["groupadd", groupname]) != 0:
|
||||||
|
raise RuntimeError(f"Failed to create group: {groupname}")
|
||||||
|
|
||||||
|
users = group_config.get(KEY_USERS_LIST, [])
|
||||||
|
for username in users:
|
||||||
|
print(f"Adding user {username} to group {groupname}")
|
||||||
|
if self._exec_command(["gpasswd", "-a", username, groupname]) != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to add user {username} to group {groupname}"
|
||||||
|
)
|
||||||
|
print(f"Group '{groupname}' configured successfully.")
|
||||||
|
|
||||||
|
def _create_configs(self):
|
||||||
|
configs = self.config.get(SECTION_CONFIGS, {})
|
||||||
|
if not configs:
|
||||||
|
print("No configs to create.")
|
||||||
|
return
|
||||||
|
for file_path, config in configs.items():
|
||||||
|
print(f"Creating config file: {file_path}")
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
if KEY_BODY not in config:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Config file '{file_path}' is missing the mandatory '{KEY_BODY}' key."
|
||||||
|
)
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
f.write(config[KEY_BODY])
|
||||||
|
if KEY_PERMISSIONS in config:
|
||||||
|
os.chmod(file_path, int(str(config[KEY_PERMISSIONS]), 8))
|
||||||
|
if KEY_OWNER in config:
|
||||||
|
owner_val = config[KEY_OWNER]
|
||||||
|
if ":" not in str(owner_val):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Config file '{file_path}' has an invalid '{KEY_OWNER}' format. Expected 'user:group'."
|
||||||
|
)
|
||||||
|
user, group = str(owner_val).split(":", 1)
|
||||||
|
shutil.chown(file_path, user=user, group=group)
|
||||||
|
print(f"Config file {file_path} created successfully.")
|
||||||
|
|
||||||
|
def process(self) -> int:
|
||||||
|
steps = [
|
||||||
|
(self._parse_config, 1),
|
||||||
|
(self._install_packages, 2),
|
||||||
|
(self._create_users, 3),
|
||||||
|
(self._create_groups, 4),
|
||||||
|
(self._create_configs, 5),
|
||||||
|
]
|
||||||
|
|
||||||
|
for func, code in steps:
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
return code
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
configurator = SystemConfigurator(SYSCONFIG_PATH)
|
||||||
|
exit_code = configurator.process()
|
||||||
|
if exit_code != 0:
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
os.execvp(sys.argv[1], sys.argv[1:])
|
||||||
30
sysconfig.toml
Normal file
30
sysconfig.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[ general ]
|
||||||
|
packages = [
|
||||||
|
"openssh-server",
|
||||||
|
]
|
||||||
|
|
||||||
|
[ users.dmitry ]
|
||||||
|
password = "1234"
|
||||||
|
pubkeys = [
|
||||||
|
"the key",
|
||||||
|
"the second key",
|
||||||
|
]
|
||||||
|
|
||||||
|
[ users.nadia ]
|
||||||
|
password = "2345"
|
||||||
|
pubkeys = [
|
||||||
|
"the key2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[groups.sftp-users]
|
||||||
|
users = ["dmitry"]
|
||||||
|
|
||||||
|
[configs."/etc/ssh/sshd_config.d/sftp.conf"]
|
||||||
|
body = """
|
||||||
|
Match group sftp-users
|
||||||
|
ChrootDirectory /chroot
|
||||||
|
ForceCommand internal-sftp
|
||||||
|
AllowTcpForwarding no
|
||||||
|
"""
|
||||||
|
permissions = 644
|
||||||
|
owner = "root:root"
|
||||||
Reference in New Issue
Block a user