Publishing services
A service on a member, such as a NAS at home, can be reached from the internet by name. A device with a public address passes the connections on; the encryption ends at the member.
What it is #
You run a service at home, say a document server on your NAS, and want to open
it from any browser as docs.example.org. Your router at home opens no port
for this, and its address is in no public record.
A few words first:
- A domain is a name you own on the internet, such as
example.org. - A DNS record is an entry at your DNS provider that says which address a name points to.
- TLS is the encryption a browser uses for
https://addresses. - A certificate proves to the browser that the server really is
docs.example.org. juist gets one from Let’s Encrypt. - An ingress is a device of your network with a public address, a small
VPS for example, that an admin gave the role
ingress. It takes connections on port 443, reads the name the browser asks for, and passes the connection through the tunnel to the member that publishes the name.
- browseranywhere
Asks for docs.example.org.
- TLSvpsingress
Reads the name in the TLS ClientHello and passes the connection on.
- WireGuardnastarget
Ends TLS, with a certificate from Let's Encrypt.
- HTTP127.0.0.1:8080service
Gets the plain connection.
The ingress ends no TLS, so it never sees the content. TLS ends at the NAS, and the NAS passes the plain connection on to the service. One ingress serves every name of the network.
Setting it up #
Set the network’s domain and give the VPS the role:
juist network domain example.org
juist grant vps ingressPoint *.example.org at the VPS’s public address, for every name at once, or
docs.example.org alone.
juist ingress serveIt asks once for sudo, starts the juist-ingress service, and opens ports 443
(TCP and UDP, for HTTP/3) and 80 in firewalld or ufw, and the ports names are
published on by TCP as they come and go
(below); on FreeBSD, pf is left to
the host’s own rules. Port 80 only redirects to https.
Its operator, the local user who manages the device, publishes the name and says where the plain connection goes:
juist publish add docs 127.0.0.1:8080docs goes to port 443 on the NAS. juist gets a certificate through the
ingress and ends TLS here. It prints nas: publishes docs.example.org:443,
serving docs.example.org and
ending TLS for docs.example.org to 127.0.0.1:8080, then the CAA record that
keeps other accounts from certificates for the name (see below), and the
first time
warning: a server published from this device reaches the network and its LAN once compromised.
Publishing is an admin’s change. Where the NAS holds no admin key, it prints
docs.example.org goes to 127.0.0.1:8080 once published on this device and
the command an admin runs, juist publish add nas docs.example.org. Where a change needs
more than one admin, it waits for the others
(how changes are approved).
juist grant and juist publish add name the next step and the device it
runs on. Run there by that device’s operator, they take that step too.
juist invite vps --ingress admits a new device as the ingress at once.
Keeping the server away from your network #
A server published from a full member reaches the network and the member’s LAN once someone breaks into it. Publish from one only what you would put on the internet anyway.
A service member keeps the server away from them: a juistd of its own beside the service, with no privilege, which every device keeps from opening any connection to it. Only its replies pass.
juist invite immich --service immichIt admits the new device, named immich, as a service member that
publishes immich.example.org, and prints the link to join with.
In Docker, the service goes on an internal network with the service member
alone, and the service member’s image (make image in the source tree, then
docker load) on that network and one with a way out:
docker network create --internal -o com.docker.network.bridge.gateway_mode_ipv4=isolated immich-only
docker network connect immich-only immich-server # and disconnect it from every other network
docker run -d --name immich-juist -v immich-juist:/var/lib/juist -e JUIST_JOIN='juist:…' \
juist:VERSION --serve immich=immich-server:2283
docker network connect immich-only immich-juistIt joins by the link once started, and ignores it once in the network;
docker logs immich-juist says how the join went, with the words to compare
where the invite asks for them.
The same as one Compose file, with a web service to try it on, for Docker and Podman: examples/service-member in the repository.
Outside Docker, juistd --serve immich=127.0.0.1:2283 runs it as any user.
--serve @=web:80 serves the domain itself.
It ends TLS itself, with a certificate from Let’s Encrypt, and answers nothing but what it publishes and log sync. The service has no way out, so one that downloads things at its first start needs that done beforehand. The isolated gateway mode, Docker 29’s, keeps the host from the service too; without it, the service reaches whatever on the host listens on every address, at the network’s gateway.
HTTP/3 #
Browsers reach a name over HTTP/3, QUIC on 443/udp, which takes one round trip less to connect and loses no time to a lost packet on a slow link, where its service speaks HTTP and juist ends its TLS:
juist publish add docs 127.0.0.1:8080 --http3It prints ending TLS for docs.example.org to 127.0.0.1:8080, with HTTP/3, and a DNS record to add at your DNS provider,
docs.example.org. HTTPS 1 . alpn="h3,h2", which has browsers try HTTP/3
from their first request; without it they do from their second, told by
the Alt-Svc header juist adds to its answers. Where 443/udp is blocked
anywhere on the way, browsers keep to HTTP/2 over TCP.
- juist then proxies the service’s HTTP over TCP too, offering clients
HTTP/2: the service gets every request with its own Host, as before, and
no
X-Forwarded-Foror other header of the proxy’s. With--http2besides, juist asks the service by h2c. - The status page is offered HTTP/3 as it is. A service member offers it
for its names with
juistd --serve NAME=ADDR --http3. - WebSockets keep to HTTP/1.1 over TCP; juist passes them on as they are.
--proxynames the client to the service on each of its connections, as over TCP.- A client whose address changes, as a phone moving from Wi-Fi to LTE, connects anew; shares stay on TCP.
- A client connecting from one UDP socket to names on two devices, as Go’s HTTP/3 client does, reaches the first device alone over QUIC.
Names for members alone #
A name can hold a certificate from the CA, as any other, and still be
reached by members of the network alone, such as the family’s photos at
photos.example.org:
juist publish add nas photos --membersIt prints nas: publishes photos.example.org:443 (members), and juist publish
lists it with (members).
juist publish add photos 127.0.0.1:2342juist gets the certificate through the ingress and ends TLS here, as for
any name. With --members here too, the service never goes to the name
while the log has it public, which juist status warns of, and juist publish marks it (members); later commands keep that until
--members=false. A service member marks its services so with
juistd --serve NAME=ADDR --members.
Without an access policy, every member may
open it. With one, the members a rule names it for: pass from <family> to name photos.
- The ingress passes the name to nobody but the CA, which checks the name by
TLS on 443 when it issues and renews the certificate; anyone else gets
nothing, and the ingress logs
members only. Its public DNS record points at the ingress all the same, for the CA. - The device ending TLS takes the name only from the members it is for, by the address in the network their connection comes from, and from the ingress only the CA’s check: an ingress someone took over gets no further.
- Members reach it at its device, as any name on 443, on Linux with
systemd-resolved. A device that resolves the network’s names publicly
reaches it not at all: FreeBSD and Linux without systemd-resolved, where
juist statuswarns of each such name, and the Android app. - Its service is its own: juist does not serve a name for members alone
whose service another name’s bytes, or a share’s, also reach as they
are, where a client of the other could ask for it by Host, and
juist statussays so. A name juist proxies HTTP for (--http3) is held to its own Host. Let the service listen on 127.0.0.1 alone, and pass no other device’s name on to it. - The device itself does not reach the name through juist, so that none of its programs, one a client of a public service there can make ask, takes it: reach the service there at its own address.
- Update the device ending its TLS, and every ingress, before the network
takes names for members alone (
juist log upgrade): an older device stops at the upgrade and serves its names as they were until its view goes stale. - A name for members alone is on 443, by TLS, without
--proxy.juist publish add nas photoskeeps it so;--members=falsemakes it public. - Its name is in the CA’s public certificate logs, as every certificate’s is: what it is called is not secret, only what it serves.
A game server or another TCP service #
A service that speaks no TLS, such as a Minecraft server, gets a port of its own on every ingress, which passes each connection on that port to the device at once, reading nothing of it.
juist publish add nas mc 25565 --tcpIt publishes port 25565 of nas on port 25565 of every ingress, as
mc.example.org, and prints nas: publishes mc.example.org:25565/tcp.
--tcp=PORT takes another public port: juist publish add nas ssh 22 --tcp=2222 prints ssh.example.org:2222/tcp to 22.
Where the service listens on every address, its operator agrees with
juist publish serve. Where it listens on loopback alone, juist passes the
bytes on to it as they are:
juist publish add mc 127.0.0.1:25565 --tcpIt prints passing mc.example.org to 127.0.0.1:25565.
Players then connect to mc.example.org:25565. Every ingress holds each
such port while a name is published on it, answering nothing where the
device does not serve it, and on Linux lets it through firewalld or ufw
while the ingress runs, closing it again once the name is withdrawn or the
ingress stops; juist status there names the ports, and those another
program holds.
- Only admins publish a TCP port, as a change of the network; the role
publishshares on 443 alone. - A public port is one name’s in the network. 80, 443 and juist’s own ports, 41640 to 41739, are refused, and so is one an ingress’s own name goes to.
- Members reach the name at its device directly where its public port is the
device’s port and it has no
--proxy, and through the ingress otherwise. --proxysends the client’s address first, in a PROXY protocol v2 header, which the service must expect: Paper and Velocity do where told to, vanilla Minecraft does not.- juist prints no SRV record. A client that looks one up, as Minecraft’s does
for a name without a port, finds one you add at your DNS provider:
_minecraft._tcp.mc.example.org. SRV 0 5 25565 mc.example.org. - UDP is not passed on, so neither is Minecraft Bedrock’s 19132/udp.
The ingress sees everything a protocol that does not encrypt itself carries, and a CAA record protects nothing there. Publish that way only what encrypts itself, as SSH does, or what you would show anyone.
Sharing at once #
A device an admin gave the role publish (juist grant laptop publish)
publishes under its own name, laptop.example.org, without asking an admin
each time:
juist share ~/photos/2026 # at https://laptop.example.org/RANDOM/, until Ctrl-C
juist publish 8000 --for 2h # this device's web server on port 8000, for two hours
juist share -b --for 7d ~/talk.pdf # in the background, for a week
juist share # lists this device's shares, with their links
juist share --stop ~/talk.pdf # ends one, by its file, port or link, whichever command holds itA share is read under its random path alone; anywhere else on the name answers
404, unless juist publish PORT takes it, and entries such as .git stay hidden. A directory is listed, sortable
by name, size or date, and downloads whole, or any folder in it, as a ZIP; a
folder holding an index.html shows that page instead. It ends with the command or at
--for, seven days at most, and where the device goes away, ten minutes
later at most. With --background (-b), the command returns once the share
is up, and the share goes on, under the same link even across a restart of
juistd, until juist share --stop or --for; juist share lists it
with its link, and juist status counts it under Sharing. One certificate for the device’s name covers every share, so
their paths reach no certificate log. A device holds up to 16 shares at
once, each under a path of its own, and one juist publish PORT beside them,
which answers what no share’s path does. juist share --stop without a name
ends the only share; where several run, it says to name one.
Renamed, it shares under its new
name, and juist share prints the new link.
A status page #
A device serves the network’s status page under a published name, in this website’s look, light or dark as the browser asks:
juist publish status-page status # at https://status.example.org/
juist publish status-page status --public all # anyone sees all of itMembers see all of it: every device with its addresses and roles, how the
serving device reaches it (direct, through a relay, sought or absent) and
when it last heard from it, the published names and shares and whether each
is up, what needs attention, and the log’s newest changes. Anyone else sees
what --public says:
--public | Anyone but a member sees |
|---|---|
none | nothing: the page answers 404 |
services, the default | the published names and shares, and whether each is up |
network | all but the changes |
all | all of it |
The page knows a member by the overlay address its connection comes from;
whoever comes through the ingress is the public, whatever the ingress says.
It refreshes itself every 15 seconds while it is open, which makes it a
status screen for a phone, too. Opened once with https://, the browser
keeps to https for it.
Devices, published names and changes are the network’s, the same on every member. Reach and what needs attention are the serving device’s own, and the page says whose: serve it from a device that is always on and reaches the others well. An ingress cannot, holding port 443 itself.
With --public network or all, anyone who has the address sees your
devices’ names, overlay addresses and roles, and with all who approved which
change. Only members see that otherwise.
juist publish status-page status --public none says anew what the public
sees, and juist publish remove status takes the page down. It is a name
whose TLS juist ends, as with juist publish add NAME HOST:PORT; a service
member serves it with --serve status=status-page:all, and its reach is
then that of a device every other keeps from opening connections.
Good to know #
- A published name is one label under the domain,
docs.example.org, or the domain itself,example.org, spelled out:juist publish add nas example.org. A device publishes 16 names at most. One device at most publishes the domain itself, and it needs A and AAAA records of its own at your DNS provider, the ingress’s, which*.example.orgdoes not cover. A CAA record there holds for every name under it that has none of its own: give each device’s names their own. - A name with a record of its own, as the CAA or HTTPS record juist prints,
gets no address from
*.example.organy more: give it A and AAAA records of its own too, the ingress’s. - A service can end TLS itself and hold its own certificate, as it would
facing the internet. An admin then publishes it with
juist publish add nas docs, on port 443 unless you give another port after the name, and nas’s operator agrees withjuist publish serve. --proxyonjuist publish addhands the service the client’s address in a PROXY protocol v2 header. Members then reach the name through the ingress too.- An ingress passes 16384 connections at once, 256 from one address and
1024 from one /24 or /48; juist ending TLS on a device takes 8192.
--max-conns Nand, for the ingress,--max-per-source Nchange that: on Linux in a drop-in forjuist-ingressorjuist-serve, on FreeBSD injuist_ingress_argsorjuist_serve_args. --http2onjuist publish add NAME HOST:PORToffers clients HTTP/2, for a service that takes it without TLS (h2c) besides HTTP/1.1. A browser then opens one connection instead of up to six. A service that takes HTTP/1.1 alone fails with it.- Every member on Linux with systemd-resolved sends the whole domain to
juistd, which answers a name published on 443, or shared, with its
device’s address while that device is reachable, so that members
reach it directly through the tunnel, with the same certificate, and passes
every other name of the domain on as before, held for 30 seconds at most,
so that a name newly published is reached directly soon. DNSSEC is off for the domain on
members (Names).
http://to such a name goes to the device’s port 80, where juist redirects it to https, unless another program listens there on every address: that program then answers, whichjuist statuswarns of. juist publishlists the published names, and where this device sends its own.juist publish remove docsstops publishing one on this device,juist publish remove nas docson another.juist publish serve --stopstops serving and keeps where names go.juist ingress serve --stopstops the ingress.- An ingress and juist’s own TLS run on Linux and FreeBSD, for the device’s default network. On FreeBSD, pf, where the host runs it, lets 443 by TCP and UDP, 80 and the ports names are published on by TCP in by the host’s own rules.
juist network domain offis refused while a device publishes names under the domain, with thejuist publish removethat comes first.- An ingress is never a voucher or an exit node, publishes nothing under its
own name or on 80, 443 or a port a name is published on by TCP, which it
holds itself, and routes no subnet; a
service member holds no other role.
juist grantnames the role that is in the way. Every member takes from it only connections to the ports it publishes and serves, and log sync, replies to what it opened itself, pings included, and ICMP errors on its own connections. Everything else from it is dropped. - A client that does not answer its service’s TLS hello within a minute is let go, and any connection quiet both ways after 5 minutes.
- A client gets 10 seconds to send its TLS hello to the ingress.
- The ingress logs a line for each connection: the client’s address and
port, the name it asked for, what came of it (
spliced,not served,no hello,target unreachable,refused), the bytes each way and how long it lasted; on port 80 the method, path, status and User-Agent too, and on a port published by TCP the port and the name it goes to; a QUIC flow is logged once it ends, asaccess: quic,not provenwhere its client never completed a handshake, which a spoofed address cannot. Lines of such flows, and of QUIC refused, come once a second at most, saying how many more went unsaid. Past port 80 it sees neither path nor User-Agent, TLS ending at the device.journalctl -u juist-ingressshows them, on FreeBSD/var/log/juist-ingress.log, kept as long as the journal or newsyslog keeps them. - The ingress runs as its own user,
juist-ingress, holds no key of the network, and may ask juistd one thing: what is published. - An ingress whose view of the network is stale serves nothing. If its juistd stops answering, the names go down within fifteen seconds.
juist revoke vps ingress, orservice, makes the device a member like any other again, which juist warns of:warning: vps is confined no more, now a member like any other. One that may be compromised goes byjuist remove. Without the ingress role, runjuist ingress serve --stopon it.
Whoever controls the ingress can get a certificate for your names and intercept
public clients, and members that reach a name through it: those on another
port than 443, behind --proxy, or without systemd-resolved. Members that resolve a name on 443
to its device connect to it directly. A CAA
record, a DNS record that says who may issue certificates for a name, bound to
the target’s ACME account closes that gap: juist publish add NAME HOST:PORT
prints it, and the terminator’s log names it for a service member.
If something goes wrong #
| You see | What to do |
|---|---|
the network has no domain to publish under | juist network domain example.org first |
on nas, juist status: Published says not served here: not agreed to on this device (juist publish serve) | run juist publish serve on nas |
on vps, juist status: Ingress says not running on this device (juist ingress serve) | run juist ingress serve on vps |
juist ingress on vps: nothing to serve | no name is published and served yet; a hint says when the view is stale |
the ingress does not answer on 443: … | journalctl -u juist-ingress shows why; on FreeBSD, /var/log/juist-ingress.log |
the terminator does not answer on … | journalctl -u juist-serve shows why; on FreeBSD, /var/log/juist-serve.log |
this device holds no publish role | an admin runs juist grant DEVICE publish |
juist status: Sharing says reached by nobody: the terminator does not run | juist share starts it; journalctl -u juist-serve shows why it stopped |
juist status: Published says …, but ends TLS for none: the terminator does not run (juist publish serve) | run juist publish serve; journalctl -u juist-serve shows why it stopped |
the terminator does not say it listens on … | it did not start in time; journalctl -u juist-serve shows why |
juist status: warning: another program holds port 80 on … | members opening http://NAME reach that program instead of a redirect to https; have it listen on the addresses it serves alone, and open the name with https:// meanwhile |
juist status: warning: no certificate for NAME: … | the CA issued none, for the reason given; most often it cannot reach NAME, whose public DNS must point at a running ingress. Until then a client’s TLS fails |
juist status: warning: not serving NAME, which is for members alone: OTHER reaches its service as it is, … | OTHER is passed by TCP, or not proxied, to the same service, whose clients could ask it for NAME; give NAME a service of its own |
juist status: warning: not serving NAME, its service meant for members alone: the log has the name public | an admin publishes it for members: juist publish add DEVICE NAME --members; or, if it is to be public, juist publish add NAME HOST:PORT --members=false on the device |
juist status: warning: NAME, for members alone, is not reached from here, … | this device resolves the network’s names publicly, so NAME leads to the ingress, which refuses it; on Linux, use systemd-resolved |
another program holds port 443 on …, where juist would end TLS | stop that program, or publish on another port |
on vps, juist status: Ingress says …; another program holds port 443/udp | another program, often a web server’s own HTTP/3, holds 443/udp on the ingress; browsers keep to TCP until it lets go, and the ingress takes the port then |
access: quic … refused: another name's flow from its address | a client connected to names on two devices from one UDP socket, which the ingress passes to one of them alone; browsers use a socket for each |
| HTTP/3 slower than HTTP/2 under load | the host’s UDP buffers are small: sysctl -w net.core.rmem_max=7500000 net.core.wmem_max=7500000 on the device ending TLS |
another program holds port 25565 on …, where juist would pass NAME by TCP | the service listens on every address, the device’s overlay addresses too: publish its port instead, as the hint says, and agree with juist publish serve |
on vps, juist status: Ingress says …; another program holds port 22 | a program on the ingress, often its sshd, holds a port a name is published on by TCP, which juist then leaves to that program and to the firewall’s rules: publish the name on another port with --tcp=PORT |
this device publishes a port already | another juist publish PORT runs, perhaps in the background: juist share lists it, juist share --stop PORT ends it |
this device holds 16 shares already | juist share lists them; end one with juist share --stop |
this device is no ingress | an admin runs juist grant DEVICE ingress |
warning: the network has no ingress, so nothing reaches it from the internet | grant a device with a public address the role ingress |
warning: nothing answers on 127.0.0.1:8000 yet | start the web server that juist publish 8000 points at |
warning: firewalld blocks tcp/443 on juist0, … | juist0 is not in firewalld’s trusted zone, which the package sets at its first install alone; run the firewall-cmd the hint gives |