Chapter 17Lesson 02220–300 min

TLS, Reverse Proxies, Context Paths, HTTP Settings, Network Boundaries, and Secure Exposure: Guided Hands-On Workflow and Core Operations

Construct a disposable loopback-only Nexus exposure with a local TLS reverse-proxy fixture, synthetic certificate, forwarded headers, a Raw hosted repository, and verifiable secure publication without exposing credentials.

TLS labLoopbackRaw hostedSynthetic certificateEvidence

Learning objectives

  • Bind or constrain a disposable Nexus backend to loopback/private reachability.
  • Run a portable local TLS reverse-proxy fixture with a synthetic certificate.
  • Verify TLS, forwarded-header intent, UI/API URLs, and explicit rejection of plain HTTP.
  • Publish and retrieve a synthetic Raw artifact through HTTPS without placing credentials in arguments.
  • Capture before/after evidence that distinguishes proxy state, Nexus metadata, and blob content.

1. Lab scenario and safety boundary

This lesson builds one disposable exposure path: Nexus listens only on loopback at 127.0.0.1:8081; a portable Python reverse-proxy fixture listens on loopback HTTPS 8443; a second plain-HTTP listener on 8080 rejects requests with 403; and one Raw hosted repository receives a synthetic text artifact through HTTPS.

Do not run this on a shared Nexus instance. Listener changes require a restart. Use only a disposable local installation. Do not import the synthetic certificate into a corporate trust store, do not expose ports 8080/8081/8443 beyond loopback, and do not place real credentials in command history or evidence files.

Version baseline (26 August 2026). The official Sonatype download and archive pages list Nexus Repository 3.95.0, build 3.95.0-07, as the current self-hosted download. Nexus Repository 3.87+ uses Java 21 and official installers include a bundled Java 21 runtime. Record the version actually running in your lab before applying any example.

2. Preflight: prove the backend and record rollback state

The lab assumes a self-hosted Community instance using the bundled Java 21 runtime. H2 is acceptable only for this small non-container learning instance; current Sonatype guidance recommends PostgreSQL for production and does not support H2 container deployments.

# POSIX/Bash. Define the data directory for YOUR disposable archive install.
export NEXUS_DATA="$HOME/nexus-lab-data"
export LAB="$HOME/nexus-ch17-lab"
mkdir -p "$LAB/evidence" "$LAB/cert"

curl -fsS http://127.0.0.1:8081/service/rest/v1/status | tee "$LAB/evidence/status-before.json"
cp "$NEXUS_DATA/etc/nexus.properties" "$LAB/evidence/nexus.properties.before"
ss -ltn 2>/dev/null | grep ':8081' | tee "$LAB/evidence/listener-before.txt" || true

On Windows, use a disposable archive installation and record the equivalent nexus.properties file plus Get-NetTCPConnection -LocalPort 8081. Keep the lab directory private because later it temporarily contains a TLS private key and an authentication file; those files are intentionally excluded from the evidence packet.

3. Restrict Nexus to loopback

Current runtime configuration supports application-host and application-port overrides in the data-directory nexus.properties. The explicit loopback binding makes the reverse proxy the only network ingress in this single-host lab.

# Add or update these lines in $NEXUS_DATA/etc/nexus.properties.
application-host=127.0.0.1
application-port=8081
# Keep the default application context for this lab:
nexus-context-path=/

Restart the disposable Nexus service using the installation method you already used in Chapter 2. Then verify the listener. If Nexus does not start, restore nexus.properties.before and inspect nexus.log; do not keep editing unrelated settings.

curl -fsS http://127.0.0.1:8081/service/rest/v1/status
ss -ltn 2>/dev/null | grep ':8081' || netstat -an | grep '8081'

4. Generate a two-day synthetic certificate with the correct SAN

The lab hostname is nexus.lab.test. We do not modify public DNS or the system hosts file. Instead, clients use --resolve to map that name to loopback for each request.

openssl req -x509 -newkey rsa:3072 -nodes -days 2 \
  -keyout "$LAB/cert/nexus.lab.test.key" \
  -out "$LAB/cert/nexus.lab.test.crt" \
  -subj "/CN=nexus.lab.test" \
  -addext "subjectAltName=DNS:nexus.lab.test"
chmod 600 "$LAB/cert/nexus.lab.test.key"

openssl x509 -in "$LAB/cert/nexus.lab.test.crt" -noout \
  -subject -issuer -dates -ext subjectAltName \
  | tee "$LAB/evidence/certificate-public.txt"

The certificate is self-signed and intentionally short-lived. The client will trust exactly this certificate with --cacert. The private key stays under $LAB/cert and must never be copied into evidence.

5. Run the local TLS reverse-proxy fixture

