Configure Squid for HTTPS on Debian VM

Verify we’re running the latest version of Debian

lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 11 (bullseye)
Release:	11
Codename:	bullseye

Become root

sudo su

Update packages

apt update && apt upgrade -y

Install the Squid package that has openssl configured and enabled

apt install squid-openssl

Create a local CA, using a 4096-bit key and SHA-2 hashing. This one is good for the next 10 years

openssl req -new -newkey rsa:4096 -sha256 -days 3653 -nodes -x509 -keyout /etc/squid/CA.key -out /etc/squid/CA.crt

Combine the key and cert in to a single file for convenience

cat CA.key CA.crt > CA.pem

Initialize the directory used for minted certs and set permissions so squid owns it

/usr/lib/squid/security_file_certgen -c -s /var/spool/squid/ssl_db -M 4MB
chown -R proxy:proxy /var/spool/squid

Finally, configure Squid to use HTTPS

http_port 3128 ssl-bump cert=/etc/squid/CA.pem generate-host-certificates=on options=NO_SSLv3
ssl_bump bump all

Restart Squid

service squid restart

Test connections by configuring 3128. Note the certificate from the CA, good for 10 years:

export https_proxy=http://localhost:3128

curl -v --cacert CA.crt 

* Uses proxy env variable https_proxy == 'http://localhost:3128'
*   Trying ::1:3128...
* Connected to localhost (::1) port 3128 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to
> Host:
> User-Agent: curl/7.74.0
> Proxy-Connection: Keep-Alive
< HTTP/1.1 200 Connection established
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: CA.crt
*  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CONNECT phase completed!
* CONNECT phase completed!
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject:
*  start date: Nov  6 04:03:48 2022 GMT
*  expire date: Nov  6 04:03:48 2032 GMT
*  subjectAltName: host "" matched cert's ""
*  issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=localhost
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 418 I'm a teapot
< Server: nginx
< Date: Sun, 06 Nov 2022 04:08:13 GMT
< Content-Type: application/json
< Content-Length: 483
< X-Cache: MISS from test-1
< X-Cache-Lookup: MISS from test-1:3128
< Via: 1.1 test-1 (squid/4.13)
< Connection: keep-alive
    "host": "",
    "user-agent": "curl/7.74.0",
    "x-forwarded-for": "::1,,",
    "x-forwarded-proto": "https",


VPNs to Google Cloud Platform (GCP) when FortiGate is behind a NAT gateway

Ran in to problems getting a VPN up and running between GCP and a FortiGate 60-E that was behind a NAT gateway (with ports udp/500 + udp/4500 forwarded). On the GCP side, these messages would show up in the logs:

remote host is behind NAT
generating IKE_AUTH response 1 [ N(AUTH_FAILED) ]
looking for peer configs matching GCP.VPN.GATEWAY.IP[%any]...[]

This error means that GCP connected to the Peer VPN gateway successfully, but it in the IKEv2 headers, it identified itself by the private IP rather than the expected public one. AWS is not picky about this, but with GCP, the Peer VPN gateway must identify itself by using the same external IP address of the NAT device.

Most vendors have long supported an option to manually override the IP address for such scenarios. In Cisco IOS or IOS-XE, this can be controlled in the IKEv2 profile with the identity local address option:

crypto ikev2 profile GCP_IKEV2_PROFILE
 match address local interface GigabitEthernet1
 identity local address MY.PUBLIC.IP.ADDRESS
 authentication remote pre-share
 authentication local pre-share
 keyring local GCP_KEYRING
 lifetime 36000
 dpd 20 5 on-demand

With Palo Alto, this is configured in the IKE Gateway, Local Identification field:

For the sake of argument, we’ll say that CheckPoint uses the “Statically NATed IP” field to influence Local ID, although this doesn’t actually work.

Fortigate does offer “Local ID” field in version 6.4.6 and higher, under the Phase 1 proposal:

Seems nice and straightforward, but even after changing this setting, the VPN tunnel still won’t establish. Logs on the GCP end change slightly and now show this:

looking for peer configs matching GCP.VPN.GATEWAY.IP[%any]...[]

The private IP is no longer showing, so it seems the issue should be solved. Instead, GCP reports a “Peer not responding” message. The Fortigate actually reports Phase 1 success, waits a few seconds, and then starts the negotiation all over. So not very helpful.

I configured a test VPN between the FortiGate and a Palo Alto, which then gave a very specific and extremely useful error message:

