Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ A cron job is required on the host for some tasks. Make sure the cron user is pr

## Certificate Management

The master container will automatically attempt to get certificates initiall if they don't exist. Certificates will not be automatically renewed. To renew certificates, run the following command:
The master container provisions certificates before marking itself ready and checks them for renewal every five minutes. To force renewal manually, run the following commands:

```bash
# Renew certificates
docker compose exec master bash /testbed/dist/ndncert/renew.sh --force
docker compose exec master bash /testbed/dist/nlsr/renew.sh --force
docker compose exec master bash /testbed/dist/ndn-python-repo/renew.sh --force
docker compose exec master bash /testbed/dist/ndnd-repo/renew.sh --force

# Restart containers
docker compose restart nlsr ndncert serve-certs ndn-python-repo
docker compose restart nlsr ndncert serve-certs ndn-python-repo ndnd-repo
```

To get the list of currently installed certificates, run
Expand All @@ -58,11 +59,24 @@ To get the list of currently installed certificates, run
docker compose exec -e HOME=/testbed/dist/ndncert master ndnsec list -c
docker compose exec -e HOME=/testbed/dist/nlsr master ndnsec list -c
docker compose exec -e HOME=/testbed/dist/ndn-python-repo master ndnsec list -c
docker compose exec ndnd-repo /ndnd sec key-list dir:///config/keys