This small Python fixture is deliberately limited to the academy’s tiny requests. It is not a production reverse proxy. It forwards the client-facing host and scheme to the Nexus backend and prints a concise request log so the learner can prove which headers were emitted.

cat > "$LAB/reverse_proxy.py" <<'PYPROXY'
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from http.client import HTTPConnection
import ssl, threading, sys

BACKEND_HOST, BACKEND_PORT = "127.0.0.1", 8081
TLS_HOST, TLS_PORT = "127.0.0.1", 8443
HTTP_HOST, HTTP_PORT = "127.0.0.1", 8080
CERT, KEY = sys.argv[1], sys.argv[2]
HOP = {"connection","keep-alive","proxy-authenticate","proxy-authorization",
       "te","trailers","transfer-encoding","upgrade"}

class Proxy(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def forward(self):
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length) if length else None
        headers = {k:v for k,v in self.headers.items() if k.lower() not in HOP}
        external_host = self.headers.get("Host", "nexus.lab.test:8443")
        headers["Host"] = external_host
        headers["X-Forwarded-Proto"] = "https"
        headers["X-Forwarded-Host"] = external_host
        headers["X-Forwarded-For"] = self.client_address[0]
        upstream = HTTPConnection(BACKEND_HOST, BACKEND_PORT, timeout=60)
        upstream.request(self.command, self.path, body=body, headers=headers)
        resp = upstream.getresponse(); data = resp.read()
        self.send_response(resp.status, resp.reason)
        for k,v in resp.getheaders():
            if k.lower() not in HOP and k.lower() != "content-length": self.send_header(k,v)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        if self.command != "HEAD": self.wfile.write(data)
        print(f"PROXY_REQUEST method={self.command} path={self.path} host={external_host} "
              f"forwarded_proto=https forwarded_for={self.client_address[0]}", flush=True)
        upstream.close()
    do_GET=do_POST=do_PUT=do_DELETE=do_HEAD=forward
    def log_message(self, fmt, *args): pass

class DenyHTTP(BaseHTTPRequestHandler):
    def reject(self):
        data=b"Plain HTTP is disabled in this lab. Use HTTPS.\n"
        self.send_response(403); self.send_header("Content-Type","text/plain")
        self.send_header("Content-Length",str(len(data))); self.end_headers()
        if self.command != "HEAD": self.wfile.write(data)
    do_GET=do_POST=do_PUT=do_DELETE=do_HEAD=reject
    def log_message(self, fmt, *args): pass

plain = ThreadingHTTPServer((HTTP_HOST, HTTP_PORT), DenyHTTP)
threading.Thread(target=plain.serve_forever, daemon=True).start()
https = ThreadingHTTPServer((TLS_HOST, TLS_PORT), Proxy)
ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); ctx.load_cert_chain(CERT, KEY)
https.socket=ctx.wrap_socket(https.socket, server_side=True)
print(f"TLS proxy https://nexus.lab.test:{TLS_PORT} -> http://{BACKEND_HOST}:{BACKEND_PORT}", flush=True)
print(f"HTTP deny http://{HTTP_HOST}:{HTTP_PORT}", flush=True)
https.serve_forever()
PYPROXY

python3 "$LAB/reverse_proxy.py" \\
  "$LAB/cert/nexus.lab.test.crt" "$LAB/cert/nexus.lab.test.key" \\
  >"$LAB/evidence/proxy.log" 2>&1 &
export PROXY_PID=$!
printf 'proxy pid=%s\n' "$PROXY_PID"

For production, replace this fixture with a supported reverse proxy/load balancer, managed certificates, rate/size/time-out policy, observability, and a network rule that permits only the proxy tier to reach the Nexus backend.

6. Prove certificate identity, API reachability, and HTTP rejection

openssl s_client -connect 127.0.0.1:8443 -servername nexus.lab.test \
  -CAfile "$LAB/cert/nexus.lab.test.crt" </dev/null 2>&1 \
  | grep -E 'Verify return code|subject=|issuer=' \
  | tee "$LAB/evidence/tls-handshake.txt"

curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
  --resolve nexus.lab.test:8443:127.0.0.1 \
  https://nexus.lab.test:8443/service/rest/v1/status \
  | tee "$LAB/evidence/status-through-tls.json"

curl -i http://127.0.0.1:8080/service/rest/v1/status \
  | tee "$LAB/evidence/plain-http-rejected.txt"

Expected evidence: TLS verification succeeds only for the named host; the Nexus status endpoint is reachable through HTTPS; and the plain-HTTP edge returns 403. None of those tests require a repository credential.

7. Create one disposable Raw hosted repository

In the Nexus UI on the disposable instance, create raw (hosted) named academy-ch17-secure. Use the default file blob store, strict content type validation, and Allow once write policy. Create or reuse a disposable writer identity limited to browse/read/add/edit on this one repository. Do not use admin for publication.