IKE phase-1 negotiation is failed. When pre-shared key is used, peer-ID must be type IP address. Received type FQDN

Now this explains the problem! Even though the FortiGate is sending the correct IP address in the IKEv2 header, it’s being sent as the wrong identity type. The 5 identity types are listed in RFC 7815:

  • ID_IPV4_ADDR = 32 bit IPv4 address
  • ID_IPV6_ADDR = 128 bit IPv6 address
  • ID_FQDN = DNS hostname
  • ID_RFC822_ADDR = e-mail address
  • ID_KEY_ID = octet stream

If Fortigate were smart, it would either default to IPv4 address type or auto-determine this based on the text inputted in to the field. But it seems to simply default to FQDN. Oddly, there is a CLI option called “localid-type” under the Phase1-interface that clear is intended to provide this functionality:

FGT60E1234567890 # config vpn ipsec phase1-interface

FGT60E1234567890 (phase1-interface) # edit gcp

FGT60E1234567890 (gcp) # set localid-type 
auto         Select ID type automatically.
fqdn         Use fully qualified domain name.
user-fqdn    Use user fully qualified domain name.
keyid        Use key-id string.
address      Use local IP address.

But, similar to CheckPoint, it just doesn’t work, and can be considered a broken feature.

Since GCP does not support FQDN authentication, VPNs between GCP and FortiGates behind a NAT are not possible at this time.

Policy-Based VPNs on Cisco ISRs when behind NAT

A couple years ago I wrote a post about route-based IPSec VPNs involving NAT-T on Cisco Routers. However today I had to setup a lab environment using policy-based VPNs. This was a real blast from past as I hadn’t done a policy-based VPN on a Cisco router since the late 1990s :-O

VPN Parameters:

  • Local side, private IP of external interface of router:
  • Local side, private IP subnet
  • Local side, public IP address:
  • Remote side, public IP address:
  • Remote side, private IP subnet:
  • Pre-shared key: MySecretKey1234
  • Phase 1 encryption and lifetime: AES-256, SHA-384, Group 14, 1 day
  • Phase 2 encryption and lifetime: AES-128, SHA-1, Group 2, 1 hour

With both IKEv1 or v2, you’ll want to start by verifying NAT-T is enabled, which is the default setting. This will allow the router to detect behind behind NAT and tunnel traffic on udp/4500 rather than using regular ESP (protocol 50):

crypto ipsec nat-transparency udp-encapsulation

If the other side is expecting or requiring NAT-T and it’s been disabled, Cisco IOS will log this warning:

%IKEV2-3-NEG_ABORT: Negotiation aborted due to ERROR: NAT-T disabled via cli


As with route-based VPN, I start by setting some global ISAKMP parameters:

crypto isakmp disconnect-revoked-peers
crypto isakmp invalid-spi-recovery
crypto isakmp keepalive 30 2 on-demand
crypto isakmp nat keepalive 900

The ISAKMP policy defines global encryption and authentication settings.

! 256-bit AES + SHA2-384 + PFS Group14 (2048-bit key)
crypto isakmp policy 100
 encr aes 256
 hash sha384
 authentication pre-share
 group 14
 lifetime 86400              ! 1 day, which is the default

Configure authentication for the peer by defining a keyring, specifying the public IP of the remote side. Then create an ISAKMP profile, again specifying the remote’s public IP and the local’s external interface:

crypto keyring CRYPTO_KEYRING
  local-address GigabitEthernet0/0
  pre-shared-key address key MySecretKey1234
crypto isakmp profile ISAKMP_PROFILE
   match identity address 
   local-address GigabitEthernet0/0

Now the crypto map, which replaces the crypto ipsec profile of route-based VPNs. I’m just using the typical encryption settings of 128-bit AES/SHA-1/Group2 PFS. The access-list must be defined to match “interesting” traffic to send across the VPN.

access-list 101 permit ip
crypto ipsec security-association replay window-size 1024
crypto ipsec df-bit clear
crypto ipsec transform-set ESP_AES128_SHA esp-aes esp-sha-hmac 
 mode tunnel
crypto map CRYPTO_MAP 1 ipsec-isakmp 
 set peer
 set security-association lifetime seconds 3600      ! 1 hour, which is the default
 set transform-set ESP_AES128_SHA
 set pfs group2
 match address 101

Finish by applying the crypto map to the external interface:

ip route
interface GigabitEthernet0/0
 ip address
 crypto map CRYPTO_MAP
