A Webmin module for managing StrongSwan IPsec site-to-site tunnels and road-warrior connections, through either the modern swanctl / vici stack or the legacy ipsec.conf / stroke stack — chosen automatically.
- Version: 1.0
- Category: Networking
- Requires: Webmin ≥ 2.651, Perl ≥ 5.26, StrongSwan 5.9+ (
strongswan-swanctl+charon-systemd, or the legacystrongswanmetapackage) - OS: any Linux; developed and verified on Ubuntu 22.04 / StrongSwan 5.9.5
- Reads and writes tunnel configuration in place — your comments, ordering and any directive the module does not manage are left untouched. It edits the lines it owns and nothing else.
- Presents the editor as a VPN request form would: General → Phase 1 (IKE) → Phase 2
(IPsec) → Advanced, with a
?tooltip on every field naming the form row it maps to. - Manages tunnels (site-to-site and road-warrior, one or many traffic-selector pairs), pre-shared keys / EAP / X.509 credentials, address pools, and the daemon itself (start / stop / reload / boot).
- Detects the dangerous "both stacks running" state and refuses to write until it is resolved, because two charon daemons fight over the same kernel IPsec state.
- In Webmin: Webmin → Webmin Configuration → Webmin Modules → Install Module →
From uploaded file, and upload
remotswan-1.0.wbm.gz. - Or on the server command line:
/usr/share/webmin/install-module.pl --acl root /path/to/remotswan-1.0.wbm.gz - Open Webmin → Networking → StrongSwan IPsec VPN.
The banner at the top of the main page tells you which backend is in use, which systemd unit it belongs to, and the StrongSwan version. If it says StrongSwan is not installed when it is, see Debugging → "StrongSwan not installed" but it is below.
Modern stack (what the module prefers):
apt install strongswan-swanctl charon-systemd
systemctl disable --now strongswan-starter # if the legacy stack is present
systemctl enable --now strongswan
Legacy stack only:
apt install strongswan
Never run both at once.
apt install strongswan-swanctldoes not remove the legacystrongswan-starter, and neither systemd unit declaresConflicts=with the other. Two charon daemons bind the same kernel netlink sockets and corrupt each other's policies. The module shows a red banner and blocks all writes when it sees both active.
sudo ln -s /path/to/remotSwan /usr/share/webmin/remotswan
sudo chmod +x /usr/share/webmin/remotswan/*.cgi /usr/share/webmin/remotswan/*.pl
# copy the per-OS config into place (install-module.pl does this for you normally):
sudo /usr/share/webmin/copyconfig.pl "$(sed -n 's/^os_type=//p' /etc/webmin/config)/$(sed -n 's/^real_os_type=//p' /etc/webmin/config)" \
"$(sed -n 's/^os_version=//p' /etc/webmin/config)/$(sed -n 's/^real_os_version=//p' /etc/webmin/config)" \
/usr/share/webmin /etc/webmin remotswan
# grant access, then restart:
sudo systemctl restart webmin
A symlinked module needs the copyconfig.pl step, and the module finds WebminCore.pm
via SERVER_ROOT even when the symlink points out of the Webmin tree — but if you skip
copyconfig.pl, %config is empty and detection cannot work.
The editor tabs mirror a VPN request form. In brief, for a typical PSK site-to-site link:
| Tab | Field | Example |
|---|---|---|
| General | IKE version | IKEv2 |
| Phase 1 (IKE) | Local → Endpoint address | 198.51.100.20 |
| Phase 1 (IKE) | Local / Remote → Authentication | Pre-shared key |
| Phase 1 (IKE) | Remote → Endpoint address | 203.0.113.10 |
| Phase 1 (IKE) | IKE proposals | aes256-sha512-ecp521 |
| Phase 1 (IKE) | Rekey IKE SA after | 24h |
| Phase 2 (IPsec) | Local networks | 172.20.5.40/32 |
| Phase 2 (IPsec) | Remote networks | 10.50.20.7/32, 10.50.10.11/32 |
| Phase 2 (IPsec) | ESP proposals | aes256-sha512 (no DH group = No PFS) |
| Phase 2 (IPsec) | Rekey IPsec SA after | 1h |
Then add the pre-shared key under Secrets and keys, with both peer addresses as its identities. There is a full worked example, including the traps, in the module's built-in help (the ? by the page title → Configuring a tunnel from a VPN request form).
Four things forms get wrong:
- No PFS means leaving the Diffie-Hellman group out of the ESP proposals — a group there turns PFS on.
- A form's single Lifetime is the rekey time, not the hard limit. Leave Delete … after on Default.
- A bare address in an encryption domain is a /32.
- Main / Aggressive mode does not exist in IKEv2 — ignore that row.
Every algorithm name is in Cipher suite reference below.
The Phase 2 tab holds one block per traffic-selector pair. Some firewalls (FortiGate especially) create a separate Phase 2 per network, and expect the same from you: if a tunnel establishes but only one of several networks passes traffic, add the other pairs as their own Phase 2 blocks. Only the swanctl backend supports this; ipsec.conf is one pair per connection.
A form gives you algorithms in vendor spelling — AES-256, SHA-512, DH Group 21.
StrongSwan wants one hyphenated string. Getting one component wrong fails the whole
negotiation, and the only symptom is NO_PROPOSAL_CHOSEN, so look everything up rather
than guessing.
Phase 1 aes256-sha512-ecp521 encryption - hashing - DH group
Phase 2 aes256-sha512 encryption - authentication (no group = No PFS)
Order is fixed. Multiple proposals are comma separated, and the peer picks one.
Encryption Algorithm
| Form says | Use |
|---|---|
| DES | des — broken, never use |
| 3DES | 3des — obsolete, only for old equipment |
| AES-128 / AES-192 / AES-256 | aes128 / aes192 / aes256 |
| AES-128-GCM / AES-256-GCM | aes128gcm16 / aes256gcm16 |
| ChaCha20-Poly1305 | chacha20poly1305 |
| Camellia-128 / -256 | camellia128 / camellia256 |
Hashing Algorithm (the form may call it Authentication or Integrity)
| Form says | Use |
|---|---|
| MD5 | md5 — broken, never use |
| SHA-1 | sha1 — deprecated |
| SHA-256 / SHA-384 / SHA-512 | sha256 / sha384 / sha512 |
| AES-XCBC | aesxcbc |
Diffie-Hellman Group — forms give the number, so look up by number
| Group | Use | Group | Use |
|---|---|---|---|
| 1 | modp768 — broken |
19 | ecp256 |
| 2 | modp1024 — weak |
20 | ecp384 |
| 5 | modp1536 — weak |
21 | ecp521 |
| 14 | modp2048 |
24 | modp2048s256 |
| 15 | modp3072 |
25 | ecp192 |
| 16 | modp4096 |
26 | ecp224 |
| 17 | modp6144 |
27 – 30 | bp224 / bp256 / bp384 / bp512 |
| 18 | modp8192 |
31 / 32 | curve25519 / curve448 |
Same encryption and authentication keywords as above. Two rules decide the rest:
- PFS is the DH group's presence, not a checkbox. Append a group to turn it on, leave it out for No PFS. Group numbers translate exactly as in Phase 1.
- An AEAD cipher takes no separate authentication algorithm.
aes256gcm16is a complete proposal;aes256gcm16-sha256is wrong. GCM already authenticates.
One of the more confusing form rows, because it can mean any of four things — and they land in four different places:
| Form says | Means | Where it goes |
|---|---|---|
| Encapsulation: ESP | encrypts the payload | Nowhere — StrongSwan uses ESP unless told otherwise |
| Encapsulation: AH | authenticates only, no encryption | Not supported — see Known limitations |
| Mode: Tunnel / Transport | whether an inner IP header is added | Phase 2 (IPsec) → Mode |
| NAT-T / NAT traversal | wraps ESP in UDP to cross NAT | Advanced → Force UDP encapsulation — leave on Default, which auto-detects |
A form reading AES-256 / SHA-512 / DH Group 21, and AES-256 / SHA-512 / No PFS:
Phase 1 (IKE) IKE proposals aes256-sha512-ecp521
Phase 2 (IPsec) ESP proposals aes256-sha512
Confirm it negotiated by comparing charon's two lines — they must be identical:
received proposals: IKE:AES_CBC_256/HMAC_SHA2_512_256/PRF_HMAC_SHA2_512/ECP_521
configured proposals: IKE:AES_CBC_256/HMAC_SHA2_512_256/PRF_HMAC_SHA2_512/ECP_521
Note charon echoes back its own internal names — sha512 appears as HMAC_SHA2_512_256
for integrity and PRF_HMAC_SHA2_512 for the pseudo-random function, and ecp521 as
ECP_521. That is the same proposal, not a mismatch.
The full worked example, with the traps, is in the built-in help (the ? by the page title → Configuring a tunnel from a VPN request form).
StrongSwan validates configuration at load time, not when the daemon starts — so
systemctl start succeeding tells you almost nothing about whether your tunnels loaded.
Most problems fall into a few buckets.
- Red banner, "both stacks running": you have both
strongswan.service(swanctl) andstrongswan-starter.service(stroke) active. Pick one:The banner offers a [Disable it] link that does this for the stack you are not using.systemctl disable --now strongswan-starter # keep swanctl - Yellow banner, "will conflict at boot": the idle stack is still enabled at boot. Same fix — disable it.
The module's own settings were never copied into /etc/webmin/remotswan/config (common
after a symlink install). The banner names this specifically. Run the copyconfig.pl
command from Developer install above, or re-install through Webmin.
Work outward from the config:
-
Does the config load at all?
swanctl --load-all --noprompt # modern ipsec reload # legacyErrors here are syntax or algorithm problems. The module's raw editor checks syntax before saving, but a hand edit outside it can still break the file.
-
Did charon accept the connection?
swanctl --list-conns # modern ipsec statusall # legacyIf your tunnel is missing, it did not load — check the log (below).
-
Bring it up and watch:
swanctl --initiate --child <name> # modern ipsec up <name> # legacy -
Read the log — this is where StrongSwan explains itself:
journalctl -u strongswan.service -f # modern (charon-systemd) journalctl -u strongswan-starter.service -f # legacy # or, if it logs to syslog: tail -f /var/log/syslog | grep -Ei 'charon|ipsec'
| Symptom | Usual cause |
|---|---|
NO_PROPOSAL_CHOSEN |
Phase 1 or Phase 2 algorithms do not match the peer. Compare the form exactly, including the PFS / DH group on Phase 2. |
| Tunnel establishes, no traffic | Traffic selectors (encryption domains) do not match, or a bare IP was entered without /32, or the peer wants a separate Phase 2 per network. |
AUTHENTICATION_FAILED |
Pre-shared key differs between ends (a trailing space is the classic), or the wrong identity. |
| Peer rejects our identity | This host is behind NAT (e.g. a cloud instance with an Elastic IP) and is offering its private address. Set Phase 1 → Local → Identity to the public address. Check with ip addr. |
| IKE SA up, no child SA | Phase 2 mismatch — look at PFS and the traffic selectors first. |
| Nothing on UDP 500/4500 | Firewall. Allow UDP 500 and 4500 from the peer; on a cloud host this is a security-group rule. |
Edit /etc/strongswan.d/charon-logging.conf (or use the module's raw editor on
strongswan.conf), then reload:
charon {
filelog {
/var/log/charon.log {
default = 2 # 0=silent .. 4=very verbose; 2 is a good debugging level
ike = 2
cfg = 2
}
}
}
swanctl --reload-settings # apply the logging change without dropping tunnels
Useful for verifying what the module did, or working when Webmin is unavailable. These require root (they talk to charon's control socket).
| Task | Command |
|---|---|
| Load everything (conns, secrets, pools, certs) | swanctl --load-all --noprompt |
| Load just connections | swanctl --load-conns |
| Load just credentials | swanctl --load-creds |
| Show configured connections | swanctl --list-conns |
| Show live security associations | swanctl --list-sas |
| Show installed kernel policies | swanctl --list-pols |
| Bring one child up | swanctl --initiate --child <name> |
| Tear one connection down | swanctl --terminate --ike <name> |
| Force a rekey | swanctl --rekey --ike <name> |
| Loaded certificates | swanctl --list-certs |
| Address pool usage | swanctl --list-pools |
| Daemon statistics | swanctl --stats |
| Validate a specific file | swanctl --load-conns --file /etc/swanctl/conf.d/x.conf |
swanctlcan only read files under/etc/swanctl/**— its AppArmor profile forbids anywhere else, and the error it gives ("failed to open config file") does not say so. This is why the module keeps new tunnels inside/etc/swanctl/conf.d/.
| Task | Command |
|---|---|
| Status summary | ipsec status |
| Full status | ipsec statusall |
| Reload config (keeps SAs) | ipsec reload |
| Restart (drops all SAs) | ipsec restart |
| Bring a conn up | ipsec up <name> |
| Take a conn down | ipsec down <name> |
| Version | ipsec --version |
The kernel is the ground truth for whether traffic is actually encrypted:
ip xfrm state # the ESP/AH security associations, with packet/byte counters
ip xfrm policy # the traffic-selector policies
ip -s xfrm state # ...with cumulative counters — if these move, packets went through
A successful ping proves reachability; moving ESP counters prove encryption.
| swanctl | stroke | |
|---|---|---|
| Main config | /etc/swanctl/swanctl.conf |
/etc/ipsec.conf |
| Module writes tunnels to | /etc/swanctl/conf.d/remotswan.conf |
/etc/ipsec.conf |
| Secrets | in the config, secrets { } |
/etc/ipsec.secrets |
| Certificates | /etc/swanctl/x509/, x509ca/, … |
/etc/ipsec.d/certs/, cacerts/, … |
| Private keys | /etc/swanctl/private/, rsa/, … |
/etc/ipsec.d/private/ |
| Daemon tuning | /etc/strongswan.conf, /etc/strongswan.d/ |
same |
Per-user rights are set under Webmin → Webmin Users. Notable defaults:
- Secrets are denied by default (
secrets = 0). Being able to edit tunnels does not grant the right to read every pre-shared key on the box. - The raw config editor is off by default (
raw = 0). - Downloading a certificate never serves private-key material, from any directory, under any filename.
The module ships with a test suite that runs without a live Webmin:
cd /usr/share/webmin/remotswan # or your checkout
for t in test/*.pl; do perl "$t"; done
629 assertions across 15 suites cover the parsers, both backends' read/write paths, round-trip fidelity, the injection and path-traversal guards, and the secrets/certs security properties. Many are mutation-verified.
An end-to-end tunnel test builds two network namespaces, runs a real charon in each, and pings across so traffic must be encrypted by one and decrypted by the other (needs root):
sudo bash test/netns-tunnel.sh # single child
sudo RSWAN_CHILDREN=4 bash test/netns-tunnel.sh # four traffic-selector pairs
For a site-to-site PSK tunnel whose peer is a FortiGate, tools/s2s-handoff.pl turns a
VPN request form into a two-sided handoff: the StrongSwan conf.d file (through the module's
own writers, so it matches the UI byte-for-byte) plus the mirror-image FortiOS CLI, with a
README. Edit the %form block and run it:
cd /usr/share/webmin/remotswan # or your checkout
perl tools/s2s-handoff.pl /tmp/mytunnel
The defaults use RFC 5737 documentation addresses, so an unmodified run produces a safe, shareable example. The pre-shared key is never written — both sides carry a placeholder, so the key travels separately from the topology. Keep filled-in bundles (real addresses) out of the repo.
- The editor manages two authentication rounds per side (matching ipsec.conf's
leftauth/leftauth2); swanctl allows more, which need the raw editor. - AH is not supported. The editor writes
esp_proposalsonly, so a peer that insists on AH rather than ESP needs the raw editor. ESP is what essentially every modern peer uses. - Road-warrior EAP-RADIUS setups beyond the basics, and XAuth, are best checked by hand after saving.
- Converting ipsec.conf → swanctl (Convert to swanctl) is deliberately conservative: it never touches ipsec.conf, reports per tunnel what it cannot carry across, and does not convert secrets. Re-enter those afterwards.
module.info config* config.info defaultacl — Webmin module metadata
remotswan-lib.pl — entry point, backend dispatch, model
service-lib.pl — systemd unit discovery & control
swanctl-parse-lib.pl — nested-brace parser/writer
swanctl-backend.pl stroke-backend.pl — the two backends
cert-lib.pl — certificate discovery/introspection
*.cgi — the pages
lang/en help/*.html images/ — text, help, icons
test/ — the test suite (not shipped)
tools/ — developer helpers (not shipped)