This UI step changes Nexus security/repository configuration in the database. It does not change the reverse proxy certificate or OS firewall. The first upload later creates normal asset metadata plus blob bytes.

8. Prepare a temporary credential file without command-line secrets

export NX_USER='academy-ch17-publisher'
read -r -s -p 'Disposable Nexus password: ' NX_PASS; echo
export AUTH_FILE="$(mktemp)"
chmod 600 "$AUTH_FILE"
printf 'machine nexus.lab.test login %s password %s
' "$NX_USER" "$NX_PASS" > "$AUTH_FILE"
unset NX_PASS

# Do NOT tee, cat, or copy AUTH_FILE into evidence.

Because the URL host is nexus.lab.test, curl can select this netrc entry without putting the password in the process argument list. In production, use your approved secret injection mechanism and short-lived credentials where supported.

9. Publish through TLS and verify bytes independently

printf '%s
' 'academy chapter 17 secure transport proof' > "$LAB/network-proof-1.0.0.txt"
sha256sum "$LAB/network-proof-1.0.0.txt" | tee "$LAB/evidence/source.sha256"

curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
  --resolve nexus.lab.test:8443:127.0.0.1 --netrc-file "$AUTH_FILE" \
  --upload-file "$LAB/network-proof-1.0.0.txt" \
  'https://nexus.lab.test:8443/repository/academy-ch17-secure/com/example/academy/network-proof/1.0.0/network-proof-1.0.0.txt'

curl --fail --show-error --cacert "$LAB/cert/nexus.lab.test.crt" \
  --resolve nexus.lab.test:8443:127.0.0.1 --netrc-file "$AUTH_FILE" \
  -o "$LAB/downloaded-network-proof-1.0.0.txt" \
  'https://nexus.lab.test:8443/repository/academy-ch17-secure/com/example/academy/network-proof/1.0.0/network-proof-1.0.0.txt'
sha256sum "$LAB/downloaded-network-proof-1.0.0.txt" | tee "$LAB/evidence/download.sha256"
cmp "$LAB/network-proof-1.0.0.txt" "$LAB/downloaded-network-proof-1.0.0.txt"

A successful PUT changes repository metadata and blob content because Nexus accepted an authenticated repository write. The reverse proxy remains stateless apart from logs. SHA-256 equality proves byte equality between producer and consumer; it does not prove provenance, vulnerability safety, or signer identity.

10. Inspect after-state and forwarded-header evidence

tail -n 20 "$LAB/evidence/proxy.log" | tee "$LAB/evidence/proxy-tail.txt"
tail -n 30 "$NEXUS_DATA/log/request.log" 2>/dev/null \
  | tee "$LAB/evidence/nexus-request-tail.txt" || true

# Read-only Nexus UI/API checks:
# - Browse academy-ch17-secure and locate the exact asset path.
# - Search component/assets API for repository=academy-ch17-secure.
# - Confirm Settings -> System -> Capabilities keeps HTTP Forwarded Headers enabled.

The proxy log proves which client-facing scheme/host values the fixture emitted. The Nexus request log proves Nexus served the repository request. Browse/API evidence proves the asset exists through supported Nexus state, instead of inspecting blob filenames directly.

11. Challenge: choose the control, not the symptom

You need to prevent any non-proxy host on your LAN from reaching Nexus while keeping the public package URL unchanged. Which layer should change first: repository privileges, nexus-context-path, certificate SANs, or listener/firewall reachability? Write your answer before revealing it.

Challenge answer

12. Cleanup and rollback

Delete only the disposable repository and lab writer identity through supported Nexus UI/API controls. Stop the proxy process. Restore the saved nexus.properties.before only if this lab changed your disposable instance’s binding. Remove the exact credential and private-key files; do not perform broad filesystem cleanup against Nexus data.

kill "$PROXY_PID" 2>/dev/null || true
rm -f "$AUTH_FILE"
rm -f "$LAB/cert/nexus.lab.test.key" "$LAB/cert/nexus.lab.test.crt"
# Restore nexus.properties.before and restart Nexus if you changed application-host/context.
# Remove academy-ch17-secure and academy-ch17-publisher with supported Nexus controls.

13. Knowledge check

Why bind the backend to loopback in this single-host lab?

Why use --cacert rather than -k?

What state does the Raw PUT change?

What does HTTP 403 on the lab port 8080 prove?

Why is application-host=127.0.0.1 not a universal production answer?

14. Summary and next step

The lab separated public-facing TLS policy from private Nexus service state and proved both with client and server evidence. Lesson 3 broadens the design choices for real deployments, including re-encryption, multiple package formats, context paths, connectors, private administration, and certificate rotation.

Official references and version notes

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.