interface GigabitEthernet0/1
 ip address

Send a ping that matches the interesting traffic. Make sure to use an interface that’s with the source IP range specific on the ACL referenced by the Crypto Map.

Router# ping source Gi0/1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to, timeout is 2 seconds:
Packet sent with a source address of 
Success rate is 80 percent (4/5), round-trip min/avg/max = 68/71/72 ms

Verify IPSEC SAs are up:

Router# show crypto ipsec sa peer

interface: GigabitEthernet0/0
    Crypto map tag: CRYPTO_MAP, local addr

   protected vrf: (none)
   local  ident (addr/mask/prot/port): (
   remote ident (addr/mask/prot/port): (
   current_peer port 4500
     PERMIT, flags={origin_is_acl,}
    #pkts encaps: 4, #pkts encrypt: 4, #pkts digest: 4
    #pkts decaps: 4, #pkts decrypt: 4, #pkts verify: 4


I always start IKEv2 configuration with some global settings:

crypto ikev2 nat keepalive 900
crypto ikev2 dpd 30 2 on-demand
crypto logging ikev2

As with route-based VPN, configure an IKEv2 proposal and policy. Here’s a broad one that should match anything with reason:

crypto ikev2 proposal IKEV2_PROPOSAL
 encryption aes-cbc-256 aes-cbc-128 3des
 integrity sha512 sha384 sha256 sha1
 group 21 20 19 16 14 2
crypto ikev2 policy IKEV2_POLICY 
 match fvrf any
 proposal IKEV2_PROPOSAL

Create a keyring entry for the other side specifying their public IP, then an IKEv2 profile. If the other side is expecting to see the public IP address, configure that with the identity local address option. The match identity remote address must match their IKEv2 remote ID. This usually will be the public IP, but may require the private IP if they are also behind NAT and not overriding.

crypto ikev2 keyring IKEV2_KEYRING
 peer TEST1
  pre-shared-key MySecretKey1234
crypto ikev2 profile IKEV2_PROFILE
 match address local interface GigabitEthernet0/0
 match identity remote address     ! Other side's remote ID
 identity local address            ! My public IP
 authentication local pre-share
 authentication remote pre-share
 keyring local IKEV2_KEYRING
 dpd 60 5 on-demand             ! override global DPD setting, if desired

Crypto map is same as IKEv1 (see above), just with the IKEv2 profile specified:

crypto map CRYPTO_MAP 1 ipsec-isakmp 
 set ikev2-profile IKEV2_PROFILE

Finally apply crypto map to external interface. The IKEv2 SA should pop up within a few seconds.

*Feb 26 22:07:41 PST: %IKEV2-5-SA_UP: SA UP

Verify details of the IKEv2 SA:

Router# show crypto ikev2 sa remote detailed 
 IPv4 Crypto IKEv2  SA 

Tunnel-id Local                 Remote                fvrf/ivrf            Status 
1    none/none            READY  
      Encr: AES-CBC, keysize: 256, PRF: SHA384, Hash: SHA384, DH Grp:14, Auth sign: PSK, Auth verify: PSK
      Life/Active Time: 86400/115 sec
      CE id: 1007, Session-id: 4
      Status Description: Negotiation done
      Local spi: 55543FD20BD46FA2       Remote spi: 03B6B07E9090FCF2
      Local id:
      Remote id:
      Local req msg id:  0              Remote req msg id:  14        
      Local next msg id: 0              Remote next msg id: 14        
      Local req queued:  0              Remote req queued:  14        
      Local window:      5              Remote window:      1         
      DPD configured for 10 seconds, retry 2
      Fragmentation not  configured.
      Extended Authentication not configured.
      NAT-T is detected inside 
      Cisco Trust Security SGT is disabled
      Initiator of SA : No

 IPv6 Crypto IKEv2  SA

As with IKEv1, the final step is verify the IPSEC SA.

IPSec VPN Spoke Router using FQDN authentication

In a previous post I’d covered doing site-to-site IPSec tunnels on Cisco routers when one or both devices are behind NAT.  But what if multiple spoke routers have dynamic IP addresses? Or how about many behind the same NAT address? 

The solution is to have the spoke routers authenticate using FQDN hostname rather than IP address.  I’ve seen lots of examples which overly complicate how to do this.  It’s actually pretty simple and only requires minor changes.  Let’s assume the spoke routers have dynamic IPs and the hub has a static IP of…


Spoke Router

On spoke routers with IKEv1, simply replace  the “crypto keyring” statement with “crypto isakmp peer” to use FQDN authentication and IKE aggressive mode, like this:

hostname spoke1
no crypto keyring MyHub
crypto isakmp peer address [vrf InternetVRFName]
 set aggressive-mode password XXXXXXXX
 set aggressive-mode client-endpoint fqdn 

Note the fqdn hostname doesn’t necessarily need to match the router’s hostname


Spoke Router

Set the Hub’s IP address and pre-shared key in an IKEv2 keyring:

crypto ikev2 keyring MY_IKEV2_KEYRING
 peer MyHub
  pre-shared-key MySecretKey1234    ! Must be 16 chars or longer

The identity (hostname) in the IKEv2 profile via the identity local line:

hostname spoke1
crypto ikev2 profile SPOKE_IKEV2_PROFILE
 match address local interface GigabitEthernet0/0
 match identity remote address
 identity local fqdn
 authentication remote pre-share
 authentication local pre-share
 keyring local MY_IKEV2_KEYRING
 dpd 20 2 periodic 

IPSec profile:

crypto ipsec profile SPOKE_IPSEC_PROFILE
 set security-association lifetime kilobytes disable
 set pfs group14
 set ikev2-profile SPOKE_IKEV2_PROFILE

Tunnel Interface and static route to

interface Tunnel1
 ip address
 ip tcp adjust-mss 1379
 keepalive 10 3
 tunnel source GigabitEthernet0/0
 tunnel mode ipsec ipv4
 tunnel destination
 tunnel protection ipsec profile SPOKE_IPSEC_PROFILE
ip route Tunnel1

Hub Router

Each Spoke will need an entry. Use identity in the entry, not hostname

crypto ikev2 keyring SPOKES_IKEV2_KEYRING
 peer spoke1
  identity fqdn
  pre-shared-key MySecretKey1234

The IKEv2 profile will be a bit different. The domain is used to match multiple spokes:

crypto ikev2 profile HUB_IKEV2_PROFILE
 match address local interface GigabitEthernet0/0
 match identity remote fqdn domain
 identity local fqdn
 authentication remote pre-share
 authentication local pre-share
 keyring local SPOKES_IKEV2_KEYRING
 dpd 10 2 on-demand
 virtual-template 1

The IPSec profile is almost the same but is responder-only (passive):

crypto ipsec profile HUB_IPSEC_PROFILE
 set security-association lifetime kilobytes disable
 set pfs group14
 set ikev2-profile HUB_IKEV2_PROFILE

Rather than a regular tunnel interface, a virtual template one is used:

interface Virtual-Template1 type tunnel
 ip unnumbered Loopback0
 tunnel source GigabitEthernet0/0
 tunnel mode ipsec ipv4
 tunnel destination dynamic
 tunnel protection ipsec profile HUB_IPSEC_PROFILE

Cisco CSR1000v in AWS: IPSec tunnel configuration

Using IKEv1 w/ IPSec tunnels, the PSK address and tunnel destination should be the public IP of the remote side, even if the other router is behind NAT using Elastic IP:

crypto isakmp key XXXXXXXX address PUBLIC.IP.OF.REMOTE
crypto isakmp invalid-spi-recovery
crypto isakmp keepalive 10 10
crypto ipsec security-association replay window-size 1024
crypto ipsec transform-set ESP_AES128_SHA esp-aes esp-sha-hmac 
 mode tunnel
crypto ipsec profile ESP_3600_PFS-G2
 set security-association lifetime kilobytes disable
 set transform-set ESP_AES128_SHA  
 set pfs group2
interface Tunnel1
 ip address
 keepalive 10 3
 tunnel source GigabitEthernet1
 tunnel mode ipsec ipv4
 tunnel destination PUBLIC.IP.OF.REMOTE
 tunnel protection ipsec profile ESP_3600_PFS-G2
interface GigabitEthernet1
 ip address
 ip nat outside
 negotiation auto
 no mop enabled
 no mop sysid
ip route

I was surprised this worked.  Why?  Because the tunnel source is GigabitEthernet1’s private IP address of  I would expect the other side to reject the proposal because it doesn’t match the public IP address with the isakmp key configured.

But looking closer, it’s actually  using NAT-T (udp port 4500):

csr1000v-1#show crypto session 

Interface: Tunnel1
Session status: UP-ACTIVE
Peer: PUBLIC.IP.OF.REMOTE port 4500 
IPSEC FLOW: permit ip 
Active SAs: 2, origin: crypto map



FortiGate Static NAT using Port Forwarding / PAT

Easy in hindsight, but may be counter-intuitive for those coming from a Cisco or Palo Alto background such as myself.  There are two steps:

  1. Under Policy & Objects -> Virtual IPs, add a statement for each PAT rule with the “Port Forwarding” switch enabled at the bottom.
  2. Under Policy & Objects -> IPv4 Policy, add a rule from the public interface to the private interface with destination to be the object(s) created and service set to ALL.  NAT switch should remain disabled.



SSH Cipher Updates in Cisco ASA 9.4(3)12

After upgrading our Cisco ASAs from 9.4(3)11 to 9.4(3)12, Rancid could no longer log in.  Debugging by manually running clogin, the problem was clear: incompatibility with SSH ciphers.  Rancid wanted to use 3DES (“Triple DES”), but the ASA only supported AES.

rancid@localhost:~$ clogin
spawn ssh -c 3des -x -l rancid
no matching cipher found: client 3des-cbc server aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr

By default, the ASA’s SSH server uses the “medium” cipher list.  Sure enough, 3DES is no longer in the list:

ciscoasa/pri/act# show ssh ciphers 
Available SSH Encryption and Integrity Algorithms
Encryption Algorithms:
  low: 3des-cbc aes128-cbc aes192-cbc aes256-cbc aes128-ctr aes192-ctr aes256-ctr 
  medium: aes128-cbc aes192-cbc aes256-cbc aes128-ctr aes192-ctr aes256-ctr
  fips: aes128-cbc aes256-cbc  
  high: aes256-cbc aes256-ctr 

A quick and dirty work-around: configure the ASA to use the “low” cipher list.  However, since it’s time to start phasing out 3DES anyway (it’s from the 90s), I instead wanted to tell Rancid to prefer AES and only use 3DES as a last resort.  The first step was finding the possible cipher names, which were in /etc/ssh/ssh_config:

# Ciphers aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc

I simplified this a bit and added this line to Rancid’s .cloginrc:

add cyphertype * aes128-ctr,aes128-cbc,3des-cbc

This preference matches most of my devices since AES-CTR is supported in IOS 15 and is preferred over AES-CBC and 3DES.  Good enough for me.

NAT Hairpinning on Cisco ISR

I’ve never had a need to do NAT hairpinning on a Cisco ISR, as I’d typically have a fancy firewall like an ASA doing the work.  However, with this blog now hosted on a NAS inside my home network, I’ve found it necessary to support it.  Hairpinning essentially means the internal server is available via the public (global) IP address, even when coming from the private (local) network.  I didn’t want to forge DNS entries because it’s a pain to manage, and, well, it’s just wrong.

First, here’s my traditional NAT configuration.  Fa0/0 is the public interface connected to the ISP.  BVI is the Layer 3 private interface.

interface FastEthernet0/0
 ip address dhcp
 ip nat outside
interface Vlan1
 no ip address
 bridge-group 1
interface BVI1
 ip address
 ip nat inside
ip nat inside source list NATLIST interface FastEthernet0/0 overload
ip nat inside source static tcp 80 interface FastEthernet0/0 80
ip access-list extended NATLIST
 deny ip any
 deny ip any
 deny ip any
 permit ip any any
bridge 1 protocol ieee
bridge 1 route ip

Now the new config.  Pretty simple, but there’s a gotcha: the requirement for no ip redirects on both outside and inside interfaces.

interface FastEthernet0/0
 ip address dhcp
 no ip redirects
 ip nat enable
interface BVI1
 ip address
 no ip redirects
 ip nat enable
ip nat source list NATLIST interface FastEthernet0/0 overload
ip nat source static tcp 80 interface FastEthernet0/0 80

And here comes the gotcha: performance.  After switching to this configuration, my throughput over NAT went from about 90 Mbps to 15 Mbps.  Ouch.  Saw these numbers both on a 2811 and 1841.

Fixing broken Microsoft RDP connections through a Cisco ASA terminating a site-to-site VPN

By default the Cisco ASA has a TCP MSS size of 1380.  On larger packets coming over a VPN tunnel, it won’t be able to process these.  Microsoft RDP is the most common example although it can also be observed with protocols like FTP.

The quick fix is make this change on the ASA:

sysopt connection tcp-mss 1300

This will cause the packets to be fragmented, and pass successfully over the VPN and through the ASA.