# For root CA only
docker compose exec -e HOME=/testbed/root-ca-home master ndnsec list -c
```

## NDNd Repository

Each node runs the pinned `ndnd-repo` container with identity `<default_prefix>/ndnd/repo` (e.g. `/ndn/edu/ucla/ndnd/repo`). Master generates a dedicated key, obtains a certificate from the local site CA, and installs the public chain selected by `globals.ndnd_repo_anchors`. The repo can write only its keychain and storage; it has no access to CA private keys or master renewal scripts.

Certificates last up to 90 days, bounded by the issuing chain, and renew with 30 days remaining. Renewal reuses the key and restarts the repo when credentials change. Failed signing preserves the installed certificate. Pending dependent renewals and service reloads survive a partially failed site renewal and retry on the next poll. Back up `dist/ndnd-repo/keys` and `dist/ndnd-repo/storage`. Group enrollment ends on restart by design; applications enroll again while stored data persists.

Before merging this change into the branch polled by nodes, publish an NDNd image containing commit `cfffe287` or later and rebuild `testbed-master` from this Dockerfile. The master needs `python-ndn==0.5.2` to compile NDNd's LVS format; the previously pinned master image and January NDNd image are incompatible. Use the two repositories' existing Docker workflows; no new CI is needed. Compose keeps the existing dated-image convention. Publish the dated images pinned in Compose before rollout; if the workflows publish on a different date, update the pins to those published tags.

Master compiles the management trust schema while rendering configuration. Any user certified directly by a configured testbed site may enroll or leave groups; user identities may have one to five components below that site's prefix. Send commands to `<site>/ndnd/repo/32=cmd`, with embedded signed Data named `<user>/32=repo/<version>`. Publish application security-config objects as `<user>/32=repo-security/<version>/<segment>`. The schema binds these packets to the user's identity and authorizes responses only from the local repo key. Applications retain control of per-group data schemas and should validate responses using the same root-to-site-to-repo rules.

The active root expires on December 31, 2026. Before expiry, distribute its replacement, add it to `ndnd_repo_anchors`, and renew site certificates before removing the old root. Root rollover is separate from automatic site/repo renewal.

## Development

For debugging and development, you can define `DEBUG=1` in your `.env` file. This will prevent the `dist` folder from auto-rendering and disable git polling. You can then use docker compose as usual to manage the containers.
Expand Down
9 changes: 8 additions & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ services:
template_path: templates/ndn-python-repo
render_path: dist/ndn-python-repo

ndnd-repo:
template_path: templates/ndnd-repo
render_path: dist/ndnd-repo

ndncert:
template_path: templates/ndncert
render_path: dist/ndncert
Expand All @@ -61,6 +65,9 @@ globals:
supports_guest_certs: False
operator_user: ndnops
root_ca_https: https://suns.cs.ucla.edu/root-ca
# Explicitly select the active root; retain additional roots here during rollover.
ndnd_repo_anchors:
- anchors/ndn-testbed-root.ndncert.2204.base64

# Declare variables available for each host
# You can use lint.py to check variables, their types and
Expand Down Expand Up @@ -88,4 +95,4 @@ variables:
hr_radius: number
ethernet_device: string
is_root_ca: "?boolean"
is_status_page: "?boolean"
is_status_page: "?boolean"
26 changes: 24 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
name: testbed
services:
master:
image: ghcr.io/named-data/testbed-master:20260423
image: ghcr.io/named-data/testbed-master:20261012
init: true
tty: true
volumes:
Expand Down Expand Up @@ -153,6 +153,28 @@ services:
logging:
driver: local

ndnd-repo:
image: ghcr.io/named-data/ndnd:20261012
init: true
volumes:
- /run/nfd:/run/nfd:ro
- ${PWD}/dist/ndnd-repo/keys:/config/keys
- ${PWD}/dist/ndnd-repo/storage:/config/storage
- ${PWD}/dist/ndnd-repo/repo.yml:/config/repo.yml:ro
- ${PWD}/dist/ndnd-repo/management-schema.tlv:/config/management-schema.tlv:ro
environment:
NDN_CLIENT_TRANSPORT: unix:///run/nfd/nfd.sock
command: [repo, run, /config/repo.yml]
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
restart: unless-stopped
depends_on:
nfd: { condition: service_healthy }
serve-certs: { condition: service_started }
logging:
driver: local

ndncert:
image: ghcr.io/ucla-irl/ndncert-ca:20250923
init: true
Expand All @@ -170,7 +192,7 @@ services:
driver: local

root-http-ca:
image: ghcr.io/named-data/testbed-master:20260423
image: ghcr.io/named-data/testbed-master:20261012
profiles: [root-ca] # in .env
init: true
tty: true
Expand Down
19 changes: 15 additions & 4 deletions framework/internal/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import yaml
import jinja2
from ndn.app_support.light_versec import compile_lvs

from . import conf
from . import compose
Expand All @@ -14,6 +15,8 @@
'oneline': lambda text: ''.join(text.splitlines()),
'list': lambda obj: list(obj),
'str': lambda obj: str(obj),
'ndn_cert_name': utils.ndn_cert_name,
'ndn_lvs_name': utils.ndn_lvs_name,
}

def render(node_name: str, dry: bool = False) -> None:
Expand Down Expand Up @@ -97,6 +100,16 @@ def render(node_name: str, dry: bool = False) -> None:
with open(template_path, 'r') as f:
output_content = f.read()

# Compile LVS templates before a changed service is restarted.
if output_basename.endswith('.lvs'):
output_basename = os.path.splitext(output_basename)[0] + '.tlv'
schema = compile_lvs(output_content)
if schema.version != 0x00011000:
raise RuntimeError('NDNd requires LVS 1.1; rebuild master with python-ndn 0.5.2')
output_content = bytes(schema.encode())
else:
output_content = output_content.encode()

# Get relative path of template to base directory
relative_path = os.path.dirname(os.path.relpath(template_path, base_template_path))

Expand All @@ -110,15 +123,13 @@ def render(node_name: str, dry: bool = False) -> None:
# Read the existing file if it exists
existing_content = None
if pathlib.Path(output_path).is_file():
with open(output_path, 'r') as f:
existing_content = f.read()
existing_content = pathlib.Path(output_path).read_bytes()

# Check if the content has changed
if existing_content != output_content:
print(f"File {output_path} has changed.", file=sys.stderr)
service_changed = True
with open(output_path, 'w') as f:
f.write(output_content)
pathlib.Path(output_path).write_bytes(output_content)

# Skip service checkers on dry run
if dry:
Expand Down
16 changes: 16 additions & 0 deletions framework/internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ def hash_file(path: str) -> str:
"""
return hashlib.md5(open(path, 'rb').read()).hexdigest()

def ndn_cert_name(text: str) -> str:
"""Read the full certificate name from an ndn-cxx base64 certificate."""
import base64
from ndn.app_support.security_v2 import parse_certificate
from ndn.encoding import Name

wire = base64.b64decode(''.join(text.split()), validate=True)
return Name.to_str(parse_certificate(wire).name)

def ndn_lvs_name(name: str) -> str:
"""Encode an NDN name as literal components in an LVS schema."""
import json
from ndn.encoding import Component, Name

return '/'.join(json.dumps(Component.to_str(comp)) for comp in Name.normalize(name))

def read_dotenv(file='.env') -> dict[str, str]:
"""
This function reads a .env file and returns a dictionary of the key-value
Expand Down
24 changes: 20 additions & 4 deletions scripts/cert-renew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,27 @@ if [[ $(needs_renewal "${NDNCERT_CERTFILE}") ]]; then
fi

# Check and reissue certificates
bash "$(pwd)/dist/ndncert/renew.sh" "${FORCE}"
bash "$(pwd)/dist/nlsr/renew.sh" "${FORCE}"
bash "$(pwd)/dist/ndn-python-repo/renew.sh" "${FORCE}"
bash "$(pwd)/dist/ndncert/renew.sh" "${FORCE}" || exit $?
# Keep dependent renewals/reloads pending across a partially failed site renewal.
if [[ -f dist/ndncert/.security-changed ]]; then
FORCE="--force"
fi
bash "$(pwd)/dist/nlsr/renew.sh" "${FORCE}" || exit $?
bash "$(pwd)/dist/ndn-python-repo/renew.sh" "${FORCE}" || exit $?
bash "$(pwd)/dist/ndnd-repo/renew.sh" ${FORCE:+"${FORCE}"} || exit $?

# Restart containers if force
if [[ -n "${FORCE}" ]]; then
docker compose restart nlsr ndncert serve-certs ndn-python-repo
PWD="${ROOT_DIR:-$PWD}" docker compose restart nlsr ndncert serve-certs ndn-python-repo || exit $?
rm -f dist/ndncert/.security-changed
fi

# The NDNd certificate or public chain can change independently of the site CA.
# During bootstrap the first start loads the credentials; running services reload them.
if [[ -f dist/ndnd-repo/.security-changed ]]; then
RUNNING_REPO=$(PWD="${ROOT_DIR:-$PWD}" docker compose ps --status running -q ndnd-repo) || exit $?
if [[ -n "${RUNNING_REPO}" ]]; then
PWD="${ROOT_DIR:-$PWD}" docker compose restart ndnd-repo || exit $?
fi
rm -f dist/ndnd-repo/.security-changed
fi
9 changes: 5 additions & 4 deletions scripts/cron-master.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ fi
rm -f .git/index.lock
git pull

# Renew certificates if needed
bash $(pwd)/scripts/cert-renew.sh
# Render new service templates before certificate provisioning on an existing node.
PWD="${ROOT_DIR}" python3 framework/main.py || exit $?

PWD="${ROOT_DIR}" python3 framework/main.py
# Renew certificates if needed
bash $(pwd)/scripts/cert-renew.sh || exit $?

echo -e "Finished cron-master at $(date)" >&2
echo -e "Finished cron-master at $(date)" >&2
Loading
